{#combining-sql-and-cypher-combining-sql-and-cypher} # SQL과 Cypher 섞기 AgensGraph에서 SQL과 Cypher 사이에는 다리가 없습니다. 이을 것이 없기 때문입니다. 그래프는 보통 테이블과 같은 데이터베이스에 있는 PostgreSQL 테이블의 묶음이고, 두 언어는 같은 실행 계획 트리로 컴파일됩니다. 그래서 둘은 중첩으로 섞입니다. SQL이 테이블을 기대하는 자리에 Cypher 쿼리가 오고, Cypher가 값을 기대하는 자리에 SQL 쿼리가 옵니다. 이 페이지는 필요할 만한 순서대로 그 교차점들을 훑고, 그다음 모두가 놀라는 규칙 하나를 다룹니다. Cypher의 `RETURN`이 무엇을 돌려주는가입니다. 여기 있는 것은 모두 [예제 데이터셋](../get_started/example-dataset.md)에서 실행되며, 그 데이터셋에는 그래프의 일부가 아닌 보통 테이블 `rating`이 포함돼 있습니다. {#combining-sql-and-cypher-reading-a-label-with-plain-sql} ## 평범한 SQL로 레이블 읽기 레이블은 테이블이므로, 레이블을 읽는 데 Cypher가 필요하지 않습니다: ```sql 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로 레이블에 행을 쓰는 것은 켜야 합니다. [일부러 막아 둔 것](#graph-on-postgresql-what-is-deliberately-closed-off)을 참고하십시오. 읽기는 그냥 읽기입니다. 질문이 관계형일 때는 SQL을 그대로 쓰는 것이 맞는 도구입니다. 레이블별 개수, 속성 값으로 참조 테이블과 조인하기, 속성에 대한 윈도 함수 같은 것입니다. 홉 수가 정해지지 않은 질문에는 맞지 않는 도구입니다. 이 페이지의 나머지가 그런 경우를 다룹니다. {#combining-sql-and-cypher-a-cypher-query-in-a-sql-from} ## SQL의 FROM 안에 놓는 Cypher 쿼리 SQL의 `FROM`이 테이블을 기대하는 자리에 Cypher 쿼리를 괄호로 감싸 넣고 별칭을 주십시오. 그 `RETURN` 목록이 컬럼이 됩니다: ```psql 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 ``` 별칭은 필수입니다: ```psql 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` 안의 쓰기 절은 구문 오류입니다. 하나보다 많이 중첩할 수 있고, 다른 서브쿼리 둘과 마찬가지로 조인할 수 있습니다: ```psql 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 ``` {#combining-sql-and-cypher-joining-a-graph-to-an-ordinary-table} ## 그래프를 보통 테이블과 조인하기 대부분의 사람이 이 페이지에 오는 이유가 이것입니다. 그래프에는 그래프 질문을 하고, 그 답을 관계형 데이터와 조인합니다: ```psql 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 ``` 양쪽에 붙은 `#>> '{}'`이 요령의 전부이며, [다음 절](#combining-sql-and-cypher-getting-a-plain-value-out)이 그것을 설명합니다. 이것이 없으면 `jsonb` 문자열을 `text` 컬럼과 비교하는 것이 되어 타입 검사를 통과하지 못합니다. 그래프와 테이블 사이에는 외래 키가 없으므로, 조인 키는 값입니다. 제목, 코드, 외부 id 같은 것입니다. 모델을 직접 정할 수 있다면 그 id를 속성으로 저장하고 인덱스를 걸어 두십시오. 그 조인이 자주 쓰인다면 컬럼으로 승격하십시오. `text` 컬럼과 `text` 컬럼은 형 변환 없이 조인됩니다. [속성을 컬럼으로 승격하기](typed-columns.md)를 참고하십시오. {#combining-sql-and-cypher-a-cypher-query-in-a-cte} ## CTE 안에 놓는 Cypher 쿼리 `WITH` 절은 `SELECT`와 마찬가지로 Cypher 쿼리도 손쉽게 받습니다. 그러면 두 번 이상 참조할 수 있는 이름을 얻게 됩니다: ```psql 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`는 파이프라인 구분자이며 서브쿼리에 이름을 붙일 수 없습니다. [무엇이 다른가](#composing-how-it-differs-from-a-cte)를 참고하십시오. 이 방식은 순회를 관계형으로 집계하는 데까지 일반화됩니다. 아래에서는 Cypher가 영화마다 출연진 수를 세고 SQL이 평점을 평균합니다: ```psql 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 ``` {#combining-sql-and-cypher-sql-inside-a-cypher-query} ## Cypher 쿼리 안의 SQL 중첩은 반대 방향으로도 됩니다. SQL의 스칼라 서브쿼리는 표현식이므로, Cypher가 표현식을 원하는 곳이면 어디든 올 수 있습니다. `WHERE` 안, `RETURN` 안, 그리고 쓰려는 속성 값 안입니다: ```cypher 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`에서 호출할 수 있습니다: ```sql CREATE FUNCTION decade(y int) RETURNS int AS 'SELECT (y / 10) * 10' LANGUAGE sql IMMUTABLE; ``` ```cypher 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의 것이기 때문입니다. {#combining-sql-and-cypher-getting-a-plain-value-out} ## 평범한 값으로 꺼내기 Cypher의 `RETURN`이 속성에서 끌어낸 것은 무엇이든 `jsonb`로 돌아옵니다. 이것은 표시상의 특이점이 아니라 그 컬럼의 타입입니다. 쿼리 *안에서* 형 변환을 하거나 벗겨 내도 달라지지 않습니다. 투영이 결과를 다시 감싸기 때문입니다. 이 경계에서 가장 흔한 혼란의 원인이 이것이므로, 여기서 모든 각도에서 살펴봅니다. 모든 컬럼이 감싸이는 것은 아니고, 속성에서 끌어낸 컬럼만 그렇습니다. Cypher 서브쿼리의 컬럼들을 바깥에서 타입 검사해 보면 규칙이 정확히 드러납니다: ```psql 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 따옴표가 붙은 채로 돌아옵니다: ```cypher MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS t; ``` ``` t -------------- "The Matrix" ``` Cypher 쿼리 안에서 형 변환을 해도 도움이 되지 않습니다. 바깥 투영이 결과를 다시 감싸기 때문입니다: ```cypher MATCH (m:movie {title: 'The Matrix'}) RETURN m.title::text AS t; ``` ``` t -------------- "The Matrix" ``` ```psql SELECT pg_typeof(t) AS type FROM (MATCH (m:movie {title: 'The Matrix'}) RETURN m.title::text AS t) q; ``` ``` type ------- jsonb ``` Cypher 안에서 `#>> '{}'`으로 텍스트를 꺼내도 같은 이유로 마찬가지입니다: ```cypher MATCH (m:movie {title: 'The Matrix'}) RETURN m.title #>> '{}' AS t; ``` ``` t -------------- "The Matrix" ``` **빠져나가는 길은 한 단계 위인 SQL에서 벗겨 내는 것입니다.** Cypher 쿼리를 감싸고 그 바깥에서 `#>> '{}'`을 적용하십시오. 이 연산자는 jsonb의 "빈 경로에 있는 값을 텍스트로 꺼내기"입니다: ```psql SELECT t #>> '{}' AS t FROM (MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS t) q; ``` ``` t ------------ The Matrix ``` ```psql 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`이며 벗겨 낼 필요가 없습니다. [승격된 속성 읽기는 컬럼의 타입을 갖는다](#typed-columns-a-promoted-read-has-the-columns-type)를 참고하십시오. 정점이나 간선은 jsonb가 아니라 자기 복합 타입으로 돌아오며, SQL이 필드 표기로 그것을 분해할 수 있습니다: ```psql 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의 것이며, 복합 컬럼의 필드를 읽을 때는 항상 필요합니다. {#combining-sql-and-cypher-a-view-over-a-cypher-query} ## Cypher 쿼리 위에 만드는 뷰 뷰는 Cypher 절로 시작할 수 없습니다: ```psql CREATE VIEW film_rating AS MATCH (m:movie) RETURN m.title AS title; ``` ``` ERROR: syntax error at or near "MATCH" ``` `SELECT`로 감싸면 됩니다. 그래서 그래프 순회를 SQL을 말하는 모든 것 — BI 도구, ORM, 외부 데이터 래퍼 — 을 위해 관계형 뷰로 내보낼 수 있습니다: ```psql 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'; ``` ```sql 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`는 레이블 이름이 어느 그래프에 속하는지를 말할 뿐, 뷰가 어디로 가는지에 대해서는 아무것도 말하지 않습니다: ```sql SELECT schemaname, viewname FROM pg_views WHERE viewname = 'film_rating'; ``` ``` schemaname | viewname ------------+------------- public | film_rating ``` {#combining-sql-and-cypher-what-the-server-stored} ### 서버가 저장한 것 뷰는 작성한 텍스트가 아니라 파싱된 쿼리 트리를 담고 있습니다. 정의를 다시 물어보면 작성한 Cypher가 아니라 *그 Cypher가 컴파일된 SQL*이 나옵니다: ```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가 어떤 속성을 [승격된 컬럼](typed-columns.md)으로 해석했다면, 그 뷰는 그 컬럼에 대한 의존성을 갖습니다. 뷰를 없애기 전에는 그 컬럼을 지우는 것이 거부됩니다. 이것은 의도한 동작입니다. 그렇지 않으면 뷰의 뜻이 조용히 바뀌기 때문입니다. {#combining-sql-and-cypher-clean-up} ## 정리 ```sql DROP VIEW film_rating; DROP FUNCTION decade(int); ``` {#combining-sql-and-cypher-where-to-go-next} ## 다음에 갈 곳 * [그래프가 어떻게 저장되는가](../concepts/graph-on-postgresql.md) — 이 모든 것이 동작하는 이유, 그리고 SQL로 레이블에 쓰는 것을 막는 두 관문입니다. * [쿼리 조합하기](composing.md) — Cypher 자체의 파이프라인, 그리고 그 `WITH`가 CTE와 다른 지점입니다. * [데이터 적재](../guides/loading-data.md) — 보통 테이블을 그래프로 읽어 들이는 `LOAD FROM`입니다. * [속성을 컬럼으로 승격하기](typed-columns.md) — 늘 쓰는 조인 키를 위한 것입니다.