그래프가 어떻게 저장되는가#
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는 그대로 실행됩니다.