最佳实践

更新时间:
复制为 MD 格式

在处理包含复杂关系的海量数据时,常规的数据库查询往往性能不佳且实现复杂。为了解决这一痛点,PolarDB PostgreSQL提供了polar_age插件,它兼容Cypher查询语言,让您能以图的方式高效地存储、查询和分析关联数据。本文档旨在提供一系列最佳实践,帮助您通过精细的索引策略、数据唯一性设计和高效的数据写入模式,最大限度地发挥polar_age的性能优势,确保图数据处理的稳定与高效。

准备工作

  1. 安装与配置插件:安装polar_age插件,并设置搜索路径。

    -- 创建 polar_age 扩展
    CREATE EXTENSION polar_age;
    -- 在当前会话中设置搜索路径
    SET search_path = "$user", public, ag_catalog, pg_catalog;
  2. 创建一个名为graph_customer的图与Customer节点。

    -- 创建图
    SELECT ag_catalog.create_graph('graph_customer');
    
    -- 创建点与边标签
    SELECT ag_catalog.create_vlabel('graph_customer', 'Customer');
    SELECT ag_catalog.create_elabel('graph_customer', 'Like');
  3. 插入数据:

    INSERT INTO "graph_customer"."Customer" (id, properties)
    VALUES 
        -- _make_graph_id(图名称, 标签名称, 节点业务唯一键)
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "age": 30}'::ag_catalog.agtype),
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Bob'::cstring), '{"name":"Bob", "age": 40}'::ag_catalog.agtype),
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Cook'::cstring), '{"name":"Cook", "age": 28}'::ag_catalog.agtype),
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Duke'::cstring), '{"name":"Duke", "age": 38}'::ag_catalog.agtype);
        
    INSERT INTO "graph_customer"."Like" (id, start_id, end_id, properties)
    VALUES 
    (        
        ag_catalog._next_graph_id('graph_customer'::name, 'Like'::name), 
        ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring),
        ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Bob'::cstring),
        '{"from" : "Alice", "to" : "Bob"}'::ag_catalog.agtype
    ),
    (
        ag_catalog._next_graph_id('graph_customer'::name, 'Like'::name), 
        ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Duke'::cstring),
        ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Cook'::cstring),
         '{"from" : "Duke", "to" : "Cook"}'::ag_catalog.agtype
     );

优化查询性能:选择并创建索引

默认情况下,polar_age仅为点和边的id字段自动创建索引,不会为属性创建任何索引。为了提升查询性能,您需根据查询模式显式创建属性索引。

选型对比

polar_age在底层将图数据(点和边)存储为PostgreSQL的表,其中点的属性(properties)存储在agtype类型的列中。理解Cypher查询如何转换为对这些表的操作,是进行性能优化的关键。Cypher查询主要有两种风格,它们对应不同的底层执行方式,从而影响索引的选择:

查询风格

Cypher示例

适用索引

属性图模式(不带WHERE子句)

MATCH (:Label {property: value})

GIN索引

WHERE子句模式

MATCH (o:Label)-[ ]->() WHERE o.property = value

BTREE索引

  • 当您需要对单个、高选择性的属性进行精确查找时,推荐使用WHERE子句模式并创建BTREE索引,性能最优。

  • 当您的查询涉及多个属性组合,或需要更灵活的模式匹配时,推荐使用属性图模式(不带WHERE子句)并创建GIN索引

步骤一:使用EXPLAIN诊断查询性能

在创建索引前,您可以使用EXPLAIN命令分析查询计划,以确认是否存在性能瓶颈。通常,无索引的查询会显示为全表扫描(Seq Scan),这是性能低下的标志。

示例

  • 分析一个未使用索引的WHERE子句查询。

    -- 查询未使用索引时的执行计划
    EXPLAIN
    SELECT * FROM cypher('graph_customer', $$
        MATCH (n:Customer)
        WHERE n.name = 'Alice'
        RETURN n
    $$) AS (plan text);

    返回结果如下:

                                                  QUERY PLAN                                              
    ------------------------------------------------------------------------------------------------------
     Seq Scan on "Customer" n  (cost=0.00..25.64 rows=5 width=32)
       Filter: (agtype_access_operator(VARIADIC ARRAY[properties, '"name"'::agtype]) = '"Alice"'::agtype)
  • 分析一个未使用索引,且不带WHERE子句查询。

    -- 查询未使用索引时的执行计划
    EXPLAIN
    SELECT * FROM cypher('graph_customer', 
    $$
        MATCH (n:Customer {name:'Aice'}) 
        RETURN n
    $$) 
    AS (plan text);

    返回结果如下:

                             QUERY PLAN                          
    -------------------------------------------------------------
     Seq Scan on "Customer" n  (cost=0.00..1.68 rows=1 width=32)
       Filter: (properties @> '{"name": "Aice"}'::agtype)

步骤二:根据查询模式创建索引

根据您的查询风格,选择创建BTREE索引或GIN索引。

  • 优化WHERE子句查询:为Customer标签的Name属性创建一个BTREE索引,以加速WHERE n.Name = '...'形式的查询。

    -- 为 Customer 标签的 Name 属性创建BTREE索引
    SELECT age_create_prop_index('graph_customer', 'Customer', 'name');
  • 优化属性图模式(不带WHERE子句)查询:为Customer标签的所有属性创建一个GIN索引,以加速 MATCH (n:Customer {prop1:'val1', ...})形式的查询。

    SELECT age_create_gin_index('graph_customer');

保障数据唯一性

在图数据库中,确保点、边或其属性的唯一性是保障数据一致性的关键。polar_age本身不提供声明式的UNIQUE约束,但您可以通过确定性ID生成和唯一索引两种模式来实现。

方案一:使用确定性ID保证点和边的唯一性

此方案适用于您希望通过业务主键(如邮箱、订单号)来唯一标识点或边,并实现幂等写入(重复执行相同操作结果一致)。

  • 点的唯一性:使用邮箱作为唯一标识,确保每个邮箱只对应一个Customer点。在插入数据时,使用_make_graph_id函数,并将业务唯一键(如'user@example.com')作为第三个参数。

    INSERT INTO "graph_customer"."Customer" (id, properties)
    VALUES (
      ag_catalog._make_graph_id('graph_customer', 'Customer', 'user@example.com'::cstring),'{"name":"user", "email": "user@example.com"}'::ag_catalog.agtype
    );
  • 边的唯一性:确保两个特定点之间同类型的边只有一条(例如,一个用户只能关注另一个用户一次)。创建(start_id, end_id)的唯一索引。这是保证边唯一性的最佳实践。

    -- 步骤1: 在边的 "Like" 标签表上创建唯一索引
    CREATE UNIQUE INDEX ON "graph_customer"."Like" USING BTREE(start_id, end_id);
    
    -- 步骤2: 插入边数据时,即使重复执行,也只会插入一次
    INSERT INTO "graph_customer"."Like" (id, start_id, end_id, properties)
    VALUES 
    (        
        ag_catalog._next_graph_id('graph_customer'::name, 'Like'::name),                         -- 边的ID可以使用自增ID
        ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring),   -- 起点ID
        ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Bob'::cstring),     -- 终点ID
        '{"from" : "Alice", "to" : "Bob"}'::ag_catalog.agtype
    )ON CONFLICT(start_id, end_id) DO NOTHING; -- 如果冲突(边已存在),则不执行任何操作

方案二:使用唯一索引保证属性的唯一性

此方案适用于您需要确保某个特定属性的值在所有点或边中是唯一的(例如,手机号、身份证号)。

示例

确保Customer标签下所有点的name属性值唯一。在properties列的name键上创建一个唯一BTREE索引。

CREATE UNIQUE INDEX customer_name_unique_idx
ON "graph_customer"."Customer"
USING btree(agtype_access_operator(VARIADIC ARRAY[properties, '"name"'::agtype]));

高效写入与更新数据 (UPSERT)

UPSERT(Update or Insert)是一种原子操作,如果数据存在则更新,不存在则插入。polar_age借助PostgreSQLON CONFLICT语法,可以对图数据实现高效、原子的UPSERT操作。更多信息,请参见写入数据(INSERT)

点的UPSERT

如果ID对应的Customer点已存在,则更新其属性;如果不存在,则插入新点。

INSERT INTO "graph_customer"."Customer" (id, properties)
VALUES 
    (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "age": 31}'::ag_catalog.agtype)
ON CONFLICT(id) DO NOTHING;

边的UPSERT

如果(start_id, end_id)对应的Like边已存在,则更新其属性;如果不存在,则插入新边。

-- 确保唯一索引已创建
-- CREATE UNIQUE INDEX ON "graph_customer"."Like" USING BTREE(start_id, end_id);

INSERT INTO "graph_customer"."Like" (id, start_id, end_id, properties)
VALUES 
(        
    ag_catalog._next_graph_id('graph_customer'::name, 'Like'::name), 
    ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring),
    ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Bob'::cstring),
    '{"from" : "Alice", "to" : "Bob"}'::ag_catalog.agtype
)ON CONFLICT(start_id, end_id) DO NOTHING;

原子化属性更新

ON CONFLICT ... DO UPDATE子句中,您可以对properties字段执行多种原子化更新操作。

  • 覆盖属性:使用EXCLUDED.properties可以用新插入的属性完全替换旧属性。

    ON CONFLICT(id) DO UPDATE
    SET properties = EXCLUDED.properties, timeline = 'now';

    示例:将Aliceage30更新为31。

    INSERT INTO "graph_customer"."Customer" AS v (id, properties)
    VALUES 
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "age": 31}'::ag_catalog.agtype)
    ON CONFLICT(id) DO UPDATE
    SET properties = EXCLUDED.properties, timeline = 'now';
    
    -- Alice的数据
             id      |            properties        |           timeline            
    -----------------+------------------------------+-------------------------------
     3_1171544447xxx | {"age": 31, "name": "Alice"} | 2026-02-03 03:06:33.419693+00
  • 合并属性:使用agtype_concat函数,可以将新旧属性合并。如果存在相同的键,新值会覆盖旧值。

    ON CONFLICT(id) DO UPDATE
    SET properties = ag_catalog.agtype_concat(v.properties, EXCLUDED.properties),timeline = 'now';

    示例:在Alice中新增一个属性email

    INSERT INTO "graph_customer"."Customer" AS v (id, properties)
    VALUES 
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "email": "Alice@example.com"}'::ag_catalog.agtype)
    ON CONFLICT(id) DO UPDATE
    SET properties = ag_catalog.agtype_concat(v.properties, EXCLUDED.properties),timeline = 'now';
    
    -- Alice的数据
             id      |                         properties                         |           timeline            
    -----------------+------------------------------------------------------------+-------------------------------
     3_1171544447xxx | {"age": 31, "name": "Alice", "email": "Alice@example.com"} | 2026-02-03 03:06:33.419693+00
  • 累计计算:使用agtype_increase函数对数值类型的属性进行原子累加。如果属性不存在,则以累加值作为初始值。

    ON CONFLICT(id) DO UPDATE
    SET properties = ag_catalog.agtype_increase(v.properties, ag_catalog.agtype_build_map('age', 1)),timeline = 'now';

    示例:将Aliceage累加。

    INSERT INTO "graph_customer"."Customer" AS v (id, properties)
    VALUES 
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "age": 31, "email": "Alice@example.com"}'::ag_catalog.agtype)
    ON CONFLICT(id) DO UPDATE
    SET properties = ag_catalog.agtype_increase(v.properties, ag_catalog.agtype_build_map('age', 1)),timeline = 'now';
    
    -- Alice的数据
             id      |                         properties                         |           timeline            
    -----------------+------------------------------------------------------------+-------------------------------
     3_1171544447xxx | {"age": 32, "name": "Alice", "email": "Alice@example.com"} | 2026-02-03 03:06:33.419693+00
  • 属性清零:使用agtype_concat函数,可以将属性更新为0。

    ON CONFLICT(id) DO UPDATE
    SET properties = ag_catalog.agtype_concat(v.properties, ag_catalog.agtype_build_map('age', 0)),timeline = 'now';

    示例:将Aliceage清零。

    INSERT INTO "graph_customer"."Customer" AS v (id, properties)
    VALUES 
        (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "age": 32, "email": "Alice@example.com"}'::ag_catalog.agtype)
    ON CONFLICT(id) DO UPDATE
    SET properties = ag_catalog.agtype_concat(v.properties, ag_catalog.agtype_build_map('age', 0)),timeline = 'now';
    
    -- Alice的数据
             id      |                         properties                        |           timeline            
    -----------------+-----------------------------------------------------------+-------------------------------
     3_1171544447xxx | {"age": 0, "name": "Alice", "email": "Alice@example.com"} | 2026-02-03 03:06:33.419693+00
  • 嵌套操作:您可以组合使用这些函数,实现复杂的原子更新逻辑。

    • 新增一个salary属性,为1000

      INSERT INTO "graph_customer"."Customer" AS v (id, properties)
      VALUES 
          (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "salary": "1000"}'::ag_catalog.agtype)
      ON CONFLICT(id) DO UPDATE
      SET properties = ag_catalog.agtype_concat(v.properties, EXCLUDED.properties),timeline = 'now';
      
      -- Alice的数据
               id      |                                  properties                                 |           timeline            
      -----------------+-----------------------------------------------------------------------------+-------------------------------
       3_1171544447xxx | {"age": 0, "name": "Alice", "email": "Alice@example.com", "salary": "1000"} | 2026-02-03 03:06:33.419693+00
    • 累计和清零嵌套使用:将age累加,salary清零。

      INSERT INTO "graph_customer"."Customer" AS v (id, properties)
      VALUES 
          (ag_catalog._make_graph_id('graph_customer'::name, 'Customer'::name, 'Alice'::cstring), '{"name":"Alice", "age": 0, "email": "Alice@example.com", "salary": "1000"}'::ag_catalog.agtype)
      ON CONFLICT(id) DO UPDATE
      SET properties = ag_catalog.agtype_concat(
        ag_catalog.agtype_increase(v.properties, ag_catalog.agtype_build_map('age', 1)),
        ag_catalog.agtype_build_map('salary', 0)),
        timeline = 'now';
        
      -- Alice的数据
               id      |                                  properties                            |           timeline            
      -----------------+------------------------------------------------------------------------+-------------------------------
       3_1171544447xxx | {"age": 1, "name": "Alice", "email": "Alice@example.com", "salary": 0} | 2026-02-03 03:06:33.419693+00

应用于生产环境

  • 索引维护:对于大规模数据导入或删除后,建议对图相关的表执行ANALYZE命令,以更新统计信息,保证查询优化器的效率。

  • 批量数据导入:在导入大量数据时,推荐采用先删除相关索引,完成数据导入后,再统一重建索引的策略。这通常比带着索引逐条插入数据要快得多。