このトピックでは、PolarDB for PostgreSQL が提供する AGE 拡張機能を使用して、大量のデータを迅速にインポートする方法について説明します。このアプローチにより、数億個のノードとエッジをグラフデータベースに効率的にインポートできます。これにより、エッジ挿入時のクエリパフォーマンスの問題を回避できます。
背景情報
PolarDB for PostgreSQL は、グラフデータの保存とクエリを容易にするために AGE 拡張機能 を提供しています。
グラフデータベースデータをインポートおよびエクスポートするには、ノードデータとエッジデータを CSV などの形式のテーブルに変換し、ノードテーブルをインポートしてから、エッジテーブルに基づいてエッジを作成する必要があります。ただし、Cypher では、エッジの作成に使用する MATCH...CREATE 文は、対応するノードもクエリします。これにより、特に数千万個以上のエッジをインポートする場合、インポート速度が低下します。
このトピックでは、グラフデータをインポートするための新しい方法について説明します。この方法では、グラフ上のノードの一意の ID を直接生成および管理するため、MATCH ステップを実行せずにエッジを挿入できます。グラフのインポート効率を大幅に向上させることができます。この方法には、次の手順が含まれます。
グラフノードを共通テーブルにインポートします。テーブルの 1 つの列または一連の列が一意の ID として使用されます。
グラフエッジを共通テーブルにインポートします。エッジレコードには、左ノードの一意の ID、右ノードの一意の ID、およびエッジプロパティが含まれています。
ノードテーブルに一意の ID 列を追加します。エッジテーブルでは、JOIN または計算を使用して、開始ノードと終了ノードの一意の ID 列を追加します。
一意の ID 列に基づいて、ノードテーブルをグラフにインポートします。
一意の ID に基づいて、エッジテーブルをグラフにインポートします。
前提条件
PolarDB for PostgreSQL クラスタは、次のいずれかのエンジンバージョンを実行しています。
PostgreSQL 16 (リビジョンバージョン 2.0.16.8.3.0 以降)。
PostgreSQL 15 (リビジョンバージョン 2.0.15.12.4.0 以降)。
PostgreSQL 14 (リビジョンバージョン 2.0.14.12.24.0 以降)。
コンソールでリビジョンバージョンを表示するか、SHOW polardb_version; 文を実行してリビジョンバージョンをクエリできます。リビジョンバージョンをアップグレードする必要がある場合は、リビジョンバージョン バージョン管理 をご参照ください。
ベストプラクティス
このトピックの例には、次の要素が含まれています。
v_userとv_productという名前の 2 つのノードタイプ。v_user: タイプが異なるノードの uid が同じになる場合があるため、一意の ID は type 列と uid 列の両方から作成されます。v_product: product_id 列が一意の ID として使用されます。この列の値は、数値 ID に変換できない複雑な文字列です。シーケンス番号は手動で追加する必要があります。
e_own という名前のエッジタイプ。
ノードデータとエッジデータがインポートされるテーブル (xxx_raw という名前)。
ノードとエッジがインポートされる Toys という名前のグラフ。
データインポートの準備
AGE 拡張機能を作成します。
CREATE EXTENSION age;データベースまたは検索パスに拡張機能を追加し、現在のユーザーのライブラリをプリロードします。
説明データ管理 (DMS) を使用して
search_pathを構成すると、互換性の問題が発生する場合があります。そのような場合は、PolarDB-Tools を使用して関連する文を実行できます。ALTER DATABASE <dbname> SET search_path = "$user", public, ag_catalog; ALTER DATABASE <dbname> SET session_preload_libraries TO 'age';2 つのノードタイプと 1 つのエッジタイプ (v は頂点、e はエッジ) を持つグラフを作成します。
---グラフを作成する SELECT create_graph('toys'); ---ノードタイプを作成する SELECT create_vlabel('toys','v_user'); SELECT create_vlabel('toys','v_product'); ---エッジタイプを作成する SELECT create_elabel('toys','e_own');インポートするノードデータを格納するノードテーブルを作成します。
CREATE TABLE public.v_user_raw(type text, uid text, name text, age integer); INSERT INTO v_user_raw VALUES ('A','U1', 'Alice', '33'), ('B','U1', 'Bob', '21'); CREATE TABLE public.v_product_raw(product_id text, price double precision); INSERT INTO v_product_raw VALUES ('INAKLIDAS', '50'), ('ENKUCLKSD', '80'), ('IIUIHAKLS', '320'), ('SDVDSUHEE', '340');インポートするエッジデータを格納するエッジテーブルを作成します。
CREATE TABLE public.e_own_raw(user_type text, user_uid text, product_id text, buy_price text); INSERT INTO e_own_raw VALUES ('A', 'U1', 'INAKLIDAS', '45'), ('B', 'U1', 'ENKUCLKSD', '70'), ('B', 'U1', 'INAKLIDAS', '50'), ('B', 'U1', 'SDVDSUHEE', '330');
一意の ID を作成する
この例では、
v_userノードには 10 タイプ (A から J) のみ含まれています。一意の ID は、uidとtypeの組み合わせです。このマッピング関係を関数として登録します。CREATE OR REPLACE FUNCTION v_user_idgen(type text, uid text) RETURNS bigint AS $$ SELECT (ASCII(type) - ASCII('A')) + substring(uid, 2)::bigint * 10 $$ language SQL;関連するノードテーブルとエッジテーブルに ID 列を追加します。
ALTER TABLE v_user_raw ADD COLUMN _id bigint UNIQUE CHECK(_id < 281474976710656 AND _id>=0); UPDATE v_user_raw SET _id = v_user_idgen(type, uid); ALTER TABLE e_own_raw ADD COLUMN _fromid bigint CHECK(_fromid < 281474976710656 AND _fromid>=0); UPDATE e_own_raw SET _fromid = v_user_idgen(user_type, user_uid);v_productノードの場合、product_id 文字列を bigint タイプの数値に直接変換することはできません。PostgreSQL が提供する bigserial タイプを使用してシーケンス番号を自動的に生成し、結合操作を使用して対応する ID をエッジテーブルに追加できます。次に、高速化のため、一意の ID を形成する列にインデックスを作成します。説明一意の ID を形成する複数の列にインデックスを作成するには、
CREATE INDEX ON v_product_raw(column1, column2,...);構文を使用します。ALTER TABLE v_product_raw ADD COLUMN _id bigserial UNIQUE CHECK(_id < 281474976710656 AND _id>=0); CREATE INDEX ON v_product_raw(product_id); ALTER TABLE e_own_raw ADD COLUMN _toid bigint CHECK(_toid < 281474976710656 AND _toid>=0); UPDATE e_own_raw SET _toid = v_product_raw._id FROM v_product_raw WHERE v_product_raw.product_id = e_own_raw.product_id;
前述の方法には、次の違いがあります。
登録されたマッピング関数により、後続の管理が容易になります。プロパティとノード ID 間のマッピングを簡単に管理するために、同じ関数を使用できます。
bigserial タイプを使用してシーケンス番号を自動的に生成することにより、元の列を数値タイプに変換する必要はありません。ただし、結合操作を実行する必要があります。また、単純な関数を使用してプロパティとノード ID をマッピングして特定の操作を実行することはできません。
高速データインポート
PolarDB for PostgreSQL の内部一意 ID を生成するための補助関数を作成します。
CREATE OR REPLACE FUNCTION age_name_to_idx_start(graph_name text, kind_name text, label_name text) RETURNS bigint AS 'SELECT id::bigint<<48 FROM ag_catalog.ag_label WHERE kind = kind_name and name = label_name and graph = (SELECT graphid FROM ag_catalog.ag_graph WHERE name = graph_name)' language SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION age_name_to_seq(graph_name text, kind_name text, label_name text) RETURNS text AS 'SELECT graph_name || ''.'' || seq_name::text FROM ag_catalog.ag_label WHERE kind = kind_name and name = label_name and graph = (SELECT graphid FROM ag_catalog.ag_graph WHERE name = graph_name)' language SQL IMMUTABLE STRICT PARALLEL SAFE;ノードをインポートします。
(オプション) ノードをインポートする前に、一意の制約を作成して重複 ID を防ぐことができます。これらのインデックスは、後続のクエリを高速化できます。作成後も保持できます。
CREATE UNIQUE INDEX ON toys.v_user(id); CREATE UNIQUE INDEX ON toys.v_product(id);ノードをインポートする際は、_raw テーブルからノードに追加する列を選択します。_id などの新しく追加された列をノードから除外できます。
INSERT INTO toys."v_user" SELECT (age_name_to_idx_start('toys', 'v', 'v_user') + _id)::text::graphid, row_to_json((select x FROM (select type, uid, name, age) x))::text::agtype FROM v_user_raw; SELECT setval(age_name_to_seq('toys', 'v', 'v_user'), (SELECT max(_id) + 1 FROM v_user_raw)); INSERT INTO toys."v_product" SELECT (age_name_to_idx_start('toys', 'v', 'v_product') + _id)::text::graphid, row_to_json((select x FROM (select product_id, price) x))::text::agtype FROM v_product_raw; SELECT setval(age_name_to_seq('toys', 'v', 'v_product'), (SELECT max(_id) + 1 FROM v_product_raw));
エッジをインポートします。エッジは、_fromid テーブルと _toid テーブルに基づいて生成されます。両側のノードのプロパティを除外し、buy_price プロパティのみを保持できます。
INSERT INTO toys."e_own" SELECT (age_name_to_idx_start('toys', 'e', 'e_own') + nextval(age_name_to_seq('toys', 'e', 'e_own')))::text::graphid, (age_name_to_idx_start('toys', 'v', 'v_user') + _fromid)::text::graphid, (age_name_to_idx_start('toys', 'v', 'v_product') + _toid)::text::graphid, row_to_json((select x FROM (select buy_price) x))::text::agtype FROM e_own_raw;
テスト検証
ノードをクエリします。
SELECT * FROM cypher('toys', $$ MATCH (v:v_user) RETURN v $$) as (v agtype);サンプル結果:
v -------------------------------------------------------------------------------------------------------------------------- {"id": 844424930131978, "label": "v_user", "properties": {"age": 33, "uid": "U1", "name": "Alice", "type": "A"}}::vertex {"id": 844424930131979, "label": "v_user", "properties": {"age": 21, "uid": "U1", "name": "Bob", "type": "B"}}::vertex (2 rows)SELECT * FROM cypher('toys', $$ MATCH (v:v_product) RETURN v $$) as (v agtype);サンプル結果:
v ------------------------------------------------------------------------------------------------------------------- {"id": 1125899906842625, "label": "v_product", "properties": {"price": "50", "product_id": "INAKLIDAS"}}::vertex {"id": 1125899906842626, "label": "v_product", "properties": {"price": "80", "product_id": "ENKUCLKSD"}}::vertex {"id": 1125899906842627, "label": "v_product", "properties": {"price": "320", "product_id": "IIUIHAKLS"}}::vertex {"id": 1125899906842628, "label": "v_product", "properties": {"price": "340", "product_id": "SDVDSUHEE"}}::vertex (4 rows)
エッジをクエリします。
Bob が各購入に使用する割引をクエリします。
SELECT buy, sell, sell::int - buy::int as discount FROM cypher('toys', $$ MATCH (v1:v_user)-[e:e_own]-(v2:v_product) WHERE v1.name = 'Bob' RETURN e.buy_price, v2.price $$) as (buy agtype, sell agtype);サンプル結果:
buy | sell | discount -------+-------+---------- "70" | "80" | 10 "50" | "50" | 0 "330" | "340" | 10 (3 rows)
後続のデータのインポートと更新
Cypher で
CREATE文を使用してノードを作成した後、高速データインポートメソッドを使用することはできません。CREATE文は、xxx_idgen 関数または JOIN 操作によって生成された一意の ID マッピングを破壊する可能性があります。インポートが完了したら、この方法を使用して再度データをインポートする場合を除き、xxx_raw テーブルと xxx_idgen 関数を削除できます。グラフデータは影響を受けません。インポート中は、ノードとエッジが重複しないように、既にインポートされているノードとエッジをスキップします。
この方法を使用して新しいノードを挿入します。
INSERT INTO v_user_raw VALUES ('A','U2', 'Alien', '99', v_user_idgen('A', 'U2')); INSERT INTO toys.v_user SELECT (age_name_to_idx_start('toys', 'v', 'v_user') + _id)::text::graphid, row_to_json((select x FROM (select type, uid, name, age) x))::text::agtype FROM (SELECT (('A','U2', 'Alien', '97', v_user_idgen('A', 'U2'))::v_user_raw).*) raw; SELECT setval(age_name_to_seq('toys', 'v', 'v_user'), (SELECT max(_id) + 1 FROM v_user_raw)); INSERT INTO v_product_raw(product_id, price) VALUES ('AIEEEEEEE', '999'); INSERT INTO toys."v_product" SELECT (age_name_to_idx_start('toys', 'v', 'v_product') + _id)::text::graphid, row_to_json((select x FROM (select product_id, price) x))::text::agtype FROM v_product_raw WHERE product_id = 'AIEEEEEEE'; SELECT setval(age_name_to_seq('toys', 'v', 'v_product'), (SELECT max(_id) + 1 FROM v_product_raw));この方法を使用して新しいエッジを挿入します。元のエッジテーブルを削除し、エッジのインポート手順を再度実行する必要があります。
DELETE FROM e_own_raw; INSERT INTO e_own_raw VALUES ('A', 'U2', 'AIEEEEEEE', '9999'); UPDATE e_own_raw SET _fromid = v_user_idgen(user_type, user_uid); UPDATE e_own_raw SET _toid = v_product_raw._id FROM v_product_raw WHERE v_product_raw.product_id = e_own_raw.product_id; INSERT INTO toys."e_own" SELECT (age_name_to_idx_start('toys', 'e', 'e_own') + nextval(age_name_to_seq('toys', 'e', 'e_own')))::text::graphid, (age_name_to_idx_start('toys', 'v', 'v_user') + _fromid)::text::graphid, (age_name_to_idx_start('toys', 'v', 'v_product') + _toid)::text::graphid, row_to_json((select x FROM (select buy_price) x))::text::agtype FROM e_own_raw;
まとめ
テーブルを使用して、外部ノードとエッジをグラフデータベースにすばやくインポートできます。この方法のパフォーマンスは、MATCH ... CREATE ... メソッドの 2 倍以上です。
無料トライアル
PolarDB 無料トライアル ページにアクセスし、「PolarDB」を選択して、PolarDB for PostgreSQL が提供する GanosBase のグラフコンピューティング機能をお試しください。