그래프가 어떻게 저장되는가#

AgensGraph는 PostgreSQL에 그래프 엔진을 덧붙이지 않습니다. 이미 거기 있는 스키마, 테이블, 인덱스, 상속을 써서 그래프를 PostgreSQL 객체 그 자체로 저장합니다. 이 대응 관계를 알고 나면 PostgreSQL 운영에 대해 아는 것 대부분이 그대로 적용됩니다. 그리고 한 문장이 Cypher와 SQL을 섞을 수 있다는 것도 더는 놀랍지 않게 됩니다. 테이블은 처음부터 한 벌뿐이기 때문입니다.

대응 관계는 세 문장입니다.

그래프는 스키마입니다. CREATE GRAPH moviekb는 moviekb라는 이름의 스키마를 만듭니다.

레이블은 테이블입니다. CREATE VLABEL movie는 moviekb.movie 테이블을 만듭니다.

정점이나 간선은 그 테이블의 행입니다.

레이블 테이블은 어떻게 생겼는가#

정점 레이블에는 컬럼이 두 개 있습니다:

\d moviekb.movie
                                              Table "moviekb.movie"
   Column   |  Type   | Nullable |                                   Default
------------+---------+----------+-------------------------------------------------------------------------
 id         | graphid | not null | graphid(graph_labid('moviekb.movie'::cstring), nextval('moviekb.movie_id_seq'))
 properties | jsonb   | not null | jsonb_build_object()
Indexes:
    "movie_pkey" PRIMARY KEY, btree (id)
Inherits: moviekb.ag_vertex

간선 레이블에는 같은 컬럼 두 개에 양 끝점의 id가 더해집니다:

\d moviekb.acted_in
                                              Table "moviekb.acted_in"
   Column   |  Type   | Nullable |                                     Default
------------+---------+----------+---------------------------------------------------------------------------
 id         | graphid | not null | graphid(graph_labid('moviekb.acted_in'::cstring), nextval('moviekb.acted_in_id_seq'))
 start      | graphid | not null |
 end        | graphid | not null |
 properties | jsonb   | not null | jsonb_build_object()
Indexes:
    "acted_in_end_idx" btree ("end", start)
    "acted_in_id_idx" brin (id)
    "acted_in_start_idx" btree (start, "end")
Inherits: moviekb.ag_edge

레이블은 그 컬럼들보다 더 많은 것을 담을 수 있습니다. 속성 승격은 properties 옆에 실제 타입이 있는 컬럼을 추가하고, 쓰기 시점에 그 값을 채웁니다. 그러면 자주 쓰는 속성이 두 번 저장됩니다 — 한 번은 jsonb 키로, 한 번은 플래너가 인덱싱하고 그 타입 그대로 비교할 수 있는 컬럼으로입니다. 어떤 속성 키가 어느 컬럼에서 답을 얻는지는 카탈로그 ag_label_property가 기록합니다. Cypher에서는 계속 키 이름을 부르므로 컬럼 이름이 쿼리에 나타나는 일은 없습니다.

그러므로 간선은 두 정점을 지목하는 행이고, 간선을 따라가는 것은 graphid에 대한 조인입니다. (start, "end")와 ("end", start)에 걸린 btree 인덱스 두 개가 그 조인을 어느 방향으로든 빠르게 만들어 주며, 레이블을 만들 때 AgensGraph가 이 인덱스를 함께 만들어 줍니다. id에 걸린 BRIN 인덱스는 작고, 한 레이블의 id 범위를 스캔할 때 도움이 됩니다.

레이블 종류마다 어떤 인덱스가 붙는지 보십시오. 정점 레이블에는 id에 대한 기본 키가 있지만 간선 레이블에는 없습니다. 간선은 id로 찾는 것이 아니라 양 끝점을 통해 도달하기 때문입니다.

properties가 하나의 jsonb 컬럼이므로, 레이블은 자기 행들에 컬럼 목록을 강제하지 않습니다. 두 개의 movie 정점이 완전히 다른 키를 가질 수 있습니다. 여기서 “스키마 없는 속성”이 뜻하는 것이 그것입니다. 그리고 속성을 읽는 것이 JSON 문서 안을 읽는 일이 되는 이유도 그것입니다. 그 비용이 언제 문제가 되고 무엇을 할 수 있는지는 속성을 컬럼으로 승격하기가 다룹니다.

식별자: graphid#

graphid는 두 부분으로 이루어진 64비트 식별자입니다. 상위 16비트가 레이블 id이고, 하위 48비트가 레이블마다 매겨지는 시퀀스인 로컬 id입니다. 출력 형태는 labid.locid입니다:

SELECT id, graphid_labid(id) AS labid, graphid_locid(id) AS locid
FROM moviekb.movie ORDER BY id LIMIT 3;
 id  | labid | locid
-----+-------+-------
 3.1 |     3 |     1
 3.2 |     3 |     2
 3.3 |     3 |     3

이 배치에서 두 가지가 따라오고, 둘 다 중요합니다.

첫째, id가 자기 레이블을 함께 지니고 있습니다. 정점이 어느 테이블에서 왔는지 알기 위해 무언가를 찾아볼 필요가 없습니다.

둘째, 레이블 id가 상위 비트이므로 한 레이블의 모든 id는 graphid 값의 연속된 한 구간을 이룹니다. 그래서 id에 걸린 평범한 btree 인덱스가 movie 테이블을 전혀 건드리지 않고도 “이 정점이 movie인가?”에 답할 수 있습니다.

레이블 id 자체는 ag_label에 있습니다:

SELECT labname, labid, labkind FROM pg_catalog.ag_label
WHERE graphid = (SELECT oid FROM pg_catalog.ag_graph WHERE graphname = 'moviekb')
ORDER BY labid;
  labname   | labid | labkind
------------+-------+---------
 ag_vertex  |     1 | v
 ag_edge    |     2 | e
 movie      |     3 | v
 person     |     4 | v
 acted_in   |     5 | e
 directed   |     6 | e
 related_to |     7 | e

labid는 16비트이고 0은 유효한 레이블이 아니므로, 데이터베이스는 최대 65,535개의 레이블을 가집니다. 하나의 레이블은 최대 248 − 1개의 행을 가집니다. 제한을 참고하십시오.

모든 그래프에는 만들지 않은 레이블 두 개가 있다#

ag_vertex와 ag_edge는 그래프와 함께 만들어지고, 직접 만드는 모든 레이블은 그중 하나를 상속합니다:

SELECT c.relname AS child, p.relname AS parent
FROM pg_inherits i
  JOIN pg_class c ON c.oid = i.inhrelid
  JOIN pg_class p ON p.oid = i.inhparent
  JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'moviekb' ORDER BY 1;
   child    |  parent
------------+-----------
 acted_in   | ag_edge
 directed   | ag_edge
 movie      | ag_vertex
 person     | ag_vertex
 related_to | ag_edge

레이블 없는 패턴에 뜻을 주는 것이 이 상속입니다. MATCH (n)은 모든 것을 담은 특별한 인덱스를 스캔하는 것이 아닙니다. ag_vertex를 스캔하고, 상속을 통해 그 아래의 모든 테이블을 스캔합니다:

EXPLAIN (COSTS OFF) MATCH (n) RETURN n;
              QUERY PLAN
---------------------------------------
 Result
   ->  Append
         ->  Seq Scan on ag_vertex n_1
         ->  Seq Scan on movie n_2
         ->  Seq Scan on person n_3

레이블을 이름으로 부르면 스캔 범위가 그 테이블과 그 자식들로 좁혀지고, ONLY 수정자를 쓰면 정확히 그 테이블로만 좁혀집니다. 그것으로 무엇을 만들 수 있는지는 레이블 상속이 다룹니다.

속성 인덱스와 제약은 무엇이 되는가#

Cypher의 속성 인덱스는 properties 컬럼에 대한 PostgreSQL 표현식 인덱스입니다:

CREATE PROPERTY INDEX ON movie (title);
\d moviekb.movie
Indexes:
    "movie_pkey" PRIMARY KEY, btree (id)
    "movie_title_idx" btree ((properties.'title'))

고유성 제약은 같은 표현식에 대한 배제 제약이 됩니다:

CREATE CONSTRAINT movie_title_uq ON movie ASSERT title IS UNIQUE;
Indexes:
    "movie_title_uq" EXCLUDE USING btree ((properties.'title') WITH =)

그 정의에 나오는 properties.'title' 형태는 Cypher 파서가 속성 읽기를 표기하는 방식입니다. SQL에서 직접 입력하는 구문이 아닙니다.

튜닝 관점에서 중요한 것은 평범한 쪽입니다. 이것들은 표현식에 걸린 btree 인덱스입니다. 플래너는 평소의 규칙대로 이 인덱스를 고르며, 그 표현식과 맞출 수 있는 조건에만 도움이 됩니다. 어떤 조건이 해당되는지는 인덱스와 제약이 다룹니다.

이것으로 얻는 것#

PostgreSQL이 테이블에 해 주는 모든 것을 레이블에도 해 줍니다. 이것은 구호가 아니라, 두 번 배우지 않아도 되는 것들의 목록입니다:

  • 트랜잭션. 한 트랜잭션 안의 Cypher 쓰기와 SQL 쓰기는 함께 커밋되거나 함께 롤백됩니다. 같은 데이터베이스에 있는 테이블에 쓰는 것이기 때문입니다.

  • 접근 제어. GRANT SELECT ON moviekb.movie TO analyst가 동작하고, 레이블에 대한 행 수준 보안도 동작합니다. 보안을 참고하십시오.

  • 백업과 복제. pg_dump, pg_basebackup, WAL 아카이빙, 스트리밍 복제는 그래프를 있는 그대로 — 스키마와 테이블로 — 다룹니다.

  • 확장. pgvector가 그래프 옆에 임베딩을 저장할 수 있는 것은, 속성이 확장이 인덱싱할 수 있는 테이블 안의 jsonb이기 때문입니다. 벡터 검색 가이드 전체가 이 사실에 기반합니다.

  • 플래너. Cypher 쿼리에 EXPLAIN을 쓰면 순차 스캔, 인덱스 스캔, 조인이 보이고, 다른 PostgreSQL 쿼리를 튜닝하는 방식으로 그것을 튜닝합니다.

그리고 이것이 SQL과 Cypher가 다리를 필요로 하지 않고 한 문장 안에서 조합되는 이유입니다: SQL과 Cypher 섞기.

일부러 막아 둔 것#

대응 관계는 실제이지만, 그래프를 보통 테이블처럼 편집해도 된다는 뜻은 아닙니다. 관문 두 개가 길을 막고 있으며 둘 다 기본적으로 닫혀 있습니다. 어느 작업이든 그래프 카탈로그가 더 이상 사실이 아닌 것을 설명하는 상태로 만들 수 있기 때문입니다.

행을 직접 쓰는 것은 거부됩니다:

INSERT INTO moviekb.movie (properties) VALUES ('{"title": "x"}');
ERROR:  DML query to graph objects is not allowed

레이블의 모양을 바꾸는 것도 거부됩니다:

ALTER TABLE moviekb.movie ADD COLUMN foo int;
ERROR:  cannot add a column to graph label "movie" with ALTER TABLE
HINT:  Use ALTER VLABEL or ALTER ELABEL instead, which keeps the graph catalog in step.

enable_graph_dml과 enable_graph_ddl이 각각 그 문을 엽니다. 그렇게 함으로써 무엇을 감당하게 되는지는 서버 설정에 적혀 있습니다. SQL로 레이블을 읽는 데는 어떤 설정도 필요하지 않습니다 — 이 페이지의 모든 SELECT는 그대로 실행됩니다.