PostgreSQL 19 新特性:基于 SQL/PGQ 实现图数据查询

PostgreSQL 19 将引入 SQL/PGQ(SQL Property Graph Queries)支持,这是 ISO/IEC 9075-16:2023 标准定义的图查询语言。

SQL/PGQ 允许在现有关系表之上定义属性图(Property Graph),并使用图查询语法进行关系遍历和模式匹配。对于社交网络、知识图谱、供应链分析以及推荐系统等场景而言,这意味着 PostgreSQL 将具备更完善的原生图查询能力。

本文将从一个最简单的示例出发,介绍 PostgreSQL 19 中 SQL/PGQ 的基本语法,以及其如何将图查询转换为传统关系查询来执行。

PostgreSQL 19 中的第一个 Property Graph

SQL/PGQ 的引入,为 PostgreSQL 新增了两项核心能力:使用 CREATE PROPERTY GRAPH 定义图模型和使用 GRAPH_TABLE 查询图数据。

两者共同构成了 PostgreSQL 19 中 SQL/PGQ 的基础框架。下面先从 Property Graph 的定义方式开始,了解 PostgreSQL 是如何将关系模型映射为图模型的:

复制代码
friends=# \h CREATE PROPERTY GRAPH
Command:     CREATE PROPERTY GRAPH
Description: define a new SQL-property graph
Syntax:
CREATE [ TEMP | TEMPORARY ] PROPERTY GRAPH name
    [ {VERTEX|NODE} TABLES ( vertex_table_definition [, ...] ) ]
    [ {EDGE|RELATIONSHIP} TABLES ( edge_table_definition [, ...] ) ]

where vertex_table_definition is:

    vertex_table_name [ AS alias ]
[ KEY ( column_name [, ...] ) ]
[ element_table_label_and_properties ]

and edge_table_definition is:

    edge_table_name [ AS alias ] [ KEY ( column_name [, ...] ) ]
        SOURCE [ KEY ( column_name [, ...] )
REFERENCES ] source_table [ ( column_name [, ...] ) ]
        DESTINATION [ KEY ( column_name [, ...] )
REFERENCES ] dest_table [ ( column_name [, ...] ) ]
        [ element_table_label_and_properties ]

and element_table_label_and_properties is either:

    NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES
( { expression [ AS property_name ] } [, ...] )

or:

   { { LABEL label_name | DEFAULT LABEL }
[ NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES
( { expression [ AS property_name ] } [, ...] ) ] } [...]

URL: https://www.postgresql.org/docs/devel/sql-create-property-graph.html

在深入介绍语法细节之前,需要先理解 SQL/PGQ 的设计目标。在传统关系数据库中,数据以表的形式存储。而 CREATE PROPERTY GRAPH 的作用,则是在现有关系模型之上定义一个图模型(Graph Model)。这种定义仅包含图的元数据信息,并不会创建新的数据副本。

定义完成后,即可通过 SQL 语句中的 GRAPH_TABLE 对图进行查询。需要特别说明的是,SQL/PGQ 并不会引入额外的数据存储机制:

  • 无需安装扩展;
  • 无需复制数据;
  • 无需维护独立的图数据库。

图结构完全建立在现有关系表及其关联关系之上,本质上是将表之间的连接关系以更符合图模型的方式进行描述和查询。为了帮助理解这一机制,本文将从一个最简单且具有实际意义的示例入手,仅通过两张表和少量 SQL/PGQ 语句,逐步展示 PostgreSQL 19 中图查询功能的工作原理。

SQL/PGQ 示例:构建一个简单社交网络

为了演示 SQL/PGQ 的基本能力,先创建一个简单的社交网络模型。整个模型包含两张表:

  • person:存储人员信息
  • knows:记录人员之间的认识关系

基于这两张表,希望实现一个典型的图查询需求:查找任意用户的"朋友的朋友"(Friends of Friends),即发现用户之间的间接关联关系。

创建基础表:

复制代码
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);

在 PostgreSQL 中定义 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 标签的节点(Vertex),而表中的列则作为节点属性(Property)对外暴露。

与此同时,knows 表被映射为图中的边(Edge),用于描述节点之间的关联关系。通过 SOURCE 和 DESTINATION 的定义,PostgreSQL 能够确定边的起点和终点,并据此建立节点之间的连接关系。

最终,一个基于关系表构建的属性图(Property Graph)便定义完成。

运行第一个 SQL/PGQ 查询

定义完图之后,即可使用 GRAPH_TABLE 进行查询。最简单的查询如下:

复制代码
tutorial=# SELECT name
FROM GRAPH_TABLE (social
MATCH (p IS person)
COLUMNS (p.name)
)
ORDER BY name;
 name
-------
 Alice
 Bob
 Carol
 Dan
 Eve
 Frank
(6 rows)

从结果来看,该 SQL/PGQ 查询与下面的传统 SQL 查询作用相同:

复制代码
SELECT name FROM person ORDER BY name

那么,这条查询是如何工作的?

GRAPH_TABLE 用于执行图查询,MATCH (p IS person) 表示匹配一个 person 节点,并使用变量 p 对其进行引用。随后通过 COLUMNS (p.name) 返回该节点的 name 属性。

这是 SQL/PGQ 中最简单的一类查询。下面通过几个简单变体,进一步了解其查询方式:

复制代码
tutorial=# SELECT *
FROM  GRAPH_TABLE (social
MATCH (p IS person )
COLUMNS (p.id, p.name)
);
 id | name
----+-------
  1 | Alice
  2 | Bob
  3 | Carol
  4 | Dan
  5 | Eve
  6 | Frank
(6 rows)

tutorial=# SELECT *
FROM GRAPH_TABLE (social
MATCH (p IS person )
COLUMNS (p.*)
);
ERROR:  "*" is not supported here
LINE 1: ...OM GRAPH_TABLE (social MATCH (p IS person ) COLUMNS (p.*) );

这里需要注意一个细节:COLUMNS 子句支持同时返回多个属性,但目前并不支持使用 * 进行字段展开,因此需要显式列出需要返回的属性列表。

在了解了最基础的节点查询之后,接下来通过一个更具实际意义的示例来展示 SQL/PGQ 的能力。下面的问题非常典型:谁认识谁(Who Knows Whom)?也就是说,如何通过图查询找出节点之间的直接关联关系。

下面来看具体实现方式:

复制代码
tutorial=# 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;
 id | name  | id | name
----+-------+----+-------
  1 | Alice |  2 | Bob
  1 | Alice |  3 | Carol
  2 | Bob   |  1 | Alice
  2 | Bob   |  3 | Carol
  3 | Carol |  2 | Bob
  3 | Carol |  4 | Dan
  4 | Dan   |  5 | Eve
  5 | Eve   |  6 | Frank
  6 | Frank |  1 | Alice
(9 rows)

图查询的核心思想是在节点(Vertex)和边(Edge)之间进行遍历。以上述查询为例,首先从一个 person 节点开始,然后沿着已经定义好的 knows 关系(在图模型中对应一条边),访问与其关联的另一个 person 节点。例如(a)-[IS knows]->(b)就是一个边模式。其中,箭头 -> 表示按照边定义时声明的方向进行遍历,即SOURCE → DESTINATION,而 <- 则表示按照相反方向进行遍历。

如果将箭头写成:(a)-[IS knows]-(b),情况则有所不同。单独的 - 表示无向关系。此时查询不会只沿一个方向遍历,而是会同时匹配两个方向,因此结果中的数据会被重复返回。对于当前示例而言,这意味着每条关系都会被匹配两次。

下面来看一个错误的查询示例:

复制代码
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;
 id | name  | id | name
----+-------+----+-------
  1 | Alice |  2 | Bob
  1 | Alice |  2 | Bob
  1 | Alice |  3 | Carol
 ...
(18 rows)

结果行数之所以会出现重复,是因为无向操作符(-)并不是简单地忽略方向。在底层实现中,PostgreSQL 会将其转换为包含 OR 条件的匹配逻辑:

复制代码
(person.id = knows.a AND person_1.id = knows.b)  OR

(person_1.id = knows.a AND person.id = knows.b)

SQL/PGQ 中的多跳查询

接下来进一步扩展查询路径,查找"朋友的朋友"(Friends of Friends)关系。其目标是判断用户之间是否存在间接认识的情况。下面的查询展示了如何实现这一过程:

复制代码
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;
   a   |  via  |   c
-------+-------+-------
 Alice | Bob   | Alice
 Alice | Carol | Bob
 Alice | Bob   | Carol
 Alice | Carol | Dan
 Bob   | Alice | Bob
 Bob   | Carol | Bob
 Bob   | Alice | Carol
 Bob   | Carol | Dan
 Carol | Bob   | Alice
 Carol | Bob   | Carol
 Carol | Dan   | Eve
 Dan   | Eve   | Frank
 Eve   | Frank | Alice
 Frank | Alice | Bob
 Frank | Alice | Carol
(15 rows)

为了实现这一查询,需要在原有图查询的基础上继续扩展路径,也就是在第二跳之后再增加一条边,从而形成三层节点之间的关联关系。

不过,观察查询结果后会发现一个有趣的现象。例如:Alice → Bob → Alice。这表示 Alice 认识 Bob,而 Bob 又认识 Alice。从数据角度来看,这条路径是完全成立的;但从"朋友的朋友"这一查询场景来看,这样的结果往往并没有太大的实际意义。

因此,在大多数情况下,通常希望将这类记录过滤掉。那么应该如何实现呢?答案很简单------使用 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;
   a   |  via  |   c
-------+-------+-------
 Alice | Carol | Bob
 Alice | Bob   | Carol
 Alice | Carol | Dan
 Bob   | Alice | Carol
 Bob   | Carol | Dan
 Carol | Bob   | Alice
 Carol | Dan   | Eve
 Dan   | Eve   | Frank
 Eve   | Frank | Alice
 Frank | Alice | Bob
 Frank | Alice | Carol
(11 rows)

这里有一个值得关注的细节:WHERE 子句可以直接出现在 GRAPH_TABLE 中,与 MATCH 模式一起参与图查询,而不是像传统 SQL 那样统一放在外层查询进行过滤。

SQL/PGQ 的底层实现原理

在简要了解 PostgreSQL 19 中的图查询功能之后,接下来进一步看看其底层实现机制。

对于 PostgreSQL 而言,理解查询执行过程最有效的方式通常是查看执行计划。因此,下面通过 EXPLAIN 来观察 SQL/PGQ 查询在内部是如何被处理的:

复制代码
tutorial=# 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;
                              QUERY PLAN
------------------------------------------------------------------
 Sort  (cost=575.72..588.55 rows=5132 width=96)
  Sort Key: person.name, person_2.name, person_1.name
  ->  Hash Join  (cost=145.96..259.45 rows=5132 width=96)
        Hash Cond: (knows_1.b = person_2.id)
        Join Filter: (person.id <> person_2.id)
        ->  Hash Join  (cost=117.74..217.66 rows=5138 width=72)
            Hash Cond: (knows.b = person_1.id)
            ->  Hash Join  (cost=28.23..64.01 rows=2040 width=40)
                Hash Cond: (knows.a = person.id)
                ->  Seq Scan on knows
(cost=0.00..30.40 rows=2040 width=8)
                ->  Hash  (cost=18.10..18.10 rows=810 width=36)
                    ->  Seq Scan on person
(cost=0.00..18.10 rows=810 width=36)
            ->  Hash  (cost=64.01..64.01 rows=2040 width=44)
                ->  Hash Join
(cost=28.23..64.01 rows=2040 width=44)
                    Hash Cond: (knows_1.a = person_1.id)
                    ->  Seq Scan on knows knows_1
(cost=0.00..30.40 rows=2040 width=8)
                    ->  Hash  (cost=18.10..18.10 rows=810 width=36)
                        ->  Seq Scan on person person_1
(cost=0.00..18.10 rows=810 width=36)
        ->  Hash  (cost=18.10..18.10 rows=810 width=36)
            ->  Seq Scan on person person_2
(cost=0.00..18.10 rows=810 width=36)
(20 rows)

那么,从执行计划中能够看到什么呢?

最重要的一点是,执行计划中并没有出现额外的执行器节点,也没有任何专门用于图查询的新组件。PostgreSQL 所做的事情实际上非常简单:在后台将 SQL/PGQ 查询重写为普通关系查询,从而以更加简洁、紧凑的语法表达原本复杂的关联逻辑。这也是 SQL/PGQ 的一个重要优势------在不改变 PostgreSQL 执行框架的前提下,为开发人员提供更直观的图查询能力。

此外,CREATE PROPERTY GRAPH 所定义的图模型也能够更清晰地描述数据之间的关系,从而支持这种 SQL 语法层面的简化。

总结

SQL/PGQ 为 PostgreSQL 19 带来了标准化的图查询能力,使关系数据库能够直接支持属性图模型。

其主要特点包括:

  • 基于 ISO SQL/PGQ 国际标准
  • 无需额外扩展
  • 无需同步数据
  • 支持节点与边建模
  • 支持路径遍历与模式匹配
  • 支持多跳查询
  • 底层复用 PostgreSQL 现有优化器与执行器

对于社交网络、知识图谱、供应链分析、推荐系统等涉及复杂关系建模的场景,SQL/PGQ 将成为 PostgreSQL 19 最值得关注的新特性之一。

作者:Hans-Jürgen Schönig

原文链接:

https://www.cybertec-postgresql.com/en/handling-graphs-with-sql-pgq-in-postgresql/

相关推荐
jghhh012 小时前
C# 图片水印工具(支持9个位置)
数据库·microsoft·c#
辰海Coding2 小时前
MiniSpring框架学习笔记-JDBC 访问框架:如何抽取 JDBC 模板并隔离数据库?
java·数据库·笔记·学习·spring
救救孩子把2 小时前
01 Milvus-向量数据库基础
数据库·milvus
闪电悠米2 小时前
黑马点评-Redis 消息队列-01_why_redis_mq
java·数据库·spring boot·redis·缓存·junit·消息队列
oradh2 小时前
Oracle数据库扩展区(extent)概述
数据库·oracle·oracle基础·oracle数据库扩展区概述
IT策士2 小时前
Redis 从入门到精通:初识 Redis
数据库·redis·缓存
不剪发的Tony老师2 小时前
DBHub:一款免费开源的数据库MCP服务器
数据库·mcp
oqX0Cazj22 小时前
Go-Zero数据库事务实战:本地事务+失败自动回滚+生产避坑+简单分布式事务方案
数据库·分布式·golang
小肥君2 小时前
sqlite查询
数据库·sqlite