SQL/PGQ 图查询
PostgreSQL 19 属性图查询 —— CREATE PROPERTY GRAPH、GRAPH_TABLE 写法、索引规则与 WITH RECURSIVE 边界
PostgreSQL 19 实现了 SQL/PGQ(ISO/IEC 9075-16,SQL:2023 第 16 部分):在普通关系表上直接做图模式匹配。属性图本质是现有表之上的只读视图——不装扩展、不复制数据——GRAPH_TABLE 查询走的也是普通 JOIN 所用的同一套优化器。
仅限 PostgreSQL 19
SQL/PGQ 是 PostgreSQL 19 的新特性,PostgreSQL 18 及更早版本没有 CREATE PROPERTY GRAPH 和 GRAPH_TABLE。版本状态见 PostgreSQL 19 发布与升级指南;本文语法以官方文档为准(5.15 Property Graphs、7.9 Graph Queries)。
什么场景值得用图查询
- 社交关系:朋友的朋友、共同好友。
- 数据血缘:报表里的数字由哪些源数据、经过哪些转换算出来。
- 风控与审计:资金流向、可疑路径、合规追溯。
取舍在于深度固定。PostgreSQL 19 的 SQL/PGQ 按跳逐段匹配模式,不支持变长路径。几跳之内,模式语法比等价的 JOIN 链清晰得多;超出这个范围就用 WITH RECURSIVE(见边界与 WITH RECURSIVE)。
定义图:CREATE PROPERTY GRAPH
经典的社交网络模型:person 和 knows。
CREATE TABLE person (
id int PRIMARY KEY,
name text NOT NULL,
age int,
city text
);
CREATE TABLE knows (
a int NOT NULL REFERENCES person(id), -- 认识谁
b int NOT NULL REFERENCES person(id), -- 被谁认识
since int,
PRIMARY KEY (a, b)
);把它声明成图——顶点是 person,边是 knows,方向从 a 指向 b:
CREATE PROPERTY GRAPH social
VERTEX TABLES (
person KEY (id) LABEL person PROPERTIES (id, name, age, city)
)
EDGE TABLES (
knows
SOURCE KEY (a) REFERENCES person (id)
DESTINATION KEY (b) REFERENCES person (id)
LABEL knows PROPERTIES (since)
);把它当一份契约读:
- 顶点表的
KEY通常就是主键;顶点表必须有主键。 SOURCE KEY ... REFERENCES ...和DESTINATION KEY ... REFERENCES ...声明边的方向:从哪列出发、到哪张表的哪列。LABEL是图里的名字(可以与表名不同);PROPERTIES决定哪些列在图中可见。如果表名、列名本身就是想要的标签和属性,这两个子句都可以省略。
图不复制数据
CREATE PROPERTY GRAPH 只存元数据。数据仍在原表,图的创建和删除都不动业务数据,定义丢了可以随时重建。
用 GRAPH_TABLE 查询
列出所有人:
SELECT name
FROM GRAPH_TABLE (social
MATCH (p IS person)
COLUMNS (p.name)
)
ORDER BY name;逻辑上等价于 SELECT name FROM person ORDER BY name。MATCH (p IS person) 遍历每个 person 顶点并绑定为 p;COLUMNS 投影输出列。GRAPH_TABLE 的结果就是一张普通表:可以起别名、过滤、和其他 FROM 项做 JOIN。
COLUMNS 不支持 p.*
COLUMNS (p.*) 会报错 "*" is not supported here。输出列必须显式列出,例如 COLUMNS (p.id, p.name)。
边模式与方向
SELECT *
FROM GRAPH_TABLE (social
MATCH (p IS person)-[IS knows]->(p2 IS person)
COLUMNS (p.id, p.name, p2.id, p2.name)
)
ORDER BY 1, 2, 3;(p)-[IS knows]->(p2) 是边模式:
->沿声明方向走(SOURCE → DESTINATION);<-反向。- 裸
-两个方向都匹配。
无向边会返回双份数据
裸 - 表示两个方向任一匹配(相当于两个方向的 OR),同一对关系会出现两次。除非底表数据本身对称(每个方向各插了一行),否则明确写 -> 或 <-。
多跳
SELECT *
FROM GRAPH_TABLE (social
MATCH (a IS person)-[IS knows]->
(b IS person)-[IS knows]->(c IS person)
WHERE a.id <> c.id
COLUMNS (a.name AS a, b.name AS via, c.name AS c)
)
ORDER BY a, c, via;- 每多一跳,就在链上多写一段
-[IS knows]->(...)。 WHERE a.id <> c.id写在MATCH里,过滤掉 "Alice → Bob → Alice" 这类回路。去掉它就能看到包含回路在内的所有两跳路径。
多类顶点与边
真实模型通常横跨多张表。加上公司和雇佣关系:
CREATE TABLE company (
id int PRIMARY KEY,
name text NOT NULL,
industry text NOT NULL
);
CREATE TABLE works_at (
pid int NOT NULL REFERENCES person(id),
cid int NOT NULL REFERENCES company(id),
role text NOT NULL,
PRIMARY KEY (pid, cid)
);
CREATE PROPERTY GRAPH company_social
VERTEX TABLES (
person KEY (id) LABEL person PROPERTIES (id, name, age, city),
company KEY (id) LABEL company PROPERTIES (id, name, industry)
)
EDGE TABLES (
knows
SOURCE KEY (a) REFERENCES person (id)
DESTINATION KEY (b) REFERENCES person (id)
LABEL knows PROPERTIES (since),
works_at
SOURCE KEY (pid) REFERENCES person (id)
DESTINATION KEY (cid) REFERENCES company (id)
LABEL works_at PROPERTIES (role)
);一个图包含两类顶点、两类边,一条查询横跨它们——"Alice 的朋友都在哪上班?":
SELECT *
FROM GRAPH_TABLE (company_social
MATCH (me IS person WHERE me.name = 'Alice')
-[IS knows]->(friend IS person)
-[IS works_at]->(co IS company)
COLUMNS (friend.name AS friend, co.name AS company)
)
ORDER BY friend, company;WHERE 可以直接写在顶点模式里。等价的普通 SQL 要写多表 JOIN;图语法把"沿哪类关系走"直接编进了模式。
没有多模式 MATCH:JOIN 两个 GRAPH_TABLE
SQL 标准里逗号分隔的 MATCH (a...), (b...) 在 PostgreSQL 19 尚未实现。变通方案是本页最实用的写法:GRAPH_TABLE 的结果是普通表,把 ID 投影出来再 JOIN:
SELECT m.me, m.via, m.coworker, w.company
FROM GRAPH_TABLE (company_social
MATCH (a IS person)-[IS knows]->(b IS person)-[IS knows]->(c IS person)
WHERE a.id <> c.id
COLUMNS (a.id AS aid, c.id AS cid, a.name AS me,
b.name AS via, c.name AS coworker)
) m
JOIN GRAPH_TABLE (company_social
MATCH (x IS person)-[IS works_at]->(co IS company)
<-[IS works_at]-(y IS person)
WHERE x.id <> y.id
COLUMNS (x.id AS xid, y.id AS yid, co.name AS company)
) w ON w.xid = m.aid AND w.yid = m.cid
ORDER BY me, coworker;"互相认识的同事"这类问题就这么拆:一个 GRAPH_TABLE 解决一段图模式,ID 做桥接。
匿名边会 union 所有边类型
只有一个边表的图里,(a)->(b) 没有歧义;但在 company_social 这种多边图里,匿名边会静默地 union 所有边类型——knows 和 works_at 的行一起出来。图里只要有多个边表,就写明边标签([IS knows]、[IS works_at])。
性能:它就是按 JOIN 规划的
发布说明明确写到 SQL/PGQ 查询"像视图一样处理,被写成标准关系查询"。两跳模式展开成多路 JOIN 后走普通优化器,EXPLAIN 里没有任何图专用执行节点。这正是它可预测的原因:计划形状由模式形状直接决定,索引规则和你熟悉的 JOIN 完全一样。
真正要记住的规则只有一条:边表主键覆盖源端;要做反向遍历,就给目标端列补索引。
- 正向("42 认识谁")按
knows.a = 42查找,主键(a, b)直接覆盖。 - 反向("谁认识 42")按
knows.b过滤,这个主键帮不上忙——扫描退化为读整张边表,表越大差距越大。
CREATE INDEX ON knows (b);用 EXPLAIN (ANALYZE, BUFFERS) 验证,方式和普通 JOIN 完全一样,见索引与 EXPLAIN。
数据血缘查询模式
合规场景的经典问题是"报表里这个数字是怎么来的?"。把 ETL 管线建模成图:
- 每层一张顶点表:
clickstream_source(原始)→staging→fact→report,各有id主键和name。 - 每种转换类型一张边表,
PRIMARY KEY (src, dst),外键指向它连接的两层:loads_into(直拷)、aggregates_into(分组聚合)、rollup_into(进一步汇总)。
CREATE PROPERTY GRAPH lineage
VERTEX TABLES (
clickstream_source KEY (id),
staging KEY (id),
fact KEY (id),
report KEY (id)
)
EDGE TABLES (
loads_into
SOURCE KEY (src) REFERENCES clickstream_source (id)
DESTINATION KEY (dst) REFERENCES staging (id),
aggregates_into
SOURCE KEY (src) REFERENCES staging (id)
DESTINATION KEY (dst) REFERENCES fact (id),
rollup_into
SOURCE KEY (src) REFERENCES fact (id)
DESTINATION KEY (dst) REFERENCES report (id)
);边标签带语义,这是它比"外键加递归 CTE"强的地方:rollup_into 告诉你发生了哪种转换,而外键只告诉你存在关系。CREATE PROPERTY GRAPH 语句本身可以从编排工具的元数据生成(dbt、Airflow 这类工具本来就记录上下游关系)。
四个高频查询模式:
-- 1. 值溯源:这个报表数字是谁喂出来的
SELECT *
FROM GRAPH_TABLE (lineage
MATCH (r IS report)<-[IS rollup_into]-(f IS fact)
COLUMNS (r.name AS report, f.name AS fact)
);
-- 2. 下游影响:改了这个源,哪些报表会出问题?
SELECT DISTINCT rpt
FROM GRAPH_TABLE (lineage
MATCH (s IS clickstream_source WHERE s.name = 'events_raw')
-[IS loads_into]->(IS staging)
-[IS aggregates_into]->(IS fact)
-[IS rollup_into]->(r IS report)
COLUMNS (r.name AS rpt)
);
-- 3. 全链路审计:MATCH 同 2,COLUMNS 把每一跳都输出
-- (s.name、staging 名、fact 名、r.name)
-- 4. 黑洞:没有下游消费者的源数据
SELECT src.name
FROM GRAPH_TABLE (lineage
MATCH (s IS clickstream_source)
COLUMNS (s.id AS sid, s.name AS name)
) src
WHERE NOT EXISTS (
SELECT 1 FROM loads_into l WHERE l.src = src.sid
);模式 4 展示了退路:GRAPH_TABLE 的结果可以和普通 SQL 自由组合,反连接、聚合、EXISTS 都能用在图输出之上。
边界与 WITH RECURSIVE
PostgreSQL 19 的 SQL/PGQ 还没有:
- 变长路径:
(a)-[IS knows]->{1,3}(b)会被拒绝。要么逐跳展开后UNION,要么退回WITH RECURSIVE。 - 多模式 MATCH(逗号分隔)——用两个
GRAPH_TABLEJOIN 替代,如上文所示。 - 路径变量绑定(
p = (a)->(b))和ANY SHORTEST/ALL SHORTEST最短路。
超过五跳左右的深链,递归 CTE 仍然是最顺手的工具:
WITH RECURSIVE chain AS (
SELECT a, b, 1 AS depth FROM knows WHERE a = 42
UNION ALL
SELECT k.a, k.b, c.depth + 1
FROM chain c JOIN knows k ON k.a = c.b
WHERE c.depth < 5
)
SELECT * FROM chain;更多 CTE 写法见查询工具箱。
AI prompt:让 AI 起草图查询
请帮我在 PostgreSQL 19 上做图查询。 1. 我的 schema:(贴出 CREATE TABLE) 2. 我要解决的问题:(描述查询意图,如"找出 Alice 朋友中在 Acme 工作的人") 3. 请: - 先写 CREATE PROPERTY GRAPH(顶点表必须有主键,边表写明 SOURCE/DESTINATION KEY) - 再写 GRAPH_TABLE ... MATCH ... COLUMNS 查询 - 注意:PG 19 不支持变长路径、多模式 MATCH、COLUMNS 里的 *;多边类型图必须写边标签 - 最后用 EXPLAIN 说明索引需求(反向遍历要补目标端索引) 4. 如果查询需要 5 跳以上,改用 WITH RECURSIVE 并说明。
相关页面
- PostgreSQL 19 发布与升级指南 —— SQL/PGQ 的版本边界
- 索引与 EXPLAIN —— 验证反向遍历索引
- 查询工具箱 —— CTE 与
WITH RECURSIVE
Last updated on