数据库 2026-07-25 4

PostgreSQL 19 重磅新特性:SQL/PGQ 图查询初揭秘

老张

资深系统架构师

封面-PostgreSQL 19 PGQ 图查询初揭秘

PostgreSQL 19 重磅新特性:SQL/PGQ 图查询初揭秘

PG19 引入了一个很有趣的东西:SQL/PGQ。不,PGQ 不是"PostgreSQL Query",它叫 Property Graph Query。

官方文档专门吐槽过:Property graph 在图数据库领域通常缩写为 PG,这对 PostgreSQL 用户来说确实容易串台。

简单说:PG19 开始,你可以在 PostgreSQL 内核里定义属性图,然后用图模式匹配语法去查询它。

这事有多有意思?以前我们在 PG 里做图相关的事情,无非几条路:

  • 用普通表建点表、边表,手写一堆 JOIN
  • 用递归 CTE 做层级、路径、上下游追踪
  • 用 Apache AGE 扩展跑 openCypher
  • 把数据同步到 Neo4j、NebulaGraph 等专用图数据库

PG19 的 PGQ 给了另一种选择:数据仍然在关系表里,但我们可以把这些表映射成图视图,然后用图查询处理。

它不是要取代 SQL,而是让"关系网络"这类问题表达得更自然。

一、关系数据库为什么需要图查询?

先别急着学语法,想一个问题:表的世界里缺什么?

关系数据库最擅长存行列表——订单、用户、商品,一张张整整齐齐。但现实世界里的很多问题,天然不是表格状的,而是网状的:

  • 金融风控:A 转给 B,B 转给 C,C 转回 A——这算不算资金闭环?
  • 社交网络:张三的朋友的朋友,哪些也是李四的同事?
  • 供应链:某零件故障,会影响哪些上游供应商和下游订单?
  • 权限系统:一个用户通过角色、组织、项目组,继承到了哪些权限?
  • 知识图谱:一个实体通过若干关系,连接到哪些概念、文档、事件?

这些场景用 SQL 当然能写。图在关系模型里完全可以表示——点是一张表,边也是一张表,点表有主键,边表通过外键指向两个点。

但问题在于:表结构并不复杂,复杂的是查询表达。

举个例子,经典的社交关系:

CREATE TABLE person (
    id   int PRIMARY KEY,
    name text NOT NULL
);

CREATE TABLE knows (
    person1id int REFERENCES person(id),
    person2id int REFERENCES person(id)
);

INSERT INTO person VALUES
  (1, 'Alice'), (2, 'Bob'), (3, 'Cecile'),
  (4, 'Diane'), (5, 'Emily'), (6, 'Fleur');

INSERT INTO knows VALUES
  (1,2), (1,3), (1,4), (2,3),
  (3,4), (3,5), (4,5), (4,6), (5,6);

表很简单:person 是点,knows 是边。

查 Bob 认识谁,一个 JOIN 搞定。查 Bob 的朋友的朋友,多 JOIN 一次也还行。

但假如问:Bob 到 Fleur 最短隔了几层? 这就不是固定跳数能解决的问题了。你得写递归 CTE,大概这样:

WITH RECURSIVE path AS (
  -- 起点:Bob
  SELECT person1id AS src, person2id AS dst, 1 AS depth,
         ARRAY[person1id, person2id] AS route
  FROM knows WHERE person1id = 2
  UNION ALL
  -- 递归:向外扩展一跳
  SELECT p.src, k.person2id, p.depth + 1, p.route || k.person2id
  FROM path p
  JOIN knows k ON k.person1id = p.dst
  WHERE NOT k.person2id = ANY(p.route)
    AND p.dst <> 6
)
SELECT MIN(depth) FROM path WHERE dst = 6;

看得难受不?关系模型关心表和表如何关联,图模型关心点和点如何连接。 这是两个思维范式。

PGQ 要解决的,就是这种"表达上的拧巴"。

二、到底什么是图?

先捋概念。图由两类元素组成:

  • Vertex(顶点)— 也叫 node,表示实体
  • Edge(边)— 也叫 relationship,表示实体之间的关系

在属性图(Property Graph)里,顶点和边上还可以带:

  • Label(标签)— 元素类型分类,类似表名
  • Property(属性)— 具体字段,类似列

比如,Alice 认识 Bob 这件事,在关系数据库里是:

CREATE TABLE knows (
    id        int,
    person1_id int,
    person2_id int,
    since     date
);

在属性图里,天然就是一条边:

(:Person {name:'Alice'})-[:knows {since:'2018-10-03'}]->(:Person {name:'Bob'})

节点标签 Person、边标签 knows、边属性 since——关系本身也是信息载体,而不是隐藏在 JOIN 条件里的外键。

标签的作用类似类型名。比如:

  • (:Person)-[:knows]->(:Person) — 人与人认识
  • (:Person)-[:is_member]->(:Group) — 人与组织的关系
  • (:Product)-[:belongs_to]->(:Category) — 商品与分类的关系

查询时可以指定:只查 Person 节点,或者只查 is_member 关系。

三、PGQ 不是原生图数据库

很多人一听图查询,条件反射就是 Index-Free Adjacency——数据天然按点和边组织,从一个点找相邻点不需要全局索引,通过边直接走过去。

PG19 的 PGQ 不是这个路子。官方说得很清楚:

PG19 的 property graph 是定义在关系表之上的一种只读图视图

数据没有变成原生图结构。CREATE PROPERTY GRAPH 不会把数据复制成物化图,它只是给关系表"披了一层图的皮"。

架构是这样的:

关系表(普通表、视图、外部表)
     ↓
CREATE PROPERTY GRAPH 定义图映射
     ↓
GRAPH_TABLE 图模式匹配查询
     ↓
PostgreSQL 查询规划器和执行器(还是那套)

所以 PGQ 的定位很清晰:

  • 数据本来就在 PostgreSQL 关系表里
  • 业务主要还是 SQL,但某些查询天然是图模式
  • 不想为了几类关系查询单独维护一套图数据库
  • 想把图查询结果继续和普通表 JOIN、过滤、聚合
  • 想跟 SQL 标准靠拢,而不是引入一套独立的查询语言

别神化,也别小瞧。

四、PG19 的 PGQ 长什么样?

两个核心东西:

  • CREATE PROPERTY GRAPH — 定义一个属性图
  • GRAPH_TABLE — 在 SQL 查询里执行图模式匹配,返回一张关系表

官方文档有一句很重要:GRAPH_TABLE 对外表现得像一个 table function——它产生一个表,然后你可以继续 JOIN、WHERE、ORDER BY。

实操:银行转账风控场景

假设两张表:

CREATE TABLE bank_accounts (
    id         bigint PRIMARY KEY,
    name       text NOT NULL,
    risk_level text NOT NULL DEFAULT 'normal',
    balance    numeric(18,2) NOT NULL
);

CREATE TABLE bank_transfers (
    txn_id      bigint PRIMARY KEY,
    src_acct_id bigint NOT NULL REFERENCES bank_accounts(id),
    dst_acct_id bigint NOT NULL REFERENCES bank_accounts(id),
    amount      numeric(18,2) NOT NULL,
    created_at  timestamptz NOT NULL DEFAULT now()
);

INSERT INTO bank_accounts VALUES
  (1001, 'Alice', 'normal', 10000),
  (1002, 'Bob',   'normal',  8000),
  (1003, 'Carol', 'watch',   5000),
  (1004, 'Dave',  'normal',  3000);

INSERT INTO bank_transfers VALUES
  (1, 1001, 1002, 1000, '2026-06-01 10:00+08'),
  (2, 1002, 1003,  900, '2026-06-01 10:05+08'),
  (3, 1003, 1001,  800, '2026-06-01 10:10+08'),
  (4, 1001, 1004,  200, '2026-06-02 11:00+08');

把它定义成属性图:

CREATE PROPERTY GRAPH bank_graph
  VERTEX TABLES (
    bank_accounts
      KEY (id)
      LABEL account
      PROPERTIES (id, name, risk_level, balance)
  )
  EDGE TABLES (
    bank_transfers
      KEY (txn_id)
      SOURCE KEY (src_acct_id) REFERENCES bank_accounts(id)
      DESTINATION KEY (dst_acct_id) REFERENCES bank_accounts(id)
      LABEL transfer
      PROPERTIES (txn_id, amount, created_at)
  );

这段 DDL 做了几件事:

  • bank_accounts → 顶点,label = account
  • bank_transfers → 边,label = transfer
  • SOURCE 指向转出账户,DESTINATION 指向转入账户
  • PROPERTIES 暴露哪些列作为图属性

查一跳关系:Alice 转给了谁?

SELECT *
FROM GRAPH_TABLE (
  bank_graph
  MATCH (a IS account WHERE a.name = 'Alice')-[t IS transfer]->(b IS account)
  COLUMNS (
    a.id AS src_id,
    a.name AS src_name,
    b.id AS dst_id,
    b.name AS dst_name,
    t.amount AS amount,
    t.created_at AS created_at
  )
) AS gt;

结果:

 src_id | src_name | dst_id | dst_name | amount  |       created_at
--------+----------+--------+----------+---------+------------------------
   1001 | Alice    |   1002 | Bob      | 1000.00 | 2026-06-01 10:00:00+08
   1001 | Alice    |   1004 | Dave     |  200.00 | 2026-06-02 11:00:00+08

语法很直观:(Alice)-[transfer]->(谁),就像在画图。

传统 SQL 写这个当然也行——三个表 JOIN 而已。但接下来就有意思了。

查两跳路径:资金流向追踪

查 Alice 转给了谁,这个人又转给了谁:

SELECT *
FROM GRAPH_TABLE (
  bank_graph
  MATCH
      (a IS account WHERE a.name = 'Alice')
        -[t1 IS transfer]->
      (m IS account)
        -[t2 IS transfer]->
      (b IS account)
  COLUMNS (
    a.name AS src_name,
    m.name AS middle_name,
    b.name AS dst_name,
    t1.amount AS first_amount,
    t2.amount AS second_amount
  )
) AS gt;

结果很清晰,Alice → Bob → Carol,1000 → 900。

用传统 SQL 写就是 5 个表 JOIN 在一起:

SELECT a.name AS src_name,
       m.name AS middle_name,
       b.name AS dst_name,
       t1.amount AS first_amount,
       t2.amount AS second_amount
FROM bank_accounts a
JOIN bank_transfers t1 ON t1.src_acct_id = a.id
JOIN bank_accounts m ON m.id = t1.dst_acct_id
JOIN bank_transfers t2 ON t2.src_acct_id = m.id
JOIN bank_accounts b ON b.id = t2.dst_acct_id
WHERE a.name = 'Alice';

结果一样,但可读性差了太多。

路径越长,差异越明显。三跳、四跳的时候,PGQ 的语法仍然是直线式的路径描述,而 SQL 的 JOIN 链会越来越长、越来越难调。

资金闭环检测

这是金融风控最典型的场景。Alice 转给 Bob → Bob 转给 Carol → Carol 转回 Alice,检测循环:

SELECT *
FROM GRAPH_TABLE (
  bank_graph
  MATCH
      (a IS account WHERE a.name = 'Alice')
        -[t1 IS transfer]->
      (b IS account)
        -[t2 IS transfer]->
      (c IS account)
        -[t3 IS transfer]->
      (a IS account)     -- 回到起点!
  COLUMNS (
    a.name AS start_name,
    b.name AS hop1,
    c.name AS hop2,
    t1.amount + t2.amount + t3.amount AS total_flow
  )
) AS gt;

这个查询如果用 SQL 写,你得 JOIN 三张转账表、四张账户表,然后条件里还要加上 t3.dst_acct_id = a.id

而在 PGQ 里,就是很自然地把路径走通就行。

五、PGQ 的几个关键要点

结合官方文档,几个值得注意的点:

1. Property Graph 是只读图视图

CREATE PROPERTY GRAPH 只是定义图结构,不会物化数据。数据仍然在底层关系表里。插入、更新、删除数据,仍然操作原始表。

2. 边是有方向的

PGQ 里 edge 有 source 和 destination,三种匹配方式:

写法 含义
()-[ ]->() 按边的正向匹配
()<-[ ]-() 按边的反向匹配
()-[ ]-() 两个方向都可以匹配

不带方向的模式可以理解为匹配任意方向。

3. Label 不是表名

默认情况下表名会暴露成 label,但你可以自己指定。比如 bank_accounts LABEL account,查询时写 (a IS account) 而不是 (a IS bank_accounts)

4. Property 类似列

PROPERTIES (id, name, balance) 把这些列暴露成图属性,查询时可以写 a.namea.balance

5. 同名 Label 有一致性要求

如果多个元素表使用同一个 label,这些 label 暴露的属性数量、名称、类型必须一致。否则同一 label 查出来字段一会 text、一会 int,没法处理。

6. 权限仍然看底层表

GRAPH_TABLE 访问底层关系对象时,权限按执行查询的用户判断,而不是只看 property graph owner。别以为给了 graph 权限就能绕过底层表权限——这点很 PG。

六、PGQ vs 递归 CTE:谁取代谁?

我的看法:不会取代,各有各的战场。

递归 CTE 仍然很有用:

  • 组织树、分类树、简单父子层级
  • BOM(物料清单)展开
  • 这些场景递归 CTE 很成熟,也很可控

PGQ 更适合表达模式匹配:

  • 点和边类型比较多
  • 关系方向比较重要
  • 查询更像找路径而不是展开树
  • 需要把多个实体关系串起来
  • 希望查询语法更贴近业务关系图

七、性能:别被"图"字迷惑

这是 DBA 最关心的问题。

PGQ 写起来像图,但底层还是 PostgreSQL 的关系存储、查询规划、执行器。性能上没有魔法。

实际写 PGQ 时这几条要记住:

第一,边表索引非常重要。

拿银行转账的例子,至少要考虑:

CREATE INDEX ON bank_transfers(src_acct_id);
CREATE INDEX ON bank_transfers(dst_acct_id);
CREATE INDEX ON bank_transfers(src_acct_id, dst_acct_id);

如果常按金额过滤,也要加金额索引。

第二,路径越长,中间结果越容易爆。

图查询很容易出现组合爆炸。二跳还好,三跳开始要小心,四跳五跳再加不限制条件——数据库可能 OOM。这点和递归 CTE 是一个道理。

第三,过滤条件尽量前推。

不要先匹配全图再过滤账户名、日期、金额。在 element pattern 里写条件:

MATCH
  (a IS account WHERE a.name = 'Alice')
    -[t IS transfer WHERE t.amount > 1000]->
  (b IS account)

第四,EXPLAIN 仍然是好朋友。

PGQ 最后仍然要落到 PostgreSQL 执行计划上。别因为语法长得像图就忘了看计划。

第五,别拿 PGQ 硬干所有图算法。

PGQ 能解决一部分模式匹配问题,但不是专用的图计算引擎。PageRank、社区发现、全图最短路径这些,不是它的战场。

八、展望:PGQ 在 AI 时代的价值

最后聊一个更大的视角。

AI 时代,企业需要的数据库能力正在扩展:

  • 向量检索(pgvector)— 语义搜索、RAG
  • 全文检索(内置 + 扩展)— 关键词搜索
  • 图查询(SQL/PGQ)— 实体关系、证据链、权限路径

这三个能力在 PG 生态里逐步补齐了。

SQL/PGQ 的价值尤其在于:它把关系查询放进 SQL 标准体系。你不是在 SQL 和 openCypher 两套语言之间来回切,而是在同一个 SELECT 语句里,把关系表 JOIN 和图模式匹配混合使用。这对于数据血缘分析、权限继承路径、供应链追踪这类需要"表+图"混合的现代业务场景,非常关键。

PG19 的 PGQ 可能不会一夜之间让 PostgreSQL 变成"图数据库",但它打开了一扇门——以后在 PG 里,关系型查询和图模式查询不再是二选一,而是一起用。

小结

PG19 的 SQL/PGQ,意味着 PostgreSQL 开始原生支持把关系数据映射成属性图,并用 SQL 标准的方式进行图模式匹配。

它不是原生图数据库,但它让"在关系数据库里做图查询"这件事的成本降到了最低——不需要额外部署、不需要同步数据、不需要学一门新语言。

对于已经在用 PostgreSQL 的团队来说,这是一个低成本的能力升维。下次遇到"查路径""找闭环""追踪关系链"这类需求,不用再挠着头写 50 行 JOIN 了——试试 PGQ。


本文由老张整理编写。参考了 PostgreSQL 19 官方文档及 PostgreSQL 学徒的 PGQ 文章。示例代码基于 PG19 语法,如有差异以官方文档为准。

延伸阅读:

分享:
返回文章列表