{#graph-on-postgresql-how-a-graph-is-stored} # 그래프가 어떻게 저장되는가 AgensGraph는 PostgreSQL에 그래프 엔진을 덧붙이지 않습니다. 이미 거기 있는 스키마, 테이블, 인덱스, 상속을 써서 그래프를 PostgreSQL 객체 *그 자체로* 저장합니다. 이 대응 관계를 알고 나면 PostgreSQL 운영에 대해 아는 것 대부분이 그대로 적용됩니다. 그리고 한 문장이 Cypher와 SQL을 섞을 수 있다는 것도 더는 놀랍지 않게 됩니다. 테이블은 처음부터 한 벌뿐이기 때문입니다. 대응 관계는 세 문장입니다. **그래프는 스키마입니다.** `CREATE GRAPH moviekb`는 `moviekb`라는 이름의 스키마를 만듭니다. **레이블은 테이블입니다.** `CREATE VLABEL movie`는 `moviekb.movie` 테이블을 만듭니다. **정점이나 간선은** 그 테이블의 **행입니다.** {#graph-on-postgresql-what-a-label-table-looks-like} ## 레이블 테이블은 어떻게 생겼는가 정점 레이블에는 컬럼이 두 개 있습니다: ```psql \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가 더해집니다: ```psql \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 ``` 레이블은 그 컬럼들보다 더 많은 것을 담을 수 있습니다. [속성 승격](../querying/typed-columns.md)은 `properties` 옆에 실제 타입이 있는 컬럼을 추가하고, 쓰기 시점에 그 값을 채웁니다. 그러면 자주 쓰는 속성이 두 번 저장됩니다 — 한 번은 jsonb 키로, 한 번은 플래너가 인덱싱하고 그 타입 그대로 비교할 수 있는 컬럼으로입니다. 어떤 속성 키가 어느 컬럼에서 답을 얻는지는 카탈로그 `ag_label_property`가 기록합니다. Cypher에서는 계속 키 이름을 부르므로 컬럼 이름이 쿼리에 나타나는 일은 없습니다. 그러므로 간선은 두 정점을 지목하는 행이고, 간선을 따라가는 것은 `graphid`에 대한 조인입니다. `(start, "end")`와 `("end", start)`에 걸린 btree 인덱스 두 개가 그 조인을 어느 방향으로든 빠르게 만들어 주며, 레이블을 만들 때 AgensGraph가 이 인덱스를 함께 만들어 줍니다. `id`에 걸린 BRIN 인덱스는 작고, 한 레이블의 id 범위를 스캔할 때 도움이 됩니다. 레이블 종류마다 어떤 인덱스가 붙는지 보십시오. 정점 레이블에는 `id`에 대한 기본 키가 있지만 간선 레이블에는 없습니다. 간선은 id로 찾는 것이 아니라 양 끝점을 통해 도달하기 때문입니다. `properties`가 하나의 `jsonb` 컬럼이므로, 레이블은 자기 행들에 컬럼 목록을 강제하지 않습니다. 두 개의 `movie` 정점이 완전히 다른 키를 가질 수 있습니다. 여기서 "스키마 없는 속성"이 뜻하는 것이 그것입니다. 그리고 속성을 읽는 것이 JSON 문서 안을 읽는 일이 되는 이유도 그것입니다. 그 비용이 언제 문제가 되고 무엇을 할 수 있는지는 [속성을 컬럼으로 승격하기](../querying/typed-columns.md)가 다룹니다. {#graph-on-postgresql-identity-the-graphid} ## 식별자: graphid `graphid`는 두 부분으로 이루어진 64비트 식별자입니다. 상위 16비트가 *레이블 id*이고, 하위 48비트가 레이블마다 매겨지는 시퀀스인 *로컬 id*입니다. 출력 형태는 `labid.locid`입니다: ```sql 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`에 있습니다: ```sql 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개의 행을 가집니다. [제한](../appendix/limits.md)을 참고하십시오. {#graph-on-postgresql-every-graph-has-two-labels-you-did-not-create} ## 모든 그래프에는 만들지 않은 레이블 두 개가 있다 `ag_vertex`와 `ag_edge`는 그래프와 함께 만들어지고, 직접 만드는 모든 레이블은 그중 하나를 상속합니다: ```sql 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`를 스캔하고, 상속을 통해 그 아래의 모든 테이블을 스캔합니다: ```cypher 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` 수정자를 쓰면 정확히 그 테이블로만 좁혀집니다. 그것으로 무엇을 만들 수 있는지는 [레이블 상속](label-inheritance.md)이 다룹니다. {#graph-on-postgresql-what-a-property-index-and-a-constraint-become} ## 속성 인덱스와 제약은 무엇이 되는가 Cypher의 속성 인덱스는 `properties` 컬럼에 대한 PostgreSQL 표현식 인덱스입니다: ```cypher CREATE PROPERTY INDEX ON movie (title); ``` ```psql \d moviekb.movie ``` ``` Indexes: "movie_pkey" PRIMARY KEY, btree (id) "movie_title_idx" btree ((properties.'title')) ``` 고유성 제약은 같은 표현식에 대한 배제 제약이 됩니다: ```cypher 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 인덱스입니다. 플래너는 평소의 규칙대로 이 인덱스를 고르며, 그 표현식과 맞출 수 있는 조건에만 도움이 됩니다. 어떤 조건이 해당되는지는 [인덱스와 제약](../querying/indexes-and-constraints.md)이 다룹니다. {#graph-on-postgresql-what-this-buys-you} ## 이것으로 얻는 것 PostgreSQL이 테이블에 해 주는 모든 것을 레이블에도 해 줍니다. 이것은 구호가 아니라, 두 번 배우지 않아도 되는 것들의 목록입니다: * **트랜잭션.** 한 트랜잭션 안의 Cypher 쓰기와 SQL 쓰기는 함께 커밋되거나 함께 롤백됩니다. 같은 데이터베이스에 있는 테이블에 쓰는 것이기 때문입니다. * **접근 제어.** `GRANT SELECT ON moviekb.movie TO analyst`가 동작하고, 레이블에 대한 행 수준 보안도 동작합니다. [보안](../operations/security.md)을 참고하십시오. * **백업과 복제.** `pg_dump`, `pg_basebackup`, WAL 아카이빙, 스트리밍 복제는 그래프를 있는 그대로 — 스키마와 테이블로 — 다룹니다. * **확장.** `pgvector`가 그래프 옆에 임베딩을 저장할 수 있는 것은, 속성이 확장이 인덱싱할 수 있는 테이블 안의 `jsonb`이기 때문입니다. [벡터 검색](../guides/search/vector.md) 가이드 전체가 이 사실에 기반합니다. * **플래너.** Cypher 쿼리에 `EXPLAIN`을 쓰면 순차 스캔, 인덱스 스캔, 조인이 보이고, 다른 PostgreSQL 쿼리를 튜닝하는 방식으로 그것을 튜닝합니다. 그리고 이것이 SQL과 Cypher가 다리를 필요로 하지 않고 한 문장 안에서 조합되는 이유입니다: [SQL과 Cypher 섞기](../querying/combining-sql-and-cypher.md). {#graph-on-postgresql-what-is-deliberately-closed-off} ## 일부러 막아 둔 것 대응 관계는 실제이지만, 그래프를 보통 테이블처럼 편집해도 된다는 뜻은 아닙니다. 관문 두 개가 길을 막고 있으며 둘 다 기본적으로 닫혀 있습니다. 어느 작업이든 그래프 카탈로그가 더 이상 사실이 아닌 것을 설명하는 상태로 만들 수 있기 때문입니다. 행을 직접 쓰는 것은 거부됩니다: ```psql INSERT INTO moviekb.movie (properties) VALUES ('{"title": "x"}'); ``` ``` ERROR: DML query to graph objects is not allowed ``` 레이블의 모양을 바꾸는 것도 거부됩니다: ```sql 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`이 각각 그 문을 엽니다. 그렇게 함으로써 무엇을 감당하게 되는지는 [서버 설정](../reference/guc.md)에 적혀 있습니다. SQL로 레이블을 읽는 데는 어떤 설정도 필요하지 않습니다 — 이 페이지의 모든 `SELECT`는 그대로 실행됩니다.