すべてのプロダクト
Search
ドキュメントセンター

PolarDB:PolarDB ベースのグラフ分析: テーブルからグラフへの高速データインポート

最終更新日:Jun 06, 2025

このトピックでは、PolarDB for PostgreSQL が提供する AGE 拡張機能を使用して、大量のデータを迅速にインポートする方法について説明します。このアプローチにより、数億個のノードとエッジをグラフデータベースに効率的にインポートできます。これにより、エッジ挿入時のクエリパフォーマンスの問題を回避できます。

背景情報

PolarDB for PostgreSQL は、グラフデータの保存とクエリを容易にするために AGE 拡張機能 を提供しています。

グラフデータベースデータをインポートおよびエクスポートするには、ノードデータとエッジデータを CSV などの形式のテーブルに変換し、ノードテーブルをインポートしてから、エッジテーブルに基づいてエッジを作成する必要があります。ただし、Cypher では、エッジの作成に使用する MATCH...CREATE 文は、対応するノードもクエリします。これにより、特に数千万個以上のエッジをインポートする場合、インポート速度が低下します。

このトピックでは、グラフデータをインポートするための新しい方法について説明します。この方法では、グラフ上のノードの一意の ID を直接生成および管理するため、MATCH ステップを実行せずにエッジを挿入できます。グラフのインポート効率を大幅に向上させることができます。この方法には、次の手順が含まれます。

  1. グラフノードを共通テーブルにインポートします。テーブルの 1 つの列または一連の列が一意の ID として使用されます。

  2. グラフエッジを共通テーブルにインポートします。エッジレコードには、左ノードの一意の ID、右ノードの一意の ID、およびエッジプロパティが含まれています。

  3. ノードテーブルに一意の ID 列を追加します。エッジテーブルでは、JOIN または計算を使用して、開始ノードと終了ノードの一意の ID 列を追加します。

  4. 一意の ID 列に基づいて、ノードテーブルをグラフにインポートします。

  5. 一意の 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_userv_product という名前の 2 つのノードタイプ。

    • v_user: タイプが異なるノードの uid が同じになる場合があるため、一意の ID は type 列と uid 列の両方から作成されます。

    • v_product: product_id 列が一意の ID として使用されます。この列の値は、数値 ID に変換できない複雑な文字列です。シーケンス番号は手動で追加する必要があります。

  • e_own という名前のエッジタイプ。

  • ノードデータとエッジデータがインポートされるテーブル (xxx_raw という名前)。

  • ノードとエッジがインポートされる Toys という名前のグラフ。

データインポートの準備

  1. 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. 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');
  3. インポートするノードデータを格納するノードテーブルを作成します。

    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');
  4. インポートするエッジデータを格納するエッジテーブルを作成します。

    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 は、uidtype の組み合わせです。このマッピング関係を関数として登録します。

    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 をマッピングして特定の操作を実行することはできません。

高速データインポート

  1. 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;
  2. ノードをインポートします。

    1. (オプション) ノードをインポートする前に、一意の制約を作成して重複 ID を防ぐことができます。これらのインデックスは、後続のクエリを高速化できます。作成後も保持できます。

      CREATE UNIQUE INDEX ON toys.v_user(id);
      CREATE UNIQUE INDEX ON toys.v_product(id);
    2. ノードをインポートする際は、_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));
  3. エッジをインポートします。エッジは、_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 のグラフコンピューティング機能をお試しください。