
PG19 引入了一个很有趣的东西:SQL/PGQ。不,PGQ 不是"PostgreSQL Query",它叫 Property Graph Query。
官方文档专门吐槽过:Property graph 在图数据库领域通常缩写为 PG,这对 PostgreSQL 用户来说确实容易串台。
简单说:PG19 开始,你可以在 PostgreSQL 内核里定义属性图,然后用图模式匹配语法去查询它。
这事有多有意思?以前我们在 PG 里做图相关的事情,无非几条路:
PG19 的 PGQ 给了另一种选择:数据仍然在关系表里,但我们可以把这些表映射成图视图,然后用图查询处理。
它不是要取代 SQL,而是让"关系网络"这类问题表达得更自然。
先别急着学语法,想一个问题:表的世界里缺什么?
关系数据库最擅长存行列表——订单、用户、商品,一张张整整齐齐。但现实世界里的很多问题,天然不是表格状的,而是网状的:
这些场景用 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 要解决的,就是这种"表达上的拧巴"。
先捋概念。图由两类元素组成:
在属性图(Property Graph)里,顶点和边上还可以带:
比如,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 关系。
很多人一听图查询,条件反射就是 Index-Free Adjacency——数据天然按点和边组织,从一个点找相邻点不需要全局索引,通过边直接走过去。
但 PG19 的 PGQ 不是这个路子。官方说得很清楚:
PG19 的 property graph 是定义在关系表之上的一种只读图视图。
数据没有变成原生图结构。CREATE PROPERTY GRAPH 不会把数据复制成物化图,它只是给关系表"披了一层图的皮"。
架构是这样的:
关系表(普通表、视图、外部表)
↓
CREATE PROPERTY GRAPH 定义图映射
↓
GRAPH_TABLE 图模式匹配查询
↓
PostgreSQL 查询规划器和执行器(还是那套)
所以 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 = accountbank_transfers → 边,label = transferSELECT *
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 里,就是很自然地把路径走通就行。
结合官方文档,几个值得注意的点:
CREATE PROPERTY GRAPH 只是定义图结构,不会物化数据。数据仍然在底层关系表里。插入、更新、删除数据,仍然操作原始表。
PGQ 里 edge 有 source 和 destination,三种匹配方式:
| 写法 | 含义 |
|---|---|
()-[ ]->() |
按边的正向匹配 |
()<-[ ]-() |
按边的反向匹配 |
()-[ ]-() |
两个方向都可以匹配 |
不带方向的模式可以理解为匹配任意方向。
默认情况下表名会暴露成 label,但你可以自己指定。比如 bank_accounts LABEL account,查询时写 (a IS account) 而不是 (a IS bank_accounts)。
PROPERTIES (id, name, balance) 把这些列暴露成图属性,查询时可以写 a.name、a.balance。
如果多个元素表使用同一个 label,这些 label 暴露的属性数量、名称、类型必须一致。否则同一 label 查出来字段一会 text、一会 int,没法处理。
GRAPH_TABLE 访问底层关系对象时,权限按执行查询的用户判断,而不是只看 property graph owner。别以为给了 graph 权限就能绕过底层表权限——这点很 PG。
我的看法:不会取代,各有各的战场。
递归 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、社区发现、全图最短路径这些,不是它的战场。
最后聊一个更大的视角。
AI 时代,企业需要的数据库能力正在扩展:
这三个能力在 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 语法,如有差异以官方文档为准。
延伸阅读: