Cypher 호환성#
AgensGraph의 Cypher는 다른 구현과 충분히 비슷해서 대부분의 쿼리가 고치지 않고 넘어옵니다. 그러면서도 몇몇은 실패하거나, 더 나쁘게는 다른 답을 내며 성공할 만큼 다릅니다. 이 부록은 그 차이를, 서버가 내는 오류나 돌려주는 결과와 함께 나열합니다. 옮겨 온 쿼리가 이상하게 동작할 때 그 원인을 알아볼 수 있게 하려는 것입니다.
모든 예제는 graph_path를 moviekb로 둔 채
예제 데이터셋을 상대로 실행합니다.
아래의 대부분은 구조에 관한 두 사실로 설명됩니다. 정점은 레이블을 정확히 하나 가집니다. 레이블이 테이블이고 행은 테이블 하나에 살기 때문입니다. 그리고 Cypher 쿼리는 PostgreSQL 쿼리이므로, 렉서와 타입 체계와 오류 코드가 PostgreSQL의 것입니다.
존재하지 않는 구문#
관계 패턴 하나에 여러 타입#
관계 패턴은 타입을 많아야 하나 받습니다:
MATCH (p:person)-[r:acted_in|directed]->(m:movie) RETURN count(r);
ERROR: multiple types for relationship not supported
가변 길이(-[r:acted_in|directed*1..2]->)에서도, shortestpath() 안에서도 같은 오류가 돌아옵니다.
-[r:acted_in|:directed]-> 표기는 그냥 구문 오류입니다. 대신 레이블을 지정하지 않은 간선을 일치시킨 뒤 레이블을
검사하십시오:
MATCH (p:person)-[r]->(m:movie)
WHERE label(r) IN ['acted_in','directed']
RETURN count(r);
count
-------
46
정점 레이블 선택도 같은 식으로 실패합니다. MATCH (n:person|movie)는 |에서 구문 오류이고, GQL
표준이 정의하는 다른 레이블 표현식 연산자 :person&movie·:!person·:%도 마찬가지입니다.
정점의 레이블 집합#
레이블의 집합이란 것이 없고, 정점이 가진 레이블을 바꿀 방법도 없습니다. 아래는 모두 두 번째 콜론에서 구문 오류입니다:
MATCH (n:person {name:'Keanu Reeves'}) SET n:actor RETURN n;
MATCH (n:person {name:'Keanu Reeves'}) REMOVE n:actor RETURN n;
MATCH (n:person:movie) RETURN count(n);
CREATE (:aa:bb);
ERROR: syntax error at or near ":"
그 자리를 레이블 상속이 대신합니다. CREATE VLABEL actor INHERITS (person)은 별도의 레이블을 만들고, 부모 레이블과
일치시키면 이 레이블의 행도 함께 반환됩니다. 그래서 MATCH (n:person)은 배우도 찾고,
MATCH (n:person ONLY)는 그들을 뺍니다. 정점은 배우로 승격되는 것이 아니라 처음부터 배우로
만들어집니다. 레이블 상속을 참고하십시오.
FOREACH#
FOREACH (x IN [1,2] | CREATE (:zz {v:x}));
ERROR: syntax error at or near "FOREACH"
UNWIND와 GQL의 FOR 문장이 둘 다 리스트를 순회합니다:
UNWIND [1,2] AS x RETURN x;
x
---
1
2
둘 중 어느 것 뒤에도 쓰기 절이 올 수 있으므로, UNWIND list AS x CREATE (…)가 FOREACH 본문이
담는 것을 대신합니다.
CREATE UNIQUE#
MATCH (a:person {name:'Keanu Reeves'}), (b:movie {title:'Avatar'})
CREATE UNIQUE (a)-[:acted_in]->(b);
ERROR: syntax error at or near "UNIQUE"
패턴을 일치시키고 일치가 없을 때만 만드는 MERGE를 쓰십시오. 여기서는 간선이 이미 있으므로
MERGE가 하나를 더 만드는 대신 있는 것을 반환합니다:
MATCH (a:person {name:'Sam Worthington'}), (b:movie {title:'Avatar'})
MERGE (a)-[r:acted_in]->(b)
RETURN id(r);
id
------
5.21
LOAD CSV#
LOAD CSV WITH HEADERS FROM 'file:///tmp/movies.csv' AS row RETURN row;
ERROR: syntax error at or near "CSV"
가져오기 경로는 테이블을 거칩니다. COPY나 외부 테이블이 CSV를 릴레이션에 넣고, LOAD FROM이 그
행을 그래프 요소로 바꿉니다:
CREATE TABLE people_src (name text, age int);
INSERT INTO people_src VALUES ('Ann', 30), ('Bo', 41);
LOAD FROM people_src AS r CREATE (:person {name: r.name, age: r.age});
UPDATE 2
DROP TABLE people_src;
외부 테이블 래퍼를 포함한 전체 절차는 데이터 적재를 참고하십시오.
익명 화살표 -->와 --#
--는 SQL에서와 마찬가지로 주석을 시작하므로, 익명 화살표는 문장을 조용히 끝내 버립니다:
MATCH (a:movie)-->(b) RETURN count(*);
ERROR: Cypher query must end with RETURN, FINISH or update clause
<--는 다르게 실패합니다. 주석이 시작되기 전에 <가 먼저 소비되기 때문입니다:
MATCH (a:movie)<--(b) RETURN count(*);
ERROR: syntax error at end of input
대괄호를 쓴 형태로 적으십시오. -[]->·<-[]-·-[]-가 모두 동작합니다:
MATCH (a:movie)-[]->(b) RETURN count(*);
count
-------
11
맵 투영#
MATCH (m:movie {title:'Avatar'}) RETURN m {.title, .year};
ERROR: syntax error at or near "{"
맵을 직접 만드십시오. 맵 생성자는 평범한 표현식입니다:
MATCH (m:movie {title:'Avatar'}) RETURN {title: m.title, year: m.year};
?column?
-----------------------------------
{"year": 2009, "title": "Avatar"}
properties(m)이 맵 전체를 반환하므로 m {.*}를 대신합니다.
패턴 컴프리헨션#
MATCH (m:movie {title:'Avatar'}) RETURN [(m)<-[:acted_in]-(p) | p.name];
ERROR: syntax error at or near ":"
서브쿼리 표현식이 같은 일을 하며, 언어의 나머지 부분과 더 비슷하게 읽힙니다:
MATCH (m:movie {title:'Avatar'})
RETURN COLLECT { MATCH (m)<-[:acted_in]-(p) RETURN p.name };
array
---------------------
["Sam Worthington"]
reduce()·extract()·filter()#
셋 다 없습니다.
RETURN reduce(acc = 0, x IN [1,2,3] | acc + x);
ERROR: variable does not exist
extract와 filter는 이미 다른 데 쓰이고 있으므로 extract()와 filter()는 대신 구문
오류입니다. extract는 SQL의 EXTRACT(field FROM source)가, filter는 GQL의 FILTER 문장이
차지하고 있습니다.
리스트 컴프리헨션이 extract()와 filter()를 대신하며, 세 가지 표기가 모두 동작합니다:
RETURN [x IN [1,2,3] | x * 10], [x IN [1,2,3] WHERE x > 1], [x IN [1,2,3] WHERE x > 1 | x * 10];
?column? | ?column? | ?column?
--------------+----------+----------
[10, 20, 30] | [2, 3] | [20, 30]
접기(fold)가 필요하면 Cypher 결과 위에서 SQL로 집계하거나, 그 접기가 합이라면 sum()을
쓰십시오.
패턴 자체를 조건으로 쓰기#
패턴은 불리언 표현식이 아닙니다:
MATCH (m:movie) WHERE (m)<-[:acted_in]-() RETURN count(m);
ERROR: syntax error at or near ":"
exists()로, 또는 GQL의 EXISTS { } 서브쿼리 표현식으로 감싸십시오:
MATCH (m:movie) WHERE exists((m)<-[:acted_in]-()) RETURN count(m);
count
-------
18
백틱으로 감싼 식별자#
백틱은 따옴표 문자가 아닙니다:
MATCH (m:`movie`) RETURN count(m);
ERROR: syntax error at or near "`"
식별자는 SQL 방식대로 큰따옴표로 감쌉니다. MATCH (m:"movie")입니다. 2.18에서 예약어가 된
단어들에 이것이 중요합니다. 키워드를 참고하십시오.
시간·공간·거리 생성자#
duration()·distance()·haversin()은 없습니다:
RETURN haversin(1.0);
ERROR: function haversin(numeric) does not exist
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
point()와 timestamp()는 더 헷갈리게 실패합니다. 각각 PostgreSQL의 이름과 부딪히기 때문입니다.
point({x:1, y:2})는 PostgreSQL의 기하 타입 point를 찾는데, 그 오버로드는 실수 쌍과 다른 기하
타입을 받으므로 호출이 없는 것이 아니라 모호해집니다:
RETURN point({x:1, y:2});
ERROR: function point(jsonb) is not unique
HINT: Could not choose a best candidate function. You might need to add explicit type casts.
timestamp()는 구문 오류입니다. timestamp가 타입 이름이기 때문입니다.
datetime()은 있지만 다른 함수이며, 특히 조심해야 할 것입니다. 시간대나 날짜 구성 요소의 목록을
받고 ISO-8601 문자열은 결코 받지 않습니다. 타임스탬프 문자열을 넘기면 그것을 시간대로 읽습니다:
RETURN datetime('2020-01-01T00:00:00');
ERROR: time zone "2020-01-01T00:00:00" not recognized
시간대 이름이나 날짜 구성 요소 아홉 개는 받습니다:
RETURN datetime(2020, 1, 1, 0, 0, 0, 0, 0, 'UTC');
datetime
-----------------------
"2020-01-01T00:00:00"
문자열에서 타임스탬프를 얻으려면 형 변환하십시오. '2020-01-01'::date,
'2020-01-01 00:00'::timestamptz입니다. CURRENT_DATE와 CURRENT_TIMESTAMP는 키워드만으로
동작합니다. 기간은 PostgreSQL의 interval이며 SQL에서 ::interval로 닿습니다. 공간 관련 작업은
PostgreSQL 확장을 거치며, 벡터 검색과 같은 방식으로 그래프 쿼리에서 닿습니다.
벡터 검색을 참고하십시오.
db.* 프로시저#
db 네임스페이스는 없습니다:
CALL db.labels();
ERROR: schema "db" does not exist
같은 물음에는 카탈로그가 답합니다. ag_label이 그래프의 레이블을 나열합니다.
그래프 카탈로그를 참고하십시오.
PROFILE#
PROFILE MATCH (m:movie) RETURN count(m);
ERROR: syntax error at or near "PROFILE"
EXPLAIN이 PostgreSQL의 옵션과 함께 동작합니다. EXPLAIN (ANALYZE, BUFFERS) MATCH …입니다.
동작이 다른 것#
RETURN이 내놓는 모든 컬럼은 jsonb다#
속성 읽기는 jsonb 값이고 투영을 거쳐도 그대로입니다. 그래서 문자열은 JSON 따옴표와 함께
찍힙니다:
MATCH (m:movie {title:'Avatar'}) RETURN m.title, pg_typeof(m.title);
title | pg_typeof
----------+-----------
"Avatar" | "jsonb"
::text는 따옴표를 없애지 않습니다. 따옴표를 포함해 jsonb 값을 그려 낼 뿐입니다. 평범한 스칼라를
얻으려면 Cypher를 감싼 SQL에서 뽑아내십시오:
SELECT t #>> '{}' AS title
FROM (MATCH (m:movie) RETURN m.title AS t) q
ORDER BY title LIMIT 1;
title
----------
Avatar
SQL과 Cypher를 참고하십시오.
ORDER BY 2는 아무것도 정렬하지 않는다#
정렬 키의 정수는 출력 컬럼 번호가 아니라 상수입니다. 모든 행을 같은 상수로 정렬하면 순서가 그대로 남고, 아무것도 경고해 주지 않습니다:
MATCH (m:movie) RETURN m.title AS t, m.year AS y ORDER BY 2 LIMIT 5;
t | y
----------------------------+------
"The Matrix Resurrections" | 2021
"Inception" | 2010
"Interstellar" | 2014
"Avatar" | 2009
"Blade Runner" | 1982
키의 이름을 대십시오. ORDER BY y는 기대한 대로 정렬합니다. 정수 형태는 SQL에서는 동작하며,
거기서 정수 정렬 키는 그 자리의 출력 컬럼을 뜻합니다. 그러므로 Cypher를
SELECT … ORDER BY 2로 감싸도 정렬됩니다.
MATCH (m:movie) RETURN m.title AS t, m.year AS y ORDER BY y LIMIT 5;
t | y
------------------------------+------
"Blade Runner" | 1982
"The Terminator" | 1984
"Total Recall" | 1990
"Terminator 2: Judgment Day" | 1991
"Jurassic Park" | 1993
n.id는 식별자가 아니다#
id는 평범한 속성 이름입니다. 무엇인가 값을 써 두지 않았다면 없습니다:
MATCH (m:movie {title:'Avatar'}) RETURN m.id, id(m);
id | id
----+-----
| 3.7
id(m)이 식별자, 즉 labid.locid로 찍히는 graphid를 반환합니다. 그 두 부분이 무엇을
담는지는 제한을 참고하십시오.
그냥 쓴 CALL { }은 다른 물음에 답한다#
서브쿼리 본문은 바깥의 변수를 보지 못합니다. 그 이름이 아무 제약 없는 새 변수로 다시 바인딩되므로, 본문은 지금 행이 아니라 그래프 전체를 상대로 실행됩니다:
MATCH (d:person)-[:directed]->(m:movie)
CALL { MATCH (a:person)-[:acted_in]->(m) RETURN count(a) AS cast_size }
RETURN m.title AS film, cast_size ORDER BY film LIMIT 3;
film | cast_size
---------------------+-----------
"Avatar" | 25
"Blade Runner" | 25
"Blade Runner 2049" | 25
25는 그래프에 있는 acted_in 간선 전체의 개수입니다. 가져올 변수 목록을 적으면 같은 쿼리가
올바릅니다:
MATCH (d:person)-[:directed]->(m:movie)
CALL (m) { MATCH (a:person)-[:acted_in]->(m) RETURN count(a) AS cast_size }
RETURN m.title AS film, cast_size ORDER BY film LIMIT 3;
film | cast_size
---------------------+-----------
"Avatar" | 1
"Blade Runner" | 1
"Blade Runner 2049" | 2
중요
CALL 서브쿼리에는 언제나 가져올 변수 목록을 주십시오. CALL (v) { … }·CALL (*) { … }·
CALL { WITH v … } 셋 다 올바르고 서로 일치합니다. 이 차이를 보이는 형태 가운데 오류를 내지 않고 엉뚱한 결과를 내는 것은
그냥 쓴 형태뿐입니다.
shortestpath의 양 끝점은 이름 있는 변수여야 한다#
끝점에 패턴을 직접 적는 것은 거부됩니다:
MATCH p = shortestpath((:movie {title:'The Matrix'})-[:related_to*..4]->(:movie {title:'Inception'}))
RETURN length(p);
ERROR: node must be a reference to a specific node
양 끝을 먼저 바인딩하십시오:
MATCH (a:movie {title:'The Matrix'}), (b:movie {title:'Inception'})
MATCH p = shortestpath((a)-[:related_to*..4]->(b))
RETURN length(p);
length
--------
2
같은 규칙이 allshortestpaths()와 dijkstra()에도 적용됩니다.
따옴표 없는 레이블은 소문자로 바뀐다#
식별자는 SQL의 대소문자 변환 규칙을 따르므로, 대소문자를 섞어 쓴 레이블은 소문자로 찾습니다:
MATCH (m:Movie) RETURN count(m);
count
-------
18
Movie·MOVIE·movie는 같은 레이블입니다. 따옴표로 감싼 대소문자 혼용 이름으로 만든 레이블은
쓸 때마다 따옴표로 감싸야 합니다.
조건은 불리언만 받는다#
속성을 참·거짓처럼 읽는 것은 비어 있지 않은지 검사하는 대신 형 변환 오류를 냅니다:
MATCH (m:movie {year: 1999}) WHERE m.title RETURN count(m);
ERROR: cannot cast "The Matrix" (jsonb type string) to boolean
메시지는 실행기가 처음 닿은 값을 가리키므로, 여러 행에 걸친 제약 없는 MATCH는 마침 먼저 읽은
값을 알려 줍니다.
이 규칙은 WHERE·FILTER·AND·OR·NOT·XOR·CASE WHEN과 dijkstra()의 조건 인자에 모두 적용됩니다.
뜻하는 검사를 그대로 쓰십시오. WHERE m.title IS NOT NULL이나 WHERE m.count <> 0처럼 말입니다.
아니면 ::boolean으로 명시적으로 변환하십시오.
없는 레이블은 오류가 아니다#
존재하지 않는 레이블을 가리키는 패턴은 아무것과도 일치하지 않고 행 수 0을 반환합니다:
MATCH (n:no_such_label) RETURN count(n);
count
-------
0
그래서 레이블 이름의 오타는 빈 결과처럼 보입니다. 기대한 것이 하나도 나오지 않으면 ag_label의
레이블 목록을 확인하십시오.
size()와 length()는 같은 함수다#
size()는 length()의 다른 표기이며, 어느 쪽을 쓰든 출력 컬럼의 이름은 length입니다:
RETURN size([1,2,3]), size('abcd'), length('abcd');
length | length | length
--------+--------+--------
3 | 4 | 4
패턴에 대한 size()는 일치 수를 세며, 컬럼 이름은 count입니다:
MATCH (m:movie {title:'Avatar'}) RETURN size((m)<-[:acted_in]-());
count
-------
1
labels()는 언제나 레이블 하나를 반환한다#
labels()는 있고 리스트를 반환하지만, 정점이 레이블을 하나 가지므로 그 리스트의 원소는 정확히
하나입니다:
MATCH (n:person) RETURN labels(n) LIMIT 1;
labels
------------
["person"]
label(n)은 그 하나의 레이블을 스칼라로 반환하며, 이쪽이 더 나은 형태입니다. 간선에는 type(r)과
label(r)이 모두 동작합니다.
그래프 요소에 대한 collect()는 네이티브 배열을 반환한다#
정점을 모으면 사본의 목록이 아니라 vertex[]가 나오므로, 모은 요소를 그대로 쓸 수 있습니다:
MATCH (m:movie) RETURN pg_typeof(collect(m));
pg_typeof
------------
"vertex[]"
MATCH (m:movie) WITH collect(m) AS ms RETURN ms[0].title;
title
----------------------------
"The Matrix Resurrections"
속성을 모으는 것은 여전히 jsonb 리스트를 반환합니다.
간선이 달린 정점은 지워지지 않는다#
DELETE는 끝점이 없는 간선을 남기는 것을 거부합니다:
MATCH (m:movie {title:'Avatar'}) DELETE m;
ERROR: Vertex with connected edges cannot be removed
HINT: Use DETACH DELETE or remove connected edges before deleting the vertex.
쿼리에는 상대할 그래프가 있어야 한다#
기본 그래프란 것이 없습니다. graph_path가 설정돼 있지 않으면 모든 그래프 문장이 거부됩니다:
RESET graph_path;
MATCH (m:movie) RETURN count(m);
ERROR: graph_path is NULL
HINT: Use SET graph_path
세션에 설정하거나, postgresql.conf에 또는 롤마다 설정하십시오.
설정을 참고하십시오:
SET graph_path = moviekb;
AgensGraph가 더한 것#
이것들은 다른 Cypher 구현에 대응하는 것이 없으므로, 이를 쓰는 쿼리는 밖으로 옮겨 가지 못합니다.
MATCH (n:label ONLY)는 일치를 레이블 하나로 제한하며, 그것을 상속하는 레이블을 뺍니다. ONLY가
없으면 부모 레이블에 대한 일치가 자식들도 함께 반환합니다.
SET n += map은 속성 전체를 바꾸는 대신 맵을 속성에 병합합니다:
MATCH (n:person {name:'Keanu Reeves'}) SET n += {city:'Seoul'} RETURN properties(n);
properties
---------------------------------------------------------
{"born": 1964, "city": "Seoul", "name": "Keanu Reeves"}
LOAD FROM table AS r CREATE (…)는 릴레이션의 행에서 그래프 요소를 만들며, 대량 적재가 이렇게
이루어집니다.
dijkstra((a)-[e:related_to]->(b), e.weight)는 가중치가 있는 최단 경로를 찾습니다. 가중치를
따르므로 가장 싼 길이 홉 수로도 가장 짧은 길일 필요는 없습니다:
MATCH (a:movie {title:'The Matrix'}), (b:movie {title:'Inception'})
MATCH p = dijkstra((a)-[e:related_to]->(b), e.weight)
RETURN [x IN nodes(p) | x.title];
?column?
---------------------------------------------
["The Matrix", "Total Recall", "Inception"]
Cypher 쿼리는 테이블 표현식이고 레이블은 테이블이므로, 한 문장에서 SQL과 Cypher를 섞을 수 있습니다:
SELECT q.title #>> '{}' AS film, r.score
FROM (MATCH (p:person)-[:directed]->(m:movie)
WHERE p.name = 'Christopher Nolan'
RETURN m.title AS title) q
JOIN rating r ON r.title = q.title #>> '{}'
WHERE r.source = 'critics'
ORDER BY r.score DESC;
film | score
--------------+-------
Inception | 8.8
Interstellar | 8.7
관련 항목#
GQL 적합성 — ISO/IEC 39075의 어느 부분을 지원하는가
키워드 — 2.18이 예약한 단어와 그로 인해 깨지는 것
SQL과 Cypher — 두 언어를 넘나드는 방법 전부
Cypher, GQL, SQL — 세 언어가 어떻게 맞물리는가