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 — 세 언어가 어떻게 맞물리는가