SQL과 Cypher 섞기#
AgensGraph에서 SQL과 Cypher 사이에는 다리가 없습니다. 이을 것이 없기 때문입니다. 그래프는 보통 테이블과 같은 데이터베이스에 있는 PostgreSQL 테이블의 묶음이고, 두 언어는 같은 실행 계획 트리로 컴파일됩니다.
그래서 둘은 중첩으로 섞입니다. SQL이 테이블을 기대하는 자리에 Cypher 쿼리가 오고, Cypher가 값을 기대하는 자리에 SQL 쿼리가 옵니다.
이 페이지는 필요할 만한 순서대로 그 교차점들을 훑고, 그다음 모두가 놀라는 규칙 하나를 다룹니다.
Cypher의 RETURN이 무엇을 돌려주는가입니다.
여기 있는 것은 모두 예제 데이터셋에서 실행되며, 그 데이터셋에는
그래프의 일부가 아닌 보통 테이블 rating이 포함돼 있습니다.
평범한 SQL로 레이블 읽기#
레이블은 테이블이므로, 레이블을 읽는 데 Cypher가 필요하지 않습니다:
SELECT id, properties->>'title' AS title, properties->>'year' AS year
FROM moviekb.movie ORDER BY (properties->>'year')::int LIMIT 3;
id | title | year
------+----------------+------
3.8 | Blade Runner | 1982
3.15 | The Terminator | 1984
3.11 | Total Recall | 1990
이것을 위해 켜야 할 것은 없습니다. SQL로 레이블에 행을 쓰는 것은 켜야 합니다. 일부러 막아 둔 것을 참고하십시오. 읽기는 그냥 읽기입니다.
질문이 관계형일 때는 SQL을 그대로 쓰는 것이 맞는 도구입니다. 레이블별 개수, 속성 값으로 참조 테이블과 조인하기, 속성에 대한 윈도 함수 같은 것입니다. 홉 수가 정해지지 않은 질문에는 맞지 않는 도구입니다. 이 페이지의 나머지가 그런 경우를 다룹니다.
SQL의 FROM 안에 놓는 Cypher 쿼리#
SQL의 FROM이 테이블을 기대하는 자리에 Cypher 쿼리를 괄호로 감싸 넣고 별칭을 주십시오. 그
RETURN 목록이 컬럼이 됩니다:
SELECT * FROM (MATCH (m:movie) WHERE m.year > 2015 RETURN m.title AS title, m.year AS year) q
ORDER BY year;
title | year
----------------------------+------
"Blade Runner 2049" | 2017
"The Matrix Resurrections" | 2021
별칭은 필수입니다:
SELECT count(*) FROM (MATCH (m:movie) RETURN m.title AS title);
ERROR: Cypher in FROM must have an alias
HINT: For example, FROM (...) [AS] foo.
규칙이 두 개 더 있습니다. 모든 컬럼에는 이름이 있어야 합니다. 바깥 쿼리가 그것을 가리킬 수 있어야
하기 때문입니다. 그래서 AS 없는 RETURN m.title은 거부됩니다. 그리고 중첩된 쿼리는 읽기여야
합니다. FROM 안의 쓰기 절은 구문 오류입니다.
하나보다 많이 중첩할 수 있고, 다른 서브쿼리 둘과 마찬가지로 조인할 수 있습니다:
SELECT d.name #>> '{}' AS director, a.name #>> '{}' AS actor
FROM (MATCH (p:person)-[:directed]->(:movie {title: 'The Matrix'}) RETURN p.name AS name) d
CROSS JOIN (MATCH (p:person)-[:acted_in]->(:movie {title: 'The Matrix'}) RETURN p.name AS name) a
ORDER BY director, actor LIMIT 4;
director | actor
-----------------+--------------------
Lana Wachowski | Carrie-Anne Moss
Lana Wachowski | Keanu Reeves
Lana Wachowski | Laurence Fishburne
Lilly Wachowski | Carrie-Anne Moss
그래프를 보통 테이블과 조인하기#
대부분의 사람이 이 페이지에 오는 이유가 이것입니다. 그래프에는 그래프 질문을 하고, 그 답을 관계형 데이터와 조인합니다:
SELECT q.title #>> '{}' AS film, r.score
FROM (MATCH (:person {name: 'Christopher Nolan'})-[:directed]->(m:movie) 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
양쪽에 붙은 #>> '{}'이 요령의 전부이며, 다음 절이
그것을 설명합니다. 이것이 없으면 jsonb 문자열을 text 컬럼과 비교하는 것이 되어 타입 검사를
통과하지 못합니다.
그래프와 테이블 사이에는 외래 키가 없으므로, 조인 키는 값입니다. 제목, 코드, 외부 id 같은 것입니다. 모델을 직접 정할 수 있다면 그 id를 속성으로 저장하고 인덱스를 걸어 두십시오.
그 조인이 자주 쓰인다면 컬럼으로 승격하십시오. text 컬럼과 text 컬럼은 형 변환 없이 조인됩니다.
속성을 컬럼으로 승격하기를 참고하십시오.
CTE 안에 놓는 Cypher 쿼리#
WITH 절은 SELECT와 마찬가지로 Cypher 쿼리도 손쉽게 받습니다. 그러면 두 번 이상 참조할 수 있는
이름을 얻게 됩니다:
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
문장이 어느 언어인지 보십시오. WITH cte AS (...)로 시작하는 문장은 SQL이고, 그 본문도 SQL이어야
합니다. 이것은 Cypher의 WITH가 아닙니다. Cypher의 WITH는 파이프라인 구분자이며 서브쿼리에
이름을 붙일 수 없습니다. 무엇이 다른가를 참고하십시오.
이 방식은 순회를 관계형으로 집계하는 데까지 일반화됩니다. 아래에서는 Cypher가 영화마다 출연진 수를 세고 SQL이 평점을 평균합니다:
WITH cast_size AS (
MATCH (p:person)-[:acted_in]->(m:movie) RETURN m.title AS title, count(p) AS players
)
SELECT c.title #>> '{}' AS film, c.players #>> '{}' AS players, round(avg(r.score), 2) AS mean_score
FROM cast_size c JOIN rating r ON r.title = c.title #>> '{}'
GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 4;
film | players | mean_score
----------------------------+---------+------------
The Matrix | 3 | 8.90
Inception | 1 | 8.90
Interstellar | 1 | 8.65
Terminator 2: Judgment Day | 2 | 8.60
Cypher 쿼리 안의 SQL#
중첩은 반대 방향으로도 됩니다. SQL의 스칼라 서브쿼리는 표현식이므로, Cypher가 표현식을 원하는
곳이면 어디든 올 수 있습니다. WHERE 안, RETURN 안, 그리고 쓰려는 속성 값 안입니다:
MATCH (m:movie)
WHERE m.title = (SELECT title FROM rating WHERE source = 'critics' ORDER BY score DESC LIMIT 1)
RETURN m.title AS title, m.year AS year;
title | year
-------------+------
"Inception" | 2010
SQL에서와 마찬가지로 서브쿼리는 한 행, 한 컬럼을 반환해야 합니다. 여기서는 형 변환이 필요하지
않았습니다. 서브쿼리의 text가 jsonb 속성과 비교됐고 그 비교가 해결됐습니다. #>> '{}'이
필요한 쪽은 반대 방향입니다. jsonb 속성을 text 컬럼에 맞추는 경우입니다.
사용자 정의 함수도 표현식이므로, SQL 함수를 Cypher의 RETURN에서 호출할 수 있습니다:
CREATE FUNCTION decade(y int) RETURNS int AS 'SELECT (y / 10) * 10' LANGUAGE sql IMMUTABLE;
MATCH (m:movie) RETURN decade(m.year::int) AS decade, count(*) AS films ORDER BY decade;
decade | films
--------+-------
1980 | 2
1990 | 6
2000 | 4
2010 | 5
2020 | 1
m.year에 붙인 ::int가 jsonb 속성을 함수 시그니처가 받는 인자로 바꿔 줍니다. 어떤 언어든
됩니다. PL/pgSQL, PL/Python, C 모두 됩니다. 집계 함수나 윈도 함수도 마찬가지입니다. Cypher의 집계
함수가 곧 PostgreSQL의 것이기 때문입니다.
평범한 값으로 꺼내기#
Cypher의 RETURN이 속성에서 끌어낸 것은 무엇이든 jsonb로 돌아옵니다. 이것은 표시상의 특이점이
아니라 그 컬럼의 타입입니다. 쿼리 안에서 형 변환을 하거나 벗겨 내도 달라지지 않습니다. 투영이
결과를 다시 감싸기 때문입니다.
이 경계에서 가장 흔한 혼란의 원인이 이것이므로, 여기서 모든 각도에서 살펴봅니다.
모든 컬럼이 감싸이는 것은 아니고, 속성에서 끌어낸 컬럼만 그렇습니다. Cypher 서브쿼리의 컬럼들을 바깥에서 타입 검사해 보면 규칙이 정확히 드러납니다:
SELECT pg_typeof(raw) AS raw, pg_typeof(cast_text) AS cast_text,
pg_typeof(unwrapped) AS unwrapped, pg_typeof(is_modern) AS boolean_expr,
pg_typeof(gid) AS gid, pg_typeof(v) AS vertex
FROM (MATCH (m:movie {title: 'The Matrix'})
RETURN m.title AS raw, m.title::text AS cast_text, m.title #>> '{}' AS unwrapped,
m.year > 1990 AS is_modern, id(m) AS gid, m AS v) q;
raw | cast_text | unwrapped | boolean_expr | gid | vertex
-------+-----------+-----------+--------------+---------+--------
jsonb | jsonb | jsonb | boolean | graphid | vertex
즉 불리언 표현식은 boolean으로, id(m)은 graphid로, 그래프 요소는 vertex·edge·graphpath로
남습니다. 속성에서 끌어낸 세 컬럼은 jsonb로 옵니다. text로 형 변환한 것과 이미 벗겨 낸 것까지
그렇습니다.
문자열 속성은 JSON 따옴표가 붙은 채로 돌아옵니다:
MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS t;
t
--------------
"The Matrix"
Cypher 쿼리 안에서 형 변환을 해도 도움이 되지 않습니다. 바깥 투영이 결과를 다시 감싸기 때문입니다:
MATCH (m:movie {title: 'The Matrix'}) RETURN m.title::text AS t;
t
--------------
"The Matrix"
SELECT pg_typeof(t) AS type FROM (MATCH (m:movie {title: 'The Matrix'}) RETURN m.title::text AS t) q;
type
-------
jsonb
Cypher 안에서 #>> '{}'으로 텍스트를 꺼내도 같은 이유로 마찬가지입니다:
MATCH (m:movie {title: 'The Matrix'}) RETURN m.title #>> '{}' AS t;
t
--------------
"The Matrix"
빠져나가는 길은 한 단계 위인 SQL에서 벗겨 내는 것입니다. Cypher 쿼리를 감싸고 그 바깥에서
#>> '{}'을 적용하십시오. 이 연산자는 jsonb의 “빈 경로에 있는 값을 텍스트로 꺼내기”입니다:
SELECT t #>> '{}' AS t FROM (MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS t) q;
t
------------
The Matrix
SELECT pg_typeof(t #>> '{}') AS type FROM (MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS t) q;
type
------
text
거기서부터 그 값은 다른 text와 똑같이 동작합니다. 컬럼과 비교하고, 함수에 넘기고, 숫자가
필요하면 (q.year #>> '{}')::int로 이어서 형 변환하십시오.
이 규칙에 대해 세 가지를 적어 둡니다.
가장 바깥의 투영에만 적용됩니다. Cypher 표현식 안에서 읽은 속성은 끝까지 자기 타입을 유지합니다.
그래서 위의 decade(m.year::int)가 동작합니다.
승격된 속성은 예외입니다. 타입 있는 컬럼이 뒤에 있는 속성은 jsonb가 아니라 그 컬럼의 타입으로
돌아옵니다. 그래서 승격된 year에 대한 RETURN m.year는 이미 integer이며 벗겨 낼 필요가
없습니다. 승격된 속성 읽기는 컬럼의 타입을 갖는다를
참고하십시오.
정점이나 간선은 jsonb가 아니라 자기 복합 타입으로 돌아오며, SQL이 필드 표기로 그것을 분해할 수 있습니다:
SELECT pg_typeof(m) AS type, (m).id AS id, (m).properties->>'title' AS title
FROM (MATCH (m:movie {title: 'The Matrix'}) RETURN m) q;
type | id | title
--------+-----+------------
vertex | 3.1 | The Matrix
m을 감싼 괄호는 SQL의 것이며, 복합 컬럼의 필드를 읽을 때는 항상 필요합니다.
Cypher 쿼리 위에 만드는 뷰#
뷰는 Cypher 절로 시작할 수 없습니다:
CREATE VIEW film_rating AS MATCH (m:movie) RETURN m.title AS title;
ERROR: syntax error at or near "MATCH"
SELECT로 감싸면 됩니다. 그래서 그래프 순회를 SQL을 말하는 모든 것 — BI 도구, ORM, 외부 데이터
래퍼 — 을 위해 관계형 뷰로 내보낼 수 있습니다:
CREATE VIEW film_rating AS
SELECT q.title #>> '{}' AS title, (q.year #>> '{}')::int AS year, r.score
FROM (MATCH (m:movie) RETURN m.title AS title, m.year AS year) q
JOIN rating r ON r.title = q.title #>> '{}'
WHERE r.source = 'critics';
SELECT * FROM film_rating ORDER BY score DESC LIMIT 4;
title | year | score
----------------------------+------+-------
Inception | 2010 | 8.8
Interstellar | 2014 | 8.7
The Matrix | 1999 | 8.7
Terminator 2: Judgment Day | 1991 | 8.6
이 뷰는 보통 뷰이므로 그래프가 아니라 SQL 검색 경로에 놓입니다. graph_path는 레이블 이름이 어느
그래프에 속하는지를 말할 뿐, 뷰가 어디로 가는지에 대해서는 아무것도 말하지 않습니다:
SELECT schemaname, viewname FROM pg_views WHERE viewname = 'film_rating';
schemaname | viewname
------------+-------------
public | film_rating
서버가 저장한 것#
뷰는 작성한 텍스트가 아니라 파싱된 쿼리 트리를 담고 있습니다. 정의를 다시 물어보면 작성한 Cypher가 아니라 그 Cypher가 컴파일된 SQL이 나옵니다:
SELECT pg_get_viewdef('public.film_rating'::regclass, true);
pg_get_viewdef
-----------------------------------------------------------------------
SELECT q.title #>> '{}'::text[] AS title, +
(q.year #>> '{}'::text[])::integer AS year, +
r.score +
FROM ( SELECT (_agens_default_s.m).properties.'title' AS title, +
(_agens_default_s.m).properties.'year' AS year +
FROM ( SELECT ROW(m.id, m.properties, m.ctid)::vertex AS m+
FROM moviekb.movie m) _agens_default_s) q +
JOIN rating r ON r.title = (q.title #>> '{}'::text[]) +
WHERE r.source = 'critics'::text;
이것은 한 번 읽어 볼 만합니다. 이 엔진에서 Cypher 쿼리가 무엇인지를 가장 분명하게 보여 주는
진술이기 때문입니다. MATCH (m:movie)는 moviekb.movie를 스캔하고 그 행들을 vertex 값으로
조립하는 것이 됐습니다. m.title은 (m).properties.'title'이 됐습니다. _agens_default_s로
시작하는 이름들은 파이프라인 단계에 자동으로 붙은 별칭입니다.
여기서 두 가지가 따라옵니다.
작성한 Cypher는 카탈로그에서 되찾을 수 없습니다. 다른 스키마 객체와 마찬가지로 뷰의 원본을 버전 관리에 두십시오. 이것은 뷰에만 해당하는 이야기가 아닙니다. 저장된 쿼리 트리 위에 세워진 것은 모두 컴파일된 형태를 기록합니다. 룰과 구체화된 뷰가 그렇습니다.
왕복은 됩니다. pg_dump가 그 컴파일된 SQL을 그대로 내보내고, 복원하면 동작하는 뷰가 다시
만들어집니다. 새 데이터베이스로 덤프·복원한 뒤의 정의는 글자 하나까지 같고, 뷰는 같은 행을
반환합니다.
그러므로 Cypher 쿼리 위의 뷰는 작성한 대로 읽히지 않더라도 운영에서 신뢰하고 쓸 수 있습니다.
한 가지 주의할 점이 있습니다. Cypher가 어떤 속성을 승격된 컬럼으로 해석했다면, 그 뷰는 그 컬럼에 대한 의존성을 갖습니다. 뷰를 없애기 전에는 그 컬럼을 지우는 것이 거부됩니다. 이것은 의도한 동작입니다. 그렇지 않으면 뷰의 뜻이 조용히 바뀌기 때문입니다.
정리#
DROP VIEW film_rating;
DROP FUNCTION decade(int);
다음에 갈 곳#
그래프가 어떻게 저장되는가 — 이 모든 것이 동작하는 이유, 그리고 SQL로 레이블에 쓰는 것을 막는 두 관문입니다.
쿼리 조합하기 — Cypher 자체의 파이프라인, 그리고 그
WITH가 CTE와 다른 지점입니다.데이터 적재 — 보통 테이블을 그래프로 읽어 들이는
LOAD FROM입니다.속성을 컬럼으로 승격하기 — 늘 쓰는 조인 키를 위한 것입니다.