PostgreSQL 19 的 SQL/PGQ:在关系表上直接写图查询

2026-06-03 14 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

关系数据库里存的是表,但很多业务问题本质上是图——"谁认识谁的朋友"、"订单经过哪些环节流转"。过去要么把数据导到专门的图数据库,要么手写多层 JOIN 把自己绕晕。PostgreSQL 19 引入了 SQL/PGQ(ISO/IEC 9075-16 (2023) 标准),让你在现有的关系表上定义图语义,然后用图模式匹配语法来查询。不需要安装扩展,不需要复制数据,底层仍然是 PostgreSQL 的执行引擎。

两张表,一个图

SQL/PGQ 引入了两个核心构造:CREATE PROPERTY GRAPH 定义图的元数据,GRAPH_TABLE 在 SQL 语句中执行图查询。先看最小的社交网络例子:

-- 顶点:人
CREATE TABLE person (
    id   int PRIMARY KEY,
    name text NOT NULL,
    age  int NOT NULL,
        city text NOT NULL
);

-- 边:谁认识谁
CREATE TABLE knows (
    a    int NOT NULL REFERENCES person(id),
    b    int NOT NULL REFERENCES person(id),
    since int NOT NULL,
    PRIMARY KEY (a, b)
);

-- 填入样本数据
INSERT INTO person VALUES
    (1, 'Alice', 30, 'Berlin'),
    (2, 'Bob',   25, 'Berlin'),
    (3, 'Carol', 35, 'Paris'),
    (4, 'Dan',   28, 'Paris'),
    (5, 'Eve',   40, 'London'),
    (6, 'Frank', 33, 'London');

INSERT INTO knows VALUES
    (1, 2, 2018), (2, 1, 2019),
    (1, 3, 2020), (2, 3, 2020),
    (3, 2, 2021), (3, 4, 2021),
    (4, 5, 2022), (5, 6, 2019),
    (6, 1, 2023);

数据就是普通的关系表,外键已经暗示了连接方向。接下来用 CREATE PROPERTY GRAPH 把这些表"声明"为一个图:

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)
    );

这段定义可以当作一份"契约"来读:person 表的所有行成为图的顶点,标签也叫 person(标签可以和表名不同);knows 表的每一行是一条边,SOURCE 指向起点,DESTINATION 指向终点,since 列作为边的属性暴露出来。没有新增存储,没有数据迁移——PostgreSQL 只记住了这份元数据。

从最简单的图查询开始

列出所有人的名字:

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) 声明了一个顶点模式变量 p,绑定到 person 标签;COLUMNS 指定输出列。

需要注意的一点:COLUMNS 里必须显式列出列名,p.* 不被支持——会直接报错:

ERROR: "*" is not supported here

这是当前实现的一个限制,写查询时要把需要的列逐一写出来。

边的方向:-><--

"谁认识谁"的查询引入了边模式:

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 方向行走。结果返回 9 行,正好是 knows 表的全部数据。

如果写成无方向的 -(不带箭头),查询会编译成 OR 条件:

-- 方向未指定,数据会被双向遍历
MATCH (p IS person)-[IS knows]-(p2 IS person)

底层等价于 (person.id = knows.a AND person_1.id = knows.b) OR (person_1.id = knows.a AND person.id = knows.b),结果从 9 行膨胀到 18 行——每条边被走了两次。对于 knows 这种有方向语义的关系("A 从 2018 年认识 B"不等于"B 从 2019 年认识 A"),无方向遍历会让行数出错。方向运算符的选择要和业务语义对齐。

多跳遍历与 WHERE 过滤

图查询的核心价值在于多跳遍历。"朋友的朋友"只需在 MATCH 里串联两条边:

SELECT *
FROM GRAPH_TABLE (
    social
    MATCH (a IS person)-[IS knows]->
          (b IS person)-[IS knows]->(c IS person)
    COLUMNS (a.name AS a, b.name AS via, c.name AS c)
)
ORDER BY a, c, via;

结果中会出现 Alice -> Bob -> Alice 这种回路——数据上合法,语义上往往没意义。SQL/PGQ 允许在 GRAPH_TABLE 内部加 WHERE 子句来过滤:

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;

WHERE a.id <> c.id 排除了起点和终点相同的行,结果从 15 行缩减到 11 行。过滤条件放在图模式匹配内部,语义清晰,不需要在外层 SQL 再包一层子查询。

底层发生了什么

EXPLAIN 看一下两跳查询的执行计划:

EXPLAIN
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;

输出是一系列 Hash Join + Seq Scan——完全是普通 PostgreSQL 的执行节点,没有新增任何图专用的执行器。SQL/PGQ 的实现本质是查询重写:PostgreSQL 在幕后把图模式匹配展开为等价的多表 JOIN,然后走正常的优化和执行路径。

这意味着: - 现有的索引、分区、并行查询等优化手段全部可用 - 不需要学习新的执行模型 - 性能调优仍然依赖你熟悉的那套 EXPLAIN 分析方法

上手实践:完整可运行示例

以下脚本可以在 PostgreSQL 19(或当前 devel 版本)上直接运行,从建表到定义图再到多跳查询,一步到位:

-- 1. 建表与数据
CREATE TABLE person (
    id   int PRIMARY KEY,
    name text NOT NULL,
    age  int NOT NULL,
    city text NOT NULL
);

CREATE TABLE knows (
    a    int NOT NULL REFERENCES person(id),
    b    int NOT NULL REFERENCES person(id),
    since int NOT NULL,
    PRIMARY KEY (a, b)
);

INSERT INTO person VALUES
    (1, 'Alice', 30, 'Berlin'),
    (2, 'Bob',   25, 'Berlin'),
    (3, 'Carol', 35, 'Paris'),
    (4, 'Dan',   28, 'Paris'),
    (5, 'Eve',   40, 'London'),
    (6, 'Frank', 33, 'London');

INSERT INTO knows VALUES
    (1, 2, 2018), (2, 1, 2019),
    (1, 3, 2020), (2, 3, 2020),
    (3, 2, 2021), (3, 4, 2021),
    (4, 5, 2022), (5, 6, 2019),
    (6, 1, 2023);

-- 2. 定义属性图
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)
    );

-- 3. 单跳:谁认识谁
SELECT p.name AS who, p2.name AS knows_whom
FROM GRAPH_TABLE (
    social
    MATCH (p IS person)-[IS knows]->(p2 IS person)
    COLUMNS (p.name, p2.name)
)
ORDER BY who, knows_whom;

-- 4. 两跳:朋友的朋友(排除回路)
SELECT a AS from_person, via AS through, c AS to_person
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 from_person, to_person, through;

-- 5. 看看底层执行计划
EXPLAIN
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)
);

运行前确认你的 PostgreSQL 版本支持 SQL/PGQ(目前是 19 devel 分支)。生产环境请等待正式发布。

采纳建议与注意事项

  • 先定义图,再写查询CREATE PROPERTY GRAPH 是一次性的元数据声明,后续所有图查询都引用这个定义。如果表结构变了,需要重建属性图。
  • 方向运算符要和业务对齐:有向关系用 -> / <-,只在语义确实对称时才用无方向的 -,否则行数会翻倍。
  • COLUMNS 不支持 *:必须显式列出输出列,这对复杂查询反而是一种约束——迫使你明确需要什么,避免意外暴露过多字段。
  • 性能调优照旧:底层是查询重写,不是新执行引擎。给 JOIN 涉及的列建索引、看 EXPLAIN、调 work_mem——这些老办法仍然有效。
  • 当前限制:SQL/PGQ 在 PostgreSQL 19 是首次引入,异构图(多种顶点/边标签混合匹配)等高级特性在后续文章中才会展开。现阶段适合在简单图场景中试用,积累语法经验。

SQL/PGQ 的价值不在于替代图数据库,而在于让关系数据库里已有的连接数据能用更自然的语法来遍历。当你下次需要写三层 JOIN 来找"朋友的朋友的朋友"时,不妨试试用一条 MATCH 链来表达——逻辑更清晰,底层执行还是你熟悉的 PostgreSQL。


相关推荐