PostgreSQL 19 新特性:基于 SQL/PGQ 实现图数据查询
PostgreSQL 19 将引入 SQL/PGQSQL Property Graph Queries支持这是 ISO/IEC 9075-16:2023 标准定义的图查询语言。SQL/PGQ 允许在现有关系表之上定义属性图Property Graph并使用图查询语法进行关系遍历和模式匹配。对于社交网络、知识图谱、供应链分析以及推荐系统等场景而言这意味着 PostgreSQL 将具备更完善的原生图查询能力。本文将从一个最简单的示例出发介绍 PostgreSQL 19 中 SQL/PGQ 的基本语法以及其如何将图查询转换为传统关系查询来执行。PostgreSQL 19 中的第一个 Property GraphSQL/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 (cost575.72..588.55 rows5132 width96) Sort Key: person.name, person_2.name, person_1.name - Hash Join (cost145.96..259.45 rows5132 width96) Hash Cond: (knows_1.b person_2.id) Join Filter: (person.id person_2.id) - Hash Join (cost117.74..217.66 rows5138 width72) Hash Cond: (knows.b person_1.id) - Hash Join (cost28.23..64.01 rows2040 width40) Hash Cond: (knows.a person.id) - Seq Scan on knows (cost0.00..30.40 rows2040 width8) - Hash (cost18.10..18.10 rows810 width36) - Seq Scan on person (cost0.00..18.10 rows810 width36) - Hash (cost64.01..64.01 rows2040 width44) - Hash Join (cost28.23..64.01 rows2040 width44) Hash Cond: (knows_1.a person_1.id) - Seq Scan on knows knows_1 (cost0.00..30.40 rows2040 width8) - Hash (cost18.10..18.10 rows810 width36) - Seq Scan on person person_1 (cost0.00..18.10 rows810 width36) - Hash (cost18.10..18.10 rows810 width36) - Seq Scan on person person_2 (cost0.00..18.10 rows810 width36) (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/