데이터 적재#

그래프에 데이터를 넣는 방법은 셋이며, 무엇이 맞는지는 데이터의 양에 달려 있습니다.

경로

알맞은 경우

속도

Cypher CREATE / MERGE

요소 수십만 개까지. Cypher의 검사를 원하는 모든 경우

행당 가장 느리고 가장 안전합니다

테이블에서 LOAD FROM

이미 테이블에 있는 데이터, 또는 외부 테이블로 노출한 파일

빠르며 Cypher 안에 머무릅니다

레이블 테이블에 COPY

직접 다루는 파일에서 오는 수백만 개의 요소

큰 차이로 가장 빠릅니다

어느 것을 쓰든 대량 적재 중의 인덱스 절이 적용되고, 적재를 마친 뒤 점검 목록도 적용됩니다. 갓 적재한 그래프의 쿼리가 나쁜 이유는 대개 두 번째를 건너뛴 것입니다.

Cypher CREATE#

가장 뻔한 경로이며, 너무 느려지기 전까지 쓸 방법입니다. 묶어서 하십시오. 요소를 여러 개 만드는 문장 하나가, 하나씩 만드는 문장 여러 개보다 낫습니다:

CREATE (:person {name: 'Ann', born: 1990}),
       (:person {name: 'Bo',  born: 1985}),
       (:person {name: 'Cy',  born: 1979});

간선은 양 끝점을 먼저 찾아야 하므로 같은 문장에서 함께 일치시키십시오:

MATCH (a:person {name: 'Ann'}), (b:person {name: 'Bo'})
CREATE (a)-[:knows {since: 2019}]->(b);

간선을 MATCH 하나씩 적재하는 것이 이 경로가 느린 이유입니다. 문장마다 양 끝점을 다시 찾기 때문입니다. 간선을 대량으로 적재한다면 다른 두 경로 중 하나를 써서 서버가 그 탐색을 조인으로 하게 하십시오.

적재가 두 번 돌 수 있고 그것이 멱등이기를 바란다면 CREATE 대신 MERGE를 쓰십시오. 다만 요소마다 탐색 비용이 든다는 점을 알아 두십시오.

테이블에서 LOAD FROM#

LOAD FROM은 평범한 테이블에서 행을 읽어 Cypher 쓰기 절에 넘기므로, 끝점 탐색이 행마다의 쿼리가 아니라 조인 하나가 됩니다. 데이터가 이미 관계형일 때 자연스러운 경로입니다. file_fdw로 외부 테이블로 노출한 파일도 여기에 들어갑니다.

행이 이미 스테이징 테이블 두 개에 들어와 있다고 합시다:

CREATE TABLE staging_people (name text, born int);
INSERT INTO staging_people VALUES ('Ann', 1990), ('Bo', 1985);

CREATE TABLE staging_knows (from_name text, to_name text, since int);
INSERT INTO staging_knows VALUES ('Ann', 'Bo', 2019);

그러면 문장 하나가 각 행을 정점으로 바꿉니다:

LOAD FROM staging_people AS row
CREATE (:person {name: row.name, born: row.born});

또는 행 전체를 속성 맵으로 받을 수도 있습니다:

LOAD FROM staging_people AS row
CREATE (:person = to_jsonb(row_to_json(row)));

중요한 경우는 간선입니다. 속도가 나오는 곳이 그 조인이기 때문입니다:

LOAD FROM staging_knows AS row
MATCH (a:person {name: row.from_name}), (b:person {name: row.to_name})
CREATE (a)-[:knows {since: row.since}]->(b);

그것을 실행하기 전에 일치에 쓰는 속성에 인덱스를 만드십시오. 아래를 참고하십시오.

파일을 테이블로 읽기#

file_fdw는 서버 파일시스템의 파일을 읽기 전용 테이블로 만듭니다. contrib 모듈이므로 빌드할 때 make install-world가 필요합니다(AgensGraph 설치 참고):

참고

아래 두 블록은 file_fdw가 필요하므로, contrib 모듈을 빌드하지 않았다면 이 페이지에서 유일하게 실행할 수 없는 부분입니다.

CREATE EXTENSION file_fdw;
CREATE SERVER import FOREIGN DATA WRAPPER file_fdw;

CREATE FOREIGN TABLE staging_people (name text, born int)
SERVER import
OPTIONS (filename '/data/people.csv', format 'csv', header 'true');

그다음은 위와 같이 LOAD FROM staging_people AS row … 입니다. 파일은 서버에서 읽으므로 경로는 서버의 경로이고, 서버를 실행하는 사용자가 그것을 읽을 수 있어야 합니다.

파일이 서버가 아니라 자기 장비에 있다면, 먼저 agens 클라이언트의 \copy로 평범한 테이블에 적재한 뒤 그것을 LOAD FROM 하십시오.

레이블 테이블에 COPY#

가장 빠른 경로이며, 요소가 수백만 개일 때 쓸 방법입니다. 레이블은 테이블이므로 거기에 곧바로 COPY 할 수 있습니다. properties 컬럼을 직접 쓰고, id 컬럼은 기본값이 스스로 채우게 두는 것입니다.

SQL로 레이블에 쓰는 것은 막혀 있으므로, 적재 동안 그 관문을 여십시오:

SET enable_graph_dml = on;

COPY moviekb.person (properties) FROM STDIN;
{"name": "Ann", "born": 1990}
{"name": "Bo", "born": 1985}
\.

간선에는 양 끝점의 id도 labid.locid 형태로 함께 주십시오:

SET enable_graph_dml = on;

COPY moviekb.acted_in (start, "end", properties) FROM STDIN;
4.1	3.1	{"role": "Neo"}
4.2	3.1	{"role": "Trinity"}
\.

그러므로 일은 그 id를 만들어 내는 데 있습니다. 두 가지 방법이 있습니다:

산술로 만드십시오. 번호 매김을 직접 다룬다면 graphid()가 레이블 id와 로컬 id로 id를 조립합니다:

SELECT graphid(graph_labid('moviekb.person'::cstring), 100::int8);
 made_id
---------
 4.100

그러므로 행 번호를 매기는 적재기는 정점 id와 그것을 가리키는 간선 끝점을 탐색 없이 함께 내보낼 수 있습니다. 가능한 가장 빠른 적재입니다.

또는 조인으로 풀어내십시오. 정점을 먼저 적재한 다음, 업무 키로 조인해 스테이징 테이블에서 간선 행을 만듭니다:

CREATE TABLE staging_roles (person_name text, movie_title text, role text);
INSERT INTO staging_roles VALUES ('Keanu Reeves', 'The Matrix', 'Neo (again)');

SET enable_graph_dml = on;

INSERT INTO moviekb.acted_in (start, "end", properties)
SELECT p.id, m.id, jsonb_build_object('role', s.role)
FROM staging_roles s
  JOIN moviekb.person p ON p.properties->>'name'  = s.person_name
  JOIN moviekb.movie  m ON m.properties->>'title' = s.movie_title;

간선마다의 쿼리가 아니라 집합 단위 문장 하나이며, 요점이 바로 그것입니다. 조인에 쓰는 키에 먼저 인덱스를 만드십시오.

DROP TABLE staging_roles;

경고

COPY와 INSERT는 Cypher가 하는 검사를 건너뜁니다. start가 존재하지 않는 정점을 가리키는 간선을 쓰는 것을 아무것도 막지 않으며, 그 결과는 어떤 쿼리도 제대로 순회할 수 없는 그래프입니다. 적재하기 전에 양 끝점을 확인하고, 끝난 뒤에는 enable_graph_dml을 다시 끄십시오.

대량 적재 중의 인덱스#

행 수백만 개가 들어오는 동안 인덱스를 유지하는 것은 비쌉니다. 레이블은 인덱스를 꺼 둔 채로 만들 수 있습니다:

CREATE VLABEL bulk_person DISABLE INDEX;

나중에 끌 수도 있습니다:

ALTER VLABEL bulk_person DISABLE INDEX;

데이터를 적재한 뒤 다시 만듭니다:

REINDEX VLABEL bulk_person;

참고

ENABLE INDEX는 없습니다. ALTER VLABEL person ENABLE INDEX는 구문 오류이고, 인덱스를 되살리는 것은 REINDEX입니다. 그것을 실행하기 전까지 레이블의 인덱스는 존재하지만 쓸 수 없으므로, 쿼리는 순차 스캔으로 물러나고 고유 제약도 적용되지 않습니다.

큰 적재에 도움이 되는 것 두 가지가 더 있습니다:

속성 인덱스는 데이터보다 먼저가 아니라 나중에 만드십시오. 다 찬 레이블에 인덱스를 한 번 만드는 것이 행마다 유지하는 것보다 훨씬 쌉니다. 속성을 컬럼으로 승격하는 것도 마찬가지입니다. ALTER가 레이블 전체를 다시 쓰므로, 적재 전이 아니라 적재 뒤에 한 번 승격하십시오.

다시 적재할 수 있는 데이터라면 unlogged 레이블을 고려하십시오. WAL을 통째로 건너뜁니다:

CREATE UNLOGGED VLABEL staging_person;

unlogged 레이블은 크래시를 견디지 못하고 복제되지도 않습니다. 그러므로 이것은 스테이징과 다시 만들 수 있는 그래프를 위한 것이지, 최종 데이터를 위한 것이 아닙니다.

재인덱싱 전에 maintenance_work_mem을 올리십시오. 한 번에 명령 하나만 쓰므로 임시로 큰 값을 두는 것은 안전합니다. 설정을 참고하십시오.

적재를 마친 뒤#

세 단계이며, 어느 것도 선택 사항이 아닙니다.

인덱스를 꺼 두었다면 위와 같이 다시 만드십시오.

승격할 생각이라면 걸러 내기와 정렬에 쓰는 속성을 승격하십시오. ALTER VLABEL person ADD COLUMN age int GENERATED는 jsonb properties 옆에 속성 age를 위한 실제 int 컬럼을 만들고 쓰기 시점에 채우며, 그것에 대한 인덱스는 평범한 컬럼 인덱스입니다. 쿼리가 계속 이름을 대는 것은 속성 키입니다. p.age는 여전히 age를 읽고, 컬럼 이름은 Cypher에 나타나지 않습니다. 컬럼 이름을 키와 다르게 하려면 GENERATED (sourceKey)를 쓰십시오. 속성을 컬럼으로 승격하기를 참고하십시오.

ANALYZE 하십시오. 갓 적재한 레이블에는 플래너가 쓸 통계가 없고, 그래프 순회 계획은 통계에 매우 민감합니다:

ANALYZE moviekb.person;
ANALYZE moviekb.acted_in;

그래프 메타데이터를 수집하십시오. ag_graphmeta는 그래프에 어떤 (시작 레이블, 간선 레이블, 끝 레이블) 삼중이 나타나고 각각이 간선 몇 개로 이루어지는지 기록합니다. 그것이 패턴이 닿을 수 없는 레이블의 스캔을 플래너가 쳐낼 수 있게 합니다:

SELECT regather_graphmeta();
SELECT graphname, graphmeta_valid FROM pg_catalog.ag_graph;

그런 다음 적재한 것의 모양을 확인하십시오. 잘못된 적재를 잡아내는 가장 빠른 방법이기도 합니다:

SELECT * FROM pg_catalog.ag_graphmeta_view WHERE graphname = 'moviekb';
 graphname | start  |    edge    |  end  | edgecount
-----------+--------+------------+-------+-----------
 moviekb   | movie  | related_to | movie |        11
 moviekb   | person | directed   | movie |        21
 moviekb   | person | acted_in   | movie |        25

기대하지 않은 간선 모양이나 있어야 할 값의 몇 배인 개수가 여기서 곧바로 드러납니다.

적재가 끝나면 스테이징 테이블과 임시 레이블을 정리하십시오:

DROP TABLE staging_people, staging_knows;
DROP VLABEL bulk_person;
DROP VLABEL IF EXISTS staging_person;

내보내기#

pg_dump는 그래프를 담으며, 스키마를 지정하면 그래프 하나를 내보냅니다. 백업과 복구를 참고하십시오. 구조가 아니라 데이터를 원한다면 레이블 테이블에 COPY … TO가 동작하고, SQL로 감싼 Cypher 쿼리는 원하는 모양 그대로를 내보냅니다:

\copy (SELECT p.properties->>'name' AS name, m.properties->>'title' AS film
       FROM moviekb.acted_in r
         JOIN moviekb.person p ON p.id = r.start
         JOIN moviekb.movie  m ON m.id = r."end") TO 'roles.csv' CSV HEADER