全部產品
Search
文件中心

PolarDB:基於PolarDB的圖分析:通過表將資料快速匯入到圖

更新時間:Jul 02, 2025

本文介紹在使用PolarDB PostgreSQL版的AGE外掛程式時,針對巨量資料量快速匯入資料的方法。該方法可以快速將圖資料庫中億級以上的節點和邊快速匯入到資料庫中,避免在插入邊時進行查詢帶來的效能瓶頸。

背景

PolarDB PostgreSQL版支援AGE外掛程式進行圖資料的儲存管理和查詢。

在進行圖資料庫資料的匯入與匯出時,通常需要將節點資料和邊資料轉換為CSV等表格式。然後,匯入節點表,再通過邊表在節點上構建邊。但是在使用Cypher的MATCH...CREATE語句建立邊時,需要查詢對應的兩側節點,導致匯入速度降低。針對千萬級以上資料量的邊進行匯入時,其效能往往無法滿足需求。

此處介紹了一種新的圖資料匯入方法。該方法通過直接產生和管理圖上節點的唯一ID,省略MATCH步驟,直接寫入邊的記錄。可以將圖的匯入效率提升兩個數量級。這種匯入方法包含以下步驟:

  1. 將圖中的節點匯入為資料庫中的普通表。通常情況下,節點表上有一列(或多列的組合)作為其唯一性標識。

  2. 將圖中的邊匯入為資料庫中的普通表。邊表上先是左側節點的唯一性標識的列的值,然後是右側節點的唯一性標識的列的值,最後是邊自身的屬性。

  3. 在節點表上添加唯一識別欄位,在邊表上,通過JOIN或計算方式,添加起始點和終止點的唯一識別欄位。

  4. 通過唯一識別欄位將節點表匯入到圖。

  5. 通過唯一性標識將邊表匯入到圖。

前提條件

支援的PolarDB 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

    • v_user:通過兩列type和uid構成唯一標識,不同type下uid可能重複。

    • v_product:使用一列product_id作為唯一標識,但其是一列複雜字串,無法轉換為數值型id,需要人工建立標號。

  • 有一種類型的邊,名為e_own。

  • 點和邊的資料都已經匯入到了名為xxx_raw的表中。

  • 將點和邊都匯入到名為Toys的圖中。

匯入前結構

  1. 建立外掛程式。

    CREATE EXTENSION age;

    建立外掛程式後,將其加入需要使用此外掛程式的資料庫或使用者的搜尋路徑和預先載入庫中:

    說明

    使用Data Management(Data Management)用戶端設定search_path時,可能會存在相容性問題,您可使用PolarDB-Tools執行相關語句。

    ALTER DATABASE <dbname> SET search_path = "$user", public, ag_catalog;
    ALTER DATABASE <dbname> SET session_preload_libraries TO 'age';
  2. 建立圖,包括兩種節點類型和一種邊類型(v代表vertex即節點, e代表edge即邊)。

    ---建立圖
    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');

建立唯一標識

  • 對於v_user節點,此處假設僅存在10種類型(A至J),因此可以採用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;

    然後在涉及的節點表和邊表上添加識別欄位:

    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節點,由於其字串無法直接轉換為bigint類型的數值,因此採用PostgreSQL的bigserial類型自動產生編號,並通過串連操作將對應的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類型自動產生編號,則不要求原先的列可以轉換為數實值型別,但需要一次額外的JOIN操作。同時,後續無法使用簡單Function Compute出屬性到節點ID的映射,可能會無法進行特定的操作。

快速匯入

  1. 建立輔助函數。輔助產生PolarDB 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)
  • 查詢邊。

    尋找Alice購買各個產品時使用的折扣。

    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所產生唯一標識之間的對應關係。

  • 匯入完成後,命名為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 ...方式,通常實現效能兩倍以上提升。

試用體驗

您可以訪問PolarDB免費試用頁面,選擇試用“雲原生資料庫PolarDB PostgreSQL版”,體驗GanosBase的圖計算能力。