PostgreSQL Field Guide

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 GRAPHGRAPH_TABLE。版本状态见 PostgreSQL 19 发布与升级指南;本文语法以官方文档为准(5.15 Property Graphs7.9 Graph Queries)。

什么场景值得用图查询

  • 社交关系:朋友的朋友、共同好友。
  • 数据血缘:报表里的数字由哪些源数据、经过哪些转换算出来。
  • 风控与审计:资金流向、可疑路径、合规追溯。

取舍在于深度固定。PostgreSQL 19 的 SQL/PGQ 按跳逐段匹配模式,不支持变长路径。几跳之内,模式语法比等价的 JOIN 链清晰得多;超出这个范围就用 WITH RECURSIVE(见边界与 WITH RECURSIVE)。

定义图:CREATE PROPERTY GRAPH

经典的社交网络模型:personknows

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 nameMATCH (p IS person) 遍历每个 person 顶点并绑定为 pCOLUMNS 投影输出列。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 所有边类型——knowsworks_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(原始)→ stagingfactreport,各有 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_TABLE JOIN 替代,如上文所示。
  • 路径变量绑定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 起草图查询

生成 SQL/PGQ 图查询
请帮我在 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 并说明。

相关页面

Last updated on

On this page