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