그래프 읽기#
모든 읽기는 패턴에서 시작합니다. 패턴은 찾고 있는 모양을 ASCII로 그린 그림입니다. 정점은
소괄호로, 간선은 대괄호와 하이픈으로 그립니다. MATCH는 그 그림을 플래너에게 넘기고, 플래너는
그것을 스캔과 조인으로 바꾸며, 그림이 데이터에 들어맞는 모든 경우마다 행 하나씩을 반환합니다.
이 페이지는 그 그림에 대한 것이고, 플래너가 그것으로 무엇을 하는지에 대한 것입니다. 정식 구문은
MATCH와 WHERE에 있습니다.
여기 있는 것은 모두 예제 데이터셋에서 실행됩니다.
정점 하나#
가장 작은 패턴은 소괄호 한 쌍입니다. 레이블을 더하면 테이블 하나로 좁혀집니다:
MATCH (m:movie) RETURN m.title AS title ORDER BY title LIMIT 3;
title
---------------------
"Avatar"
"Blade Runner"
"Blade Runner 2049"
m은 일치하는 각 정점에 바인딩되는 변수이고, movie는 레이블입니다. 둘 중 어느 것이든 생략할 수
있습니다. (m)은 모든 정점에 일치하고, (:movie)는 이름을 붙이지 않고 모든 영화에 일치하며,
()는 이름을 붙이지 않고 모든 정점에 일치합니다.
레이블을 이름으로 부르면 그것을 상속한 레이블까지 포함됩니다. ONLY는 그것들을 제외합니다:
MATCH (m:movie ONLY) RETURN count(m) AS films;
films
-------
18
여기서는 movie를 상속한 것이 없으므로 어느 쪽이든 개수가 같습니다. 계층 구조에서는 그 차이가
Append 전체입니다. 레이블 상속을 참고하십시오.
범위를 좁히는 두 가지 방법#
패턴 안의 속성 맵은 속성이 같은지를 검사합니다:
MATCH (m:movie {genre: 'Adventure'}) RETURN m.title AS title, m.year AS year;
title | year
-----------------+------
"Jurassic Park" | 1993
WHERE는 어떤 조건이든 받으므로, 같음이 아닌 것은 모두 그리로 갑니다:
MATCH (m:movie) WHERE m.year >= 2017 RETURN m.title AS title, m.year AS year ORDER BY year;
title | year
----------------------------+------
"Blade Runner 2049" | 2017
"The Matrix Resurrections" | 2021
둘은 함께 쓸 수 있고, 그냥 MATCH에서는 정확히 같은 뜻입니다. 어느 쪽으로 쓰든 플래너는 하나의
논리곱을 받습니다:
MATCH (m:movie {genre: 'Action'}) WHERE m.year < 2000
RETURN m.title AS title, m.year AS year ORDER BY year;
title | year
------------------------------+------
"The Terminator" | 1984
"Terminator 2: Judgment Day" | 1991
"The Matrix" | 1999
찾으려는 대상을 식별하는 키는 맵에 쓰고, 그 대상에 대한 조건은 WHERE에 쓰십시오. 이 선택이
실제로 달라지는 곳은 OPTIONAL MATCH 하나뿐이며, 아래에
나옵니다.
조건은 불리언이어야 합니다. WHERE m.title은 “제목이 있는 것”을 뜻하지 않고
ERROR: cannot cast "The Matrix" (jsonb type string) to boolean을 냅니다. 뜻하는 검사를 그대로
쓰십시오: WHERE m.title IS NOT NULL.
간선#
간선은 하이픈 두 개 사이에 놓인 대괄호 한 쌍이며, 방향은 화살촉으로 나타냅니다:
MATCH (p:person)-[]->(m:movie {title: 'Blade Runner 2049'})
RETURN p.name AS person ORDER BY person;
person
--------------------
"Denis Villeneuve"
"Harrison Ford"
"Ryan Gosling"
빈 대괄호를 보십시오. 대괄호는 생략할 수 없습니다. -->와 --는 패턴이 아닙니다. --가 SQL의
한 줄 주석을 시작하기 때문입니다. MATCH (p:person)-->(m:movie) RETURN count(p)는 첫 하이픈
이후를 모두 주석으로 지워 버리고 ERROR: Cypher query must end with RETURN, FINISH or update clause로
실패합니다. -[]->와 -[]-로 쓰십시오.
간선을 변수에 바인딩하면 그 레이블과 속성을 쓸 수 있게 됩니다:
MATCH (p:person)-[r]->(m:movie {title: 'Blade Runner 2049'})
RETURN p.name AS person, label(r) AS via, r.role AS role ORDER BY person;
person | via | role
--------------------+------------+----------------
"Denis Villeneuve" | "directed" |
"Harrison Ford" | "acted_in" | "Rick Deckard"
"Ryan Gosling" | "acted_in" | "K"
label(r)은 간선의 레이블이고 r.role은 간선 자체의 속성입니다. directed 간선에는 role이
없으므로 그 칸은 null입니다.
방향#
-[]->는 저장된 대로 간선을 따라가고, <-[]-는 거꾸로 따라가며, -[]-는 방향을 무시합니다.
방향은 데이터의 성질이 아니라 쿼리의 성질입니다. 간선은 한 방향으로 저장해 두고, 방향이 중요한지는
패턴마다 정합니다.
MATCH (a:movie {title: 'The Matrix Reloaded'})-[:related_to]->(b:movie)
RETURN b.title AS neighbour ORDER BY neighbour;
neighbour
--------------------------
"The Matrix Revolutions"
MATCH (a:movie {title: 'The Matrix Reloaded'})-[:related_to]-(b:movie)
RETURN b.title AS neighbour ORDER BY neighbour;
neighbour
--------------------------
"The Matrix"
"The Matrix Revolutions"
간선마다 레이블 하나#
간선 패턴은 레이블을 최대 하나만 이름으로 부릅니다. 택일 표기는 없습니다:
MATCH (p:person)-[r:acted_in|directed]->(m:movie) RETURN count(r);
ERROR: multiple types for relationship not supported
이것은 모든 길이에서, 그리고 shortestpath 안에서도 성립합니다. 간선에 타입을 주지 않고 레이블을
조건으로 검사하십시오:
MATCH (p:person)-[r]->(m:movie {title: 'Blade Runner'})
WHERE label(r) IN ['acted_in', 'directed']
RETURN p.name AS person, label(r) AS via ORDER BY person;
person | via
-----------------+------------
"Harrison Ford" | "acted_in"
"Ridley Scott" | "directed"
이 형태에는 대가가 있습니다. 타입 없는 간선은 그래프의 모든 간선 레이블을 스캔하고, 레이블 검사는
그 뒤에 적용됩니다. 택일 대상이 몇 개 레이블뿐이고 그래프가 크다면,
UNION으로 이은 두 패턴이 각 스캔을 좁게 유지해 줍니다.
경로 전체#
패턴 앞의 p = 는 양 끝만이 아니라 경로 자체를 바인딩합니다:
MATCH p = (a:person {name: 'Ryan Gosling'})-[:acted_in]->(m:movie)
RETURN length(p) AS hops,
[x IN nodes(p) | label(x)] AS labels,
[r IN relationships(p) | label(r)] AS edges;
hops | labels | edges
------+---------------------+--------------
1 | ["person", "movie"] | ["acted_in"]
length(p)는 간선 개수, nodes(p)는 순서대로의 정점, relationships(p)는 간선입니다. 경로는
값이므로 반환할 수도, 길이로 정렬할 수도, 모을 수도 있습니다. 길이를 모르는 패턴을 비롯한 나머지는
경로가 다룹니다.
패턴이 둘 이상일 때#
MATCH는 쉼표로 구분한 패턴 목록을 받습니다. 그중 둘에 함께 나타나는 변수가 그 둘을 조인합니다:
MATCH (d:person)-[:directed]->(m:movie), (a:person)-[:acted_in]->(m)
RETURN d.name AS director, a.name AS actor, m.title AS film
ORDER BY film, actor LIMIT 5;
director | actor | film
--------------------+-------------------+---------------------
"James Cameron" | "Sam Worthington" | "Avatar"
"Ridley Scott" | "Harrison Ford" | "Blade Runner"
"Denis Villeneuve" | "Harrison Ford" | "Blade Runner 2049"
"Denis Villeneuve" | "Ryan Gosling" | "Blade Runner 2049"
"Neill Blomkamp" | "Matt Damon" | "Elysium"
m은 첫 패턴에서 바인딩되고 두 번째 패턴에서 다시 쓰입니다. 그래서 이것은 곱집합이 아니라 영화를
기준으로 한 내부 조인이 됩니다. 공통 변수가 없는 두 패턴은 실제로 곱집합이며, 그것도 허용되고
때로는 원하는 것입니다:
MATCH (p:person {name: 'Keanu Reeves'}), (m:movie {genre: 'Adventure'})
RETURN p.name AS person, m.title AS film;
person | film
----------------+-----------------
"Keanu Reeves" | "Jurassic Park"
두 패턴을 연달아 오는 두 개의 MATCH 절로 쓰는 것도 같은 뜻입니다. 위의 감독·배우 쿼리에서 쉼표
형태와 두 절 형태는 모두 31행을 내고 실행 계획의 모양도 같습니다. 더 잘 읽히는 쪽을 쓰십시오.
“이것들을 함께”라면 쉼표를, “그다음”이라면 두 번째 MATCH를 쓰십시오.
들어맞지 않을 수도 있는 패턴#
MATCH는 패턴이 아무것도 찾지 못한 행을 버립니다:
MATCH (p:person {name: 'Ridley Scott'})-[:acted_in]->(m:movie)
RETURN p.name AS person, m.title AS film;
person | film
--------+------
(0 rows)
OPTIONAL MATCH는 그 행을 남겨 두고 패턴의 변수를 null로 둡니다. 이것은 왼쪽 외부 조인이며, 모든
면에서 그렇게 동작합니다:
MATCH (p:person {name: 'Ridley Scott'})
OPTIONAL MATCH (p)-[:acted_in]->(m:movie)
RETURN p.name AS person, m.title AS film;
person | film
----------------+------
"Ridley Scott" |
속성 맵과 WHERE가 더 이상 같지 않게 되는 곳이 여기입니다. OPTIONAL MATCH 에 붙여 쓴
WHERE는 조인 조건에 속하므로, 그 조건에 맞지 않는 행도 null과 함께 살아남습니다:
MATCH (m:movie {title: 'The Matrix'})
OPTIONAL MATCH (m)<-[r:acted_in]-(p:person) WHERE r.role = 'Agent Smith'
RETURN m.title AS film, p.name AS actor;
film | actor
--------------+-------
"The Matrix" |
같은 조건을 한 절 뒤에 쓰면 조인된 결과에 대한 필터가 되고, 그 행을 버립니다:
MATCH (m:movie {title: 'The Matrix'})
OPTIONAL MATCH (m)<-[r:acted_in]-(p:person)
WITH m, p, r WHERE r.role = 'Agent Smith'
RETURN m.title AS film, p.name AS actor;
film | actor
------+-------
(0 rows)
SQL을 안다면 이미 만나 본 것입니다. LEFT JOIN에서 조건을 ON에 쓰는 것과 그 뒤 WHERE에
쓰는 것의 차이입니다. OPTIONAL MATCH를 참고하십시오.
패턴이 존재하는지 묻기#
패턴은 그 자체로 조건이 아닙니다. WHERE (p)-[:acted_in]->(:movie)는 구문 오류입니다.
EXISTS { } 서브쿼리에 넣으십시오:
MATCH (p:person)
WHERE EXISTS { MATCH (p)-[:acted_in]->(:movie {title: 'The Matrix'}) }
RETURN p.name AS actor ORDER BY actor;
actor
----------------------
"Carrie-Anne Moss"
"Keanu Reeves"
"Laurence Fishburne"
EXISTS, COUNT, COLLECT, ARRAY, VALUE, IN은 모두 이런 서브쿼리를 받습니다.
쿼리 조합하기가 그것들을 함께 다룹니다.
패턴은 어떻게 실행 계획이 되는가#
패턴은 그래프를 하나씩 따라가는 방식으로 실행되지 않습니다. 레이블 테이블에 대한 평범한 관계 대수로 컴파일되며,
EXPLAIN이 정확히 그것을 보여 줍니다:
EXPLAIN (COSTS OFF) MATCH (p:person)-[r:acted_in]->(m:movie) RETURN p.name, m.title;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hash Join
Hash Cond: (r.start = p.id)
-> Hash Join
Hash Cond: (r."end" = m.id)
-> Seq Scan on acted_in r
Filter: ((start >= '4.0'::graphid) AND (start <= '4.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid))
-> Hash
-> Seq Scan on movie m
-> Hash
-> Seq Scan on person p
여기서 읽어 낼 만한 것이 세 가지 있습니다.
패턴의 각 요소가 스캔입니다. person, acted_in, movie는 세 개의 테이블이고, 패턴은 그들 사이의
조인 두 개입니다. r.start = p.id와 r."end" = m.id입니다. 간선을 따라가는 것이 곧 graphid에
대한 조인입니다. 그러므로 모든 간선 레이블과 함께 만들어지는 (start, "end")와 ("end", start)
인덱스가 대단히 중요합니다.
끝점에 대한 레이블 검사는 간선 쪽에서 답이 나옵니다. p가 person이므로 r.start는 person의
id일 수밖에 없습니다. 레이블 id는 graphid의 상위 비트에 있으므로, 한 레이블의 모든 id는 연속된
한 구간을 이룹니다.
플래너는 “start가 person이다”를 start >= '4.0' AND start <= '4.281474976710655'로 바꿉니다.
이것은 간선 스캔이나 그 인덱스로 밀어 넣을 수 있는 범위 검사이고, person을 전혀 방문하지
않습니다. 식별자: graphid를 참고하십시오.
패턴을 한쪽에 못 박으면 조인 순서가 바뀝니다. 한쪽 끝을 정점 하나에 고정하면 실행 계획이 테이블 전체를 해시하는 일을 멈춥니다:
EXPLAIN (COSTS OFF)
MATCH (p:person {name: 'Keanu Reeves'})-[r:acted_in]->(m:movie) RETURN m.title;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop
-> Nested Loop
-> Seq Scan on person p
Filter: (properties.'name' = '"Keanu Reeves"'::jsonb)
-> Bitmap Heap Scan on acted_in r
Recheck Cond: ((p.id = start) AND (start >= '4.0'::graphid) AND (start <= '4.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid))
-> Bitmap Index Scan on acted_in_start_idx
Index Cond: ((start = p.id) AND (start >= '4.0'::graphid) AND (start <= '4.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid))
-> Index Scan using movie_pkey on movie m
Index Cond: (id = r."end")
이제 acted_in은 그 한 사람에 대해 acted_in_start_idx를 통해 도달하고, movie는 찾아낸 간선
하나하나에 대해 자기 기본 키를 통해 도달합니다.
출발점이 정해진 순회에서 원하는 모양이 이것입니다. 그리고 선택도가 높은 고정점, 즉 인덱스가 걸린 속성이 보통 다른 어떤 튜닝보다 값어치가 큰 이유도 이것입니다. 어떤 조건이 고정점 역할을 할 수 있는지는 인덱스와 제약이 다룹니다.
쓰지 않는 끝점의 비용#
쿼리가 값을 전혀 읽지 않는 끝점은 아예 가져올 필요가 없습니다. 여기서 m은 바인딩되고 레이블도
붙었지만, 결과 투영에서 그것을 요구하는 것은 없습니다:
EXPLAIN (COSTS OFF) MATCH (p:person)-[r:acted_in]->(m:movie) RETURN p.name;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hash Join
Hash Cond: (r.start = p.id)
-> Seq Scan on acted_in r
Filter: (("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid) AND (start >= '4.0'::graphid) AND (start <= '4.281474976710655'::graphid))
-> Hash
-> Seq Scan on person p
m에 대한 스캔이 없습니다. m이 남긴 기여는 그 필터의 뒷부분 하나뿐입니다. "end"가 movie
범위 안에 있어야 한다는 것입니다. 즉 (m:movie) 전체가, 간선 스캔이 어차피 읽고 있던 컬럼에 대한
비교 두 개로 접혀 버린 것입니다.
이 생략을 끄면, 쿼리가 한 번도 묻지 않은 정점을 ag_vertex 계열 전체에서 가져옵니다:
SET enable_graph_endpoint_elision = off;
EXPLAIN (COSTS OFF) MATCH (p:person)-[r:acted_in]->(m) RETURN p.name;
QUERY PLAN
-----------------------------------------------------------------------------------------------
Hash Join
Hash Cond: (r."end" = m.id)
-> Hash Join
Hash Cond: (r.start = p.id)
-> Seq Scan on acted_in r
Filter: ((start >= '4.0'::graphid) AND (start <= '4.281474976710655'::graphid))
-> Hash
-> Seq Scan on person p
-> Hash
-> Append
-> Seq Scan on ag_vertex m_1
-> Seq Scan on movie m_2
-> Seq Scan on person m_3
이 설정은 기본적으로 켜져 있고, 이런 실험 말고는 끌 이유가 없습니다.
이 비교가 절약의 크기와 그 제한을 보여 줍니다. 그 RETURN에 m.title을 더하면 Append가 돌아옵니다.
쿼리가 실제로 그 영화를 필요로 하기 때문입니다. 쓰는 끝점만 투영하고 그 이상은 하지 마십시오.
참고
실행 계획을 보관해 두려는 패턴이라면 모든 요소에 이름을 붙이십시오. 이름을 붙이지 않은 요소에는
"<0000000002>" 같은 합성 별칭이 주어지고, 그 뒤의 카운터는 문장마다 올라갑니다 — 그래서 같은
쿼리를 두 번째로 실행하면 다른 계획이 출력됩니다.
참고
이름을 생략하는 것이 어디서나 공짜는 아닙니다. shortestpath의 양 끝점은 반드시 이름 있는
변수여야 하며, 쓰지 않을 변수라도 그렇습니다 — 그 자리에 익명 끝점을 두면
ERROR: node must be a reference to a specific node가 나옵니다.