쿼리 조합하기#
Cypher 문장과 GQL 문장은 파이프라인입니다. 각 절은 행의 흐름을 받아 그것에 한 가지 일을 하고,
결과를 다음 절에 넘깁니다. MATCH가 행을 만들고, WHERE가 일부를 걸러 내고, WITH가 모양을
바꾸고, RETURN이 문장을 끝냅니다.
이것은 SQL과 다른 사고 모형입니다. SQL에서 SELECT는 이름 붙은 부분들이 정해진 순서로 평가되는
하나의 블록입니다. 여기서는 순서가 곧 작성한 순서입니다. 하나의 쿼리가 일치시키고, 집계하고, 그
집계를 걸러 내고, 살아남은 것에서 다시 일치시키고, 또 집계할 수 있습니다. 서브쿼리도, CTE도,
HAVING도 없이 말입니다.
여기 있는 것은 모두 예제 데이터셋에서 실행됩니다.
WITH가 구분자다#
WITH는 파이프라인의 한 부분을 끝내고 다음 부분을 시작합니다. RETURN과 똑같이 투영 목록을
받으며, 그 목록에 있는 것만 유효 범위에 남습니다:
MATCH (d:person)-[:directed]->(m:movie)
WITH d, count(m) AS films
WHERE films > 2
RETURN d.name AS director, films ORDER BY films DESC, director;
director | films
-------------------+-------
"Lana Wachowski" | 4
"James Cameron" | 3
"Lilly Wachowski" | 3
WITH 뒤에는 m이 사라지고 d와 films가 남습니다. 그 뒤의 WHERE는 집계를 걸러 내며, SQL에서
HAVING이 필요한 일입니다.
WITH 안의 순서는 고정되어 있고 외워 둘 만합니다. WHERE가 먼저, 그다음 ORDER BY, 그다음
SKIP과 LIMIT입니다. 그래서 아래 쿼리는 영화 두 편을 먼저 고른 뒤 걸러 내는 것이 아니라, 다섯
편으로 걸러 낸 뒤 그중 두 편을 취합니다:
MATCH (m:movie) WITH m.year AS y WHERE y > 2010 ORDER BY y LIMIT 2 RETURN y;
y
------
2012
2013
이름은 한 번만 넘길 수 있습니다:
MATCH (m:movie) WITH m.year AS y, m.genre AS y RETURN y;
ERROR: WITH passes on "y" more than once
CTE와 무엇이 다른가#
두 WITH 키워드는 모두 쿼리를 나누지만, 같은 키워드가 아닙니다.
SQL의 CTE는 이름을 붙여 참조합니다. WITH films AS (...)로 선언하고 그다음 films를 원하는
곳에서, 원하는 횟수만큼, 어떤 순서로든 언급합니다.
Cypher의 WITH는 위치로 정해집니다. 이름이 없고, 바로 다음에 온다는 것으로 참조되며, 정확히 한
번 소비됩니다. Cypher 파이프라인을 두 갈래로 나눈 뒤 다시 합치는 방법은 없습니다.
둘은 섞이지 않습니다. Cypher의 WITH로는 이름 붙은 서브쿼리를 도입할 수 없습니다.
WITH d AS (MATCH ...)는 구문 오류입니다. 그러나 SQL의 CTE가 Cypher 쿼리를 담을 수 있으며, CTE의
동작이 필요할 때 그것을 얻는 방법이 이것입니다:
WITH films AS (MATCH (m:movie) WHERE m.year > 2010 RETURN m.title AS title, m.year AS year)
SELECT f.title #>> '{}' AS title, r.score
FROM films f JOIN rating r ON r.title = f.title #>> '{}'
WHERE r.source = 'critics' ORDER BY r.score DESC;
title | score
--------------------------+-------
Interstellar | 8.7
Blade Runner 2049 | 8.0
The Avengers | 8.0
Elysium | 6.6
The Matrix Resurrections | 5.7
위에서 바깥 문장은 SQL입니다. SQL의 CTE로 시작하는 문장은 SQL 문장이기 때문입니다. 그 경계를 다루는 페이지는 SQL과 Cypher 섞기입니다.
Cypher 파이프라인이 WITH로 시작할 수 없는 이유도 그것입니다. 맨 앞의 WITH는 CTE로 읽히므로,
파서는 이름과 괄호로 감싼 쿼리를 찾습니다:
WITH 1 AS x RETURN x;
ERROR: syntax error at or near "1"
LINE 1: WITH 1 AS x RETURN x;
^
LET에는 그런 충돌이 없으므로, 계산된 값으로 문장을 시작할 때
쓰는 절은 이것입니다:
LET x = 1 RETURN x;
x
---
1
집계#
집계 함수는 PostgreSQL의 집계 함수이고, 그룹화는 암시적입니다. 투영 목록에서 집계 함수 안에 들어 있지 않은 것은 모두 그룹화 키가 됩니다.
MATCH (m:movie) RETURN m.genre AS genre, count(*) AS films, min(m.year) AS earliest ORDER BY genre;
genre | films | earliest
-------------+-------+----------
"Action" | 7 | 1984
"Adventure" | 1 | 1993
"Sci-Fi" | 10 | 1982
count(*)는 행을 셉니다. count(x)는 x가 null이 아닌 행을 셉니다. 그래서 count(x)는
OPTIONAL MATCH 뒤에서 유용합니다. count(DISTINCT x)는 서로 다른 값을 세며, 이 둘의 차이는 보통
패턴이 퍼진 정도입니다:
MATCH (:person)-[:acted_in]->(m:movie) RETURN count(m) AS rows, count(DISTINCT m) AS films;
rows | films
------+-------
25 | 18
collect()는 가장 자주 꺼내 쓸 집계 함수입니다. 퍼진 것을 다시 한 행으로 되돌려 주기 때문입니다.
정점, 간선, 경로에 대해 쓰면 그 요소들의 네이티브 배열을 만들므로, 모아 놓은 요소를 계속 쓸 수
있습니다:
MATCH (p:person)-[:acted_in]->(m:movie {title: 'The Matrix'})
WITH collect(p) AS players
RETURN pg_typeof(players) AS type, cardinality(players) AS n, players[1].name AS first;
type | n | first
------------+---+----------------------
"vertex[]" | 3 | "Laurence Fishburne"
속성에 대한 collect()는 여전히 jsonb 리스트를 반환하므로, 저장된 데이터는 자기 모양을
유지합니다.
투영 전체에 붙인 DISTINCT는 행의 중복을 없앱니다. 같은 대상에 여러 경로로 도달하는 패턴 뒤에
필요한 것이 이것입니다:
MATCH (:person {name: 'Keanu Reeves'})-[:acted_in]->(:movie)<-[:acted_in]-(co:person)
RETURN DISTINCT co.name AS co_actor ORDER BY co_actor;
co_actor
----------------------
"Carrie-Anne Moss"
"Laurence Fishburne"
UNWIND#
UNWIND는 collect()의 반대입니다. 리스트를 받아 요소마다 행 하나씩을 만듭니다.
UNWIND [1999, 2010, 2014] AS y MATCH (m:movie {year: y}) RETURN y, m.title AS title ORDER BY y, title;
y | title
------+----------------
1999 | "The Matrix"
2010 | "Inception"
2014 | "Interstellar"
같은 파이프라인에서 앞서 만든 리스트를 풀어내는 데도 씁니다. 집계로 걸러 낸 다음 다시 요소마다 행 하나로 돌아가는 방법이 이것입니다:
MATCH (m:movie)<-[:acted_in]-(p:person)
WITH m, collect(p) AS players
WHERE cardinality(players) > 2
UNWIND players AS actor
RETURN m.title AS film, actor.name AS actor ORDER BY actor;
film | actor
--------------+----------------------
"The Matrix" | "Carrie-Anne Moss"
"The Matrix" | "Keanu Reeves"
"The Matrix" | "Laurence Fishburne"
FOR가 GQL 표기이며, 요소의 0부터 시작하는 인덱스를 위해
WITH OFFSET을 더해 줍니다. UNWIND를 참고하십시오.
정렬과 페이지 나누기#
ORDER BY, SKIP, OFFSET, LIMIT은 그 자체로 절입니다. SQL처럼 RETURN이나 WITH의 끝에
놓일 수 있고:
MATCH (m:movie) RETURN m.title AS title, m.year AS year ORDER BY year SKIP 2 LIMIT 3;
title | year
------------------------------+------
"Total Recall" | 1990
"Terminator 2: Judgment Day" | 1991
"Jurassic Park" | 1993
파이프라인 가운데에 홀로 설 수도 있습니다. 이것은 RETURN 형태로는 할 수 없습니다:
MATCH (m:movie) LET y = m.year ORDER BY y LIMIT 3 RETURN m.title AS title, y;
title | y
------------------+------
"Blade Runner" | 1982
"The Terminator" | 1984
"Total Recall" | 1990
이것들이 문장을 끝낼 수는 없습니다. 쿼리는 여전히 RETURN, FINISH, 또는 쓰기 절로 끝나야
합니다.
정렬 키는 jsonb로 그려진 모습이 아니라 그 값 자체로 정렬되므로, 그 속성에 걸린 인덱스가 정렬에 쓰일 수 있습니다. 이것이 이 대목을 신경 써야 하는 이유 전부입니다: 인덱스와 제약.
UNION#
UNION은 컬럼 이름이 같은 두 결과 집합을 쌓고 중복을 없앱니다. UNION ALL은 중복을 남깁니다.
MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS name
UNION
MATCH (p:person {name: 'Keanu Reeves'}) RETURN p.name AS name;
name
----------------
"Keanu Reeves"
"The Matrix"
간선 레이블 택일 표기가 없는 것을 깔끔하게 우회하는 방법도 이것입니다. 좁은 패턴 둘을 union하면 간선 테이블 두 개를 스캔하지만, 타입 없는 패턴 하나는 그 전부를 스캔합니다. 그래프 읽기를 참고하십시오.
GQL 절들#
ISO/IEC 39075의 네 개 절이, Cypher에서는 여러 절이 필요한 일을 해냅니다. 다른 모든 것과 같은 문장에서 허용되며 별도의 모드는 없습니다.
LET은 유효 범위의 어떤 것도 버리지 않으면서 계산된 값을 더합니다. WITH는 계속 쓰고 싶은 변수를
전부 나열하게 하지만, LET은 그 전부를 그대로 둡니다:
MATCH (m:movie) LET decade = (m.year / 10) * 10 FILTER decade = 2010
RETURN m.title AS title, m.year AS year, decade ORDER BY year;
title | year | decade
---------------------+------+--------
"Inception" | 2010 | 2010
"The Avengers" | 2012 | 2010
"Elysium" | 2013 | 2010
"Interstellar" | 2014 | 2010
"Blade Runner 2049" | 2017 | 2010
FILTER는 행을 걸러 내는 일만 하므로, 필터를 더 이상 WITH에 붙여야 하지 않습니다. 조건을 그냥
쓰거나 FILTER WHERE 표기를 씁니다.
NEXT는 한 쿼리를 다른 쿼리의 결과에 이어 붙입니다. WITH 없이 투영 뒤에서 걸러 내는 방법이
이것입니다:
MATCH (m:movie) RETURN m.genre AS genre, count(*) AS films
NEXT FILTER films > 3
RETURN genre, films ORDER BY genre;
genre | films
----------+-------
"Action" | 7
"Sci-Fi" | 10
FINISH는 아무것도 투영하지 않는 쿼리를 끝냅니다. 효과만을 위해 실행하는 문장에서 RETURN에
대응하는 것입니다:
MATCH (m:movie {title: 'The Matrix'}) FINISH;
--
(0 rows)
서브쿼리#
CALL { }은 들어오는 행마다 서브쿼리를 한 번 실행하고 그 결과를 다시 조인합니다. 행마다 원하는
것이 표현식이 아니라 쿼리 전체일 때 쓰십시오. 행별 집계나 행별 상위 N개 같은 경우입니다.
이것에 관한 모든 것이 평범하지만 한 가지가 다르고, 그것은 오류를 내는 대신 답을 바꿔 버립니다.
중요
가져오기 목록이 없는 CALL { }은 바깥 변수를 가져오지 않습니다. 본문 안에서 쓴 이름이 바깥에도
있다면, 그 이름은 제약이 없는 새 변수로 다시 바인딩되므로 서브쿼리가 현재 행이 아니라 그래프 전체를
대상으로 실행됩니다. 가져오기 목록을 항상 쓰십시오.
차이는 이렇습니다. m이 바깥에서 바인딩되고 본문도 m을 언급하는데, 답이 모든 영화에 대해
같습니다:
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 간선의 수입니다. 서브쿼리의 m은 바깥의 m이 아니었습니다.
가져올 것을 이름으로 적으면 바로잡힙니다:
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 { WITH m ... }은 같은 것의 옛
표기입니다. 세 형태 모두 같은 답을 냅니다.
서브쿼리는 바깥에서 이미 바인딩된 이름을 돌려줄 수 없습니다:
MATCH (m:movie) CALL (m) { MATCH (p:person)-[:acted_in]->(m) RETURN m AS m } RETURN count(*);
ERROR: variable "m" returned by the CALL subquery is already bound in the outer query
서브쿼리 본문에는 쓰기 절을 쓸 수 없습니다. OPTIONAL CALL이 왼쪽 외부 조인 형태이며, 본문이
아무것도 내놓지 않아도 행을 만듭니다. CALL { }과
OPTIONAL CALL을 참고하십시오.
값으로서의 서브쿼리#
여섯 개의 값 표현식이 서브쿼리를 받아 WHERE나 RETURN에 넣을 수 있는 것으로 줄여 줍니다.
가져오기 목록이 필요하지 않습니다. 바깥 변수가 별도의 쿼리가 아니라 표현식으로서 유효 범위에 있기
때문입니다.
EXISTS { }는 서브쿼리가 행을 하나라도 반환하면 참입니다:
MATCH (p:person) WHERE EXISTS { MATCH (p)-[:directed]->(:movie) } RETURN count(p) AS directors;
directors
-----------
12
COUNT { }는 서브쿼리가 반환하는 행의 수입니다:
MATCH (m:movie) WHERE COUNT { MATCH (:person)-[:acted_in]->(m) } > 2
RETURN m.title AS title ORDER BY title;
title
--------------
"The Matrix"
COLLECT { }와 ARRAY { }는 서브쿼리의 단일 컬럼을 리스트로 모읍니다:
MATCH (m:movie {title: 'The Matrix'})
RETURN COLLECT { MATCH (p:person)-[:acted_in]->(m) RETURN p.name ORDER BY p.name } AS players;
players
------------------------------------------------------------
["Carrie-Anne Moss", "Keanu Reeves", "Laurence Fishburne"]
VALUE { }는 그 단일 값을 취합니다:
MATCH (m:movie {title: 'The Matrix'})
RETURN VALUE { MATCH (p:person)-[:directed]->(m) RETURN p.name ORDER BY p.name LIMIT 1 } AS a_director;
a_director
------------------
"Lana Wachowski"
그리고 x IN { }은 그 결과에 속하는지를 검사합니다:
MATCH (p:person)
WHERE p.name IN { MATCH (d:person)-[:directed]->(:movie {title: 'Inception'}) RETURN d.name }
RETURN p.name AS name;
name
---------------------
"Christopher Nolan"
여섯 중 가장 많이 쓸 것은 EXISTS { }입니다. 패턴만으로는 조건이 되지 않기 때문입니다.
그래프 읽기를 참고하십시오. 여섯 모두
서브쿼리 표현식에 정의돼 있습니다.
다음에 갈 곳#
경로 — 길이를 미리 모르는 패턴입니다.
SQL과 Cypher 섞기 — CTE, 보통 테이블과의 조인, 뷰입니다.
쿼리 튜닝 — 파이프라인이 실행 계획으로는 어떻게 보이는지입니다.