{#reading-reading-a-graph} # 그래프 읽기 모든 읽기는 패턴에서 시작합니다. 패턴은 찾고 있는 모양을 ASCII로 그린 그림입니다. 정점은 소괄호로, 간선은 대괄호와 하이픈으로 그립니다. `MATCH`는 그 그림을 플래너에게 넘기고, 플래너는 그것을 스캔과 조인으로 바꾸며, 그림이 데이터에 들어맞는 모든 경우마다 행 하나씩을 반환합니다. 이 페이지는 그 그림에 대한 것이고, 플래너가 그것으로 무엇을 하는지에 대한 것입니다. 정식 구문은 [`MATCH`](../reference/clauses/match.md)와 [`WHERE`](../reference/clauses/where.md)에 있습니다. 여기 있는 것은 모두 [예제 데이터셋](../get_started/example-dataset.md)에서 실행됩니다. {#reading-one-vertex} ## 정점 하나 가장 작은 패턴은 소괄호 한 쌍입니다. 레이블을 더하면 테이블 하나로 좁혀집니다: ```cypher 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`는 그것들을 제외합니다: ```cypher MATCH (m:movie ONLY) RETURN count(m) AS films; ``` ``` films ------- 18 ``` 여기서는 `movie`를 상속한 것이 없으므로 어느 쪽이든 개수가 같습니다. 계층 구조에서는 그 차이가 `Append` 전체입니다. [레이블 상속](../concepts/label-inheritance.md)을 참고하십시오. {#reading-two-ways-to-narrow} ## 범위를 좁히는 두 가지 방법 패턴 안의 속성 맵은 속성이 같은지를 검사합니다: ```cypher MATCH (m:movie {genre: 'Adventure'}) RETURN m.title AS title, m.year AS year; ``` ``` title | year -----------------+------ "Jurassic Park" | 1993 ``` `WHERE`는 어떤 조건이든 받으므로, 같음이 아닌 것은 모두 그리로 갑니다: ```cypher 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`에서는 정확히 같은 뜻입니다. 어느 쪽으로 쓰든 플래너는 하나의 논리곱을 받습니다: ```cypher 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` 하나뿐이며, [아래](#reading-a-pattern-that-may-not-fit)에 나옵니다. 조건은 불리언이어야 합니다. `WHERE m.title`은 "제목이 있는 것"을 뜻하지 않고 `ERROR: cannot cast "The Matrix" (jsonb type string) to boolean`을 냅니다. 뜻하는 검사를 그대로 쓰십시오: `WHERE m.title IS NOT NULL`. {#reading-an-edge} ## 간선 간선은 하이픈 두 개 사이에 놓인 대괄호 한 쌍이며, 방향은 화살촉으로 나타냅니다: ```cypher 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`로 실패합니다. `-[]->`와 `-[]-`로 쓰십시오. 간선을 변수에 바인딩하면 그 레이블과 속성을 쓸 수 있게 됩니다: ```cypher 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입니다. {#reading-direction} ### 방향 `-[]->`는 저장된 대로 간선을 따라가고, `<-[]-`는 거꾸로 따라가며, `-[]-`는 방향을 무시합니다. 방향은 데이터의 성질이 아니라 쿼리의 성질입니다. 간선은 한 방향으로 저장해 두고, 방향이 중요한지는 패턴마다 정합니다. ```cypher MATCH (a:movie {title: 'The Matrix Reloaded'})-[:related_to]->(b:movie) RETURN b.title AS neighbour ORDER BY neighbour; ``` ``` neighbour -------------------------- "The Matrix Revolutions" ``` ```cypher 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" ``` {#reading-one-label-per-edge} ### 간선마다 레이블 하나 간선 패턴은 레이블을 최대 하나만 이름으로 부릅니다. 택일 표기는 없습니다: ```cypher MATCH (p:person)-[r:acted_in|directed]->(m:movie) RETURN count(r); ``` ``` ERROR: multiple types for relationship not supported ``` 이것은 모든 길이에서, 그리고 `shortestpath` 안에서도 성립합니다. 간선에 타입을 주지 않고 레이블을 조건으로 검사하십시오: ```cypher 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`](../reference/clauses/union.md)으로 이은 두 패턴이 각 스캔을 좁게 유지해 줍니다. {#reading-a-whole-path} ## 경로 전체 패턴 앞의 `p = `는 양 끝만이 아니라 경로 자체를 바인딩합니다: ```cypher 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)`는 간선입니다. 경로는 값이므로 반환할 수도, 길이로 정렬할 수도, 모을 수도 있습니다. 길이를 모르는 패턴을 비롯한 나머지는 [경로](paths.md)가 다룹니다. {#reading-more-than-one-pattern} ## 패턴이 둘 이상일 때 `MATCH`는 쉼표로 구분한 패턴 목록을 받습니다. 그중 둘에 함께 나타나는 변수가 그 둘을 조인합니다: ```cypher 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`은 첫 패턴에서 바인딩되고 두 번째 패턴에서 다시 쓰입니다. 그래서 이것은 곱집합이 아니라 영화를 기준으로 한 내부 조인이 됩니다. 공통 변수가 없는 두 패턴은 *실제로* 곱집합이며, 그것도 허용되고 때로는 원하는 것입니다: ```cypher 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`를 쓰십시오. {#reading-a-pattern-that-may-not-fit} ## 들어맞지 않을 수도 있는 패턴 `MATCH`는 패턴이 아무것도 찾지 못한 행을 버립니다: ```cypher 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로 둡니다. 이것은 왼쪽 외부 조인이며, 모든 면에서 그렇게 동작합니다: ```cypher 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과 함께 살아남습니다: ```cypher 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" | ``` 같은 조건을 한 절 뒤에 쓰면 조인된 결과에 대한 필터가 되고, 그 행을 버립니다: ```cypher 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`](../reference/clauses/optional-match.md)를 참고하십시오. {#reading-asking-whether-a-pattern-exists} ## 패턴이 존재하는지 묻기 패턴은 그 자체로 조건이 아닙니다. `WHERE (p)-[:acted_in]->(:movie)`는 구문 오류입니다. `EXISTS { }` 서브쿼리에 넣으십시오: ```cypher 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`은 모두 이런 서브쿼리를 받습니다. [쿼리 조합하기](#composing-subqueries-as-values)가 그것들을 함께 다룹니다. {#reading-how-a-pattern-becomes-a-plan} ## 패턴은 어떻게 실행 계획이 되는가 패턴은 그래프를 하나씩 따라가는 방식으로 실행되지 않습니다. 레이블 테이블에 대한 평범한 관계 대수로 컴파일되며, `EXPLAIN`이 정확히 그것을 보여 줍니다: ```cypher 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](#graph-on-postgresql-identity-the-graphid)를 참고하십시오. 패턴을 한쪽에 못 박으면 조인 순서가 바뀝니다. 한쪽 끝을 정점 하나에 고정하면 실행 계획이 테이블 전체를 해시하는 일을 멈춥니다: ```cypher 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`는 찾아낸 간선 하나하나에 대해 자기 기본 키를 통해 도달합니다. 출발점이 정해진 순회에서 원하는 모양이 이것입니다. 그리고 선택도가 높은 고정점, 즉 인덱스가 걸린 속성이 보통 다른 어떤 튜닝보다 값어치가 큰 이유도 이것입니다. 어떤 조건이 고정점 역할을 할 수 있는지는 [인덱스와 제약](indexes-and-constraints.md)이 다룹니다. {#reading-what-an-endpoint-you-do-not-use-costs} ## 쓰지 않는 끝점의 비용 쿼리가 값을 전혀 읽지 않는 끝점은 아예 가져올 필요가 없습니다. 여기서 `m`은 바인딩되고 레이블도 붙었지만, 결과 투영에서 그것을 요구하는 것은 없습니다: ```cypher 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` 계열 전체에서 가져옵니다: ```cypher 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`가 돌아옵니다. 쿼리가 실제로 그 영화를 필요로 하기 때문입니다. 쓰는 끝점만 투영하고 그 이상은 하지 마십시오. ```{note} 실행 계획을 보관해 두려는 패턴이라면 모든 요소에 이름을 붙이십시오. 이름을 붙이지 않은 요소에는 `"<0000000002>"` 같은 합성 별칭이 주어지고, 그 뒤의 카운터는 문장마다 올라갑니다 — 그래서 같은 쿼리를 두 번째로 실행하면 다른 계획이 출력됩니다. ``` ```{note} 이름을 생략하는 것이 어디서나 공짜는 아닙니다. `shortestpath`의 양 끝점은 반드시 이름 있는 변수여야 하며, 쓰지 않을 변수라도 그렇습니다 — 그 자리에 익명 끝점을 두면 `ERROR: node must be a reference to a specific node`가 나옵니다. ``` {#reading-where-to-go-next} ## 다음에 갈 곳 * [쿼리 조합하기](composing.md) — `WITH`, 집계, 서브쿼리, 그리고 `MATCH` 뒤에 이어지는 파이프라인의 나머지입니다. * [경로](paths.md) — 가변 길이 간선, 최단 경로, 가중 탐색입니다. * [그래프 쓰기](writing.md) — 같은 패턴에 쓰기 절을 뒤에 붙인 것입니다. * [인덱스와 제약](indexes-and-constraints.md) — 어떤 조건에 인덱스가 쓰일 수 있는지입니다.