{#loading-data-loading-data} # 데이터 적재 그래프에 데이터를 넣는 방법은 셋이며, 무엇이 맞는지는 데이터의 양에 달려 있습니다. | 경로 | 알맞은 경우 | 속도 | | --- | --- | --- | | Cypher `CREATE` / `MERGE` | 요소 수십만 개까지. Cypher의 검사를 원하는 모든 경우 | 행당 가장 느리고 가장 안전합니다 | | 테이블에서 `LOAD FROM` | 이미 테이블에 있는 데이터, 또는 외부 테이블로 노출한 파일 | 빠르며 Cypher 안에 머무릅니다 | | 레이블 테이블에 `COPY` | 직접 다루는 파일에서 오는 수백만 개의 요소 | 큰 차이로 가장 빠릅니다 | 어느 것을 쓰든 [대량 적재 중의 인덱스](#loading-data-indexes-during-a-bulk-load) 절이 적용되고, [적재를 마친 뒤](#loading-data-after-the-load) 점검 목록도 적용됩니다. 갓 적재한 그래프의 쿼리가 나쁜 이유는 대개 두 번째를 건너뛴 것입니다. {#loading-data-cypher-create} ## Cypher CREATE 가장 뻔한 경로이며, 너무 느려지기 전까지 쓸 방법입니다. 묶어서 하십시오. 요소를 여러 개 만드는 문장 하나가, 하나씩 만드는 문장 여러 개보다 낫습니다: ```cypher CREATE (:person {name: 'Ann', born: 1990}), (:person {name: 'Bo', born: 1985}), (:person {name: 'Cy', born: 1979}); ``` 간선은 양 끝점을 먼저 찾아야 하므로 같은 문장에서 함께 일치시키십시오: ```cypher MATCH (a:person {name: 'Ann'}), (b:person {name: 'Bo'}) CREATE (a)-[:knows {since: 2019}]->(b); ``` 간선을 `MATCH` 하나씩 적재하는 것이 이 경로가 느린 이유입니다. 문장마다 양 끝점을 다시 찾기 때문입니다. 간선을 대량으로 적재한다면 다른 두 경로 중 하나를 써서 서버가 그 탐색을 조인으로 하게 하십시오. 적재가 두 번 돌 수 있고 그것이 멱등이기를 바란다면 `CREATE` 대신 `MERGE`를 쓰십시오. 다만 요소마다 탐색 비용이 든다는 점을 알아 두십시오. {#loading-data-load-from-a-table} ## 테이블에서 LOAD FROM `LOAD FROM`은 평범한 테이블에서 행을 읽어 Cypher 쓰기 절에 넘기므로, 끝점 탐색이 행마다의 쿼리가 아니라 조인 하나가 됩니다. 데이터가 이미 관계형일 때 자연스러운 경로입니다. `file_fdw`로 외부 테이블로 노출한 파일도 여기에 들어갑니다. 행이 이미 스테이징 테이블 두 개에 들어와 있다고 합시다: ```sql 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); ``` 그러면 문장 하나가 각 행을 정점으로 바꿉니다: ```cypher LOAD FROM staging_people AS row CREATE (:person {name: row.name, born: row.born}); ``` 또는 행 전체를 속성 맵으로 받을 수도 있습니다: ```cypher LOAD FROM staging_people AS row CREATE (:person = to_jsonb(row_to_json(row))); ``` 중요한 경우는 간선입니다. 속도가 나오는 곳이 그 조인이기 때문입니다: ```cypher 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); ``` 그것을 실행하기 전에 일치에 쓰는 속성에 인덱스를 만드십시오. 아래를 참고하십시오. {#loading-data-reading-a-file-as-a-table} ### 파일을 테이블로 읽기 `file_fdw`는 서버 파일시스템의 파일을 읽기 전용 테이블로 만듭니다. contrib 모듈이므로 빌드할 때 `make install-world`가 필요합니다([AgensGraph 설치](#install-build-and-install) 참고): ```{note} 아래 두 블록은 `file_fdw`가 필요하므로, contrib 모듈을 빌드하지 않았다면 이 페이지에서 유일하게 실행할 수 없는 부분입니다. ``` ```sql 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 클라이언트](../reference/agens.md)의 `\copy`로 평범한 테이블에 적재한 뒤 그것을 `LOAD FROM` 하십시오. {#loading-data-copy-into-the-label-tables} ## 레이블 테이블에 COPY 가장 빠른 경로이며, 요소가 수백만 개일 때 쓸 방법입니다. [레이블은 테이블](../concepts/graph-on-postgresql.md)이므로 거기에 곧바로 `COPY` 할 수 있습니다. `properties` 컬럼을 직접 쓰고, `id` 컬럼은 기본값이 스스로 채우게 두는 것입니다. SQL로 레이블에 쓰는 것은 막혀 있으므로, 적재 동안 그 관문을 여십시오: ```psql SET enable_graph_dml = on; COPY moviekb.person (properties) FROM STDIN; {"name": "Ann", "born": 1990} {"name": "Bo", "born": 1985} \. ``` 간선에는 양 끝점의 id도 `labid.locid` 형태로 함께 주십시오: ```psql 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를 조립합니다: ```sql SELECT graphid(graph_labid('moviekb.person'::cstring), 100::int8); ``` ``` made_id --------- 4.100 ``` 그러므로 행 번호를 매기는 적재기는 정점 id와 그것을 가리키는 간선 끝점을 탐색 없이 함께 내보낼 수 있습니다. 가능한 가장 빠른 적재입니다. **또는 조인으로 풀어내십시오.** 정점을 먼저 적재한 다음, 업무 키로 조인해 스테이징 테이블에서 간선 행을 만듭니다: ```sql 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; ``` 간선마다의 쿼리가 아니라 집합 단위 문장 하나이며, 요점이 바로 그것입니다. 조인에 쓰는 키에 먼저 인덱스를 만드십시오. ```sql DROP TABLE staging_roles; ``` ```{warning} `COPY`와 `INSERT`는 Cypher가 하는 검사를 건너뜁니다. `start`가 존재하지 않는 정점을 가리키는 간선을 쓰는 것을 아무것도 막지 않으며, 그 결과는 어떤 쿼리도 제대로 순회할 수 없는 그래프입니다. 적재하기 전에 양 끝점을 확인하고, 끝난 뒤에는 `enable_graph_dml`을 다시 끄십시오. ``` {#loading-data-indexes-during-a-bulk-load} ## 대량 적재 중의 인덱스 행 수백만 개가 들어오는 동안 인덱스를 유지하는 것은 비쌉니다. 레이블은 인덱스를 꺼 둔 채로 만들 수 있습니다: ```cypher CREATE VLABEL bulk_person DISABLE INDEX; ``` 나중에 끌 수도 있습니다: ```cypher ALTER VLABEL bulk_person DISABLE INDEX; ``` 데이터를 적재한 뒤 다시 만듭니다: ```cypher REINDEX VLABEL bulk_person; ``` ```{note} `ENABLE INDEX`는 없습니다. `ALTER VLABEL person ENABLE INDEX`는 구문 오류이고, 인덱스를 되살리는 것은 `REINDEX`입니다. 그것을 실행하기 전까지 레이블의 인덱스는 존재하지만 쓸 수 없으므로, 쿼리는 순차 스캔으로 물러나고 고유 제약도 적용되지 않습니다. ``` 큰 적재에 도움이 되는 것 두 가지가 더 있습니다: **속성 인덱스는 데이터보다 먼저가 아니라 나중에 만드십시오.** 다 찬 레이블에 인덱스를 한 번 만드는 것이 행마다 유지하는 것보다 훨씬 쌉니다. [속성을 컬럼으로 승격하는 것](../querying/typed-columns.md)도 마찬가지입니다. `ALTER`가 레이블 전체를 다시 쓰므로, 적재 전이 아니라 적재 뒤에 한 번 승격하십시오. 다시 적재할 수 있는 데이터라면 **unlogged 레이블을 고려하십시오.** WAL을 통째로 건너뜁니다: ```cypher CREATE UNLOGGED VLABEL staging_person; ``` unlogged 레이블은 크래시를 견디지 못하고 복제되지도 않습니다. 그러므로 이것은 스테이징과 다시 만들 수 있는 그래프를 위한 것이지, 최종 데이터를 위한 것이 아닙니다. 재인덱싱 전에 `maintenance_work_mem`을 올리십시오. 한 번에 명령 하나만 쓰므로 임시로 큰 값을 두는 것은 안전합니다. [설정](#configuration-memory)을 참고하십시오. {#loading-data-after-the-load} ## 적재를 마친 뒤 세 단계이며, 어느 것도 선택 사항이 아닙니다. 인덱스를 꺼 두었다면 위와 같이 **다시 만드십시오.** 승격할 생각이라면 **걸러 내기와 정렬에 쓰는 속성을 승격하십시오.** `ALTER VLABEL person ADD COLUMN age int GENERATED`는 jsonb `properties` 옆에 속성 `age`를 위한 실제 `int` 컬럼을 만들고 쓰기 시점에 채우며, 그것에 대한 인덱스는 평범한 컬럼 인덱스입니다. 쿼리가 계속 이름을 대는 것은 속성 키입니다. `p.age`는 여전히 `age`를 읽고, 컬럼 이름은 Cypher에 나타나지 않습니다. 컬럼 이름을 키와 다르게 하려면 `GENERATED (sourceKey)`를 쓰십시오. [속성을 컬럼으로 승격하기](../querying/typed-columns.md)를 참고하십시오. **`ANALYZE` 하십시오.** 갓 적재한 레이블에는 플래너가 쓸 통계가 없고, 그래프 순회 계획은 통계에 매우 민감합니다: ```sql ANALYZE moviekb.person; ANALYZE moviekb.acted_in; ``` **그래프 메타데이터를 수집하십시오.** `ag_graphmeta`는 그래프에 어떤 (시작 레이블, 간선 레이블, 끝 레이블) 삼중이 나타나고 각각이 간선 몇 개로 이루어지는지 기록합니다. 그것이 패턴이 닿을 수 없는 레이블의 스캔을 플래너가 쳐낼 수 있게 합니다: ```cypher SELECT regather_graphmeta(); ``` ```sql SELECT graphname, graphmeta_valid FROM pg_catalog.ag_graph; ``` 그런 다음 적재한 것의 모양을 확인하십시오. 잘못된 적재를 잡아내는 가장 빠른 방법이기도 합니다: ```sql 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 ``` 기대하지 않은 간선 모양이나 있어야 할 값의 몇 배인 개수가 여기서 곧바로 드러납니다. 적재가 끝나면 스테이징 테이블과 임시 레이블을 정리하십시오: ```sql DROP TABLE staging_people, staging_knows; ``` ```cypher DROP VLABEL bulk_person; DROP VLABEL IF EXISTS staging_person; ``` {#loading-data-exporting} ## 내보내기 `pg_dump`는 그래프를 담으며, 스키마를 지정하면 그래프 하나를 내보냅니다. [백업과 복구](../operations/backup.md)를 참고하십시오. 구조가 아니라 데이터를 원한다면 레이블 테이블에 `COPY … TO`가 동작하고, SQL로 감싼 Cypher 쿼리는 원하는 모양 그대로를 내보냅니다: ```psql \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 ```