PostgreSQL 19 交互式导览

Hacker News Top 新闻

摘要

本文提供了一个关于PostgreSQL 19 beta新功能的交互式指南,重点介绍用于属性图查询的SQL/PGQ,并附有实践示例。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/07 18:32

# PostgreSQL 19 交互式导览 来源:https://victoriametrics.com/blog/postgres-19/index.html PostgreSQL 19 目前处于 Beta 测试阶段,预计在 2026 年 9 月或 10 月发布正式版。现在正是提前了解新功能的好时机。官方发布说明 (https://www.postgresql.org/docs/19/release-19.html) 是最权威的记录:其内容详尽,涵盖了每一次提交变更及其背后的贡献者。本文是官方文档的实践伴侣,我们将从发布说明中选取部分内容,将其转化为可运行的示例,以便你实际体验这些新行为。 在深入了解新特性之前,我们先设定一下背景。 本文基于官方发布说明和 PostgreSQL 源代码撰写,并遵循 PostgreSQL 许可证 (https://www.postgresql.org/about/licence/)。本文并非详尽列表,完整信息请参阅官方发布说明 (https://www.postgresql.org/docs/19/release-19.html)。 以下所有示例均针对 **PostgreSQL 19 beta 3**(发布于 2026-08-13)运行,其输出为该服务器的实际打印结果。 链接指向相关文档(D)、最相关的提交(C)以及每个功能的作者(A);建议查看以了解动机、用法和实现细节。作者(A)是在发布说明中被归功于该功能的人,通常指补丁作者,而非单一的主导作者。 背景设定完毕,让我们开始探索新特性吧。 ## 属性图查询 (#property-graph-queries) 这是本次发布的亮点。PostgreSQL 19 实现了 SQL/PGQ (https://www.postgresql.org/docs/19/queries-graph.html),即 SQL:2023 中的属性图部分。你可以基于现有表声明一个*属性图*,然后通过模式匹配来查询它,而无需自己编写连接(JOIN)语句。 在两张普通表之上创建一个图: ``` CREATE PROPERTY GRAPH social VERTEX TABLES ( person KEY (id) LABEL person PROPERTIES (id, name) ) EDGE TABLES ( follows KEY (follower, followee) SOURCE KEY (follower) REFERENCES person (id) DESTINATION KEY (followee) REFERENCES person (id) LABEL follows ); ``` 这里没有复制任何数据:`social` 是一个类似视图的对象,它声明“`person` 行是顶点,`follows` 行是边”。现在你可以用 `GRAPH_TABLE` 进行模式匹配,其中 `-[...]->` 表示一条有向边: ``` SELECT * FROM GRAPH_TABLE (social MATCH (a IS person)-[IS follows]->(b IS person) COLUMNS (a.name AS follower, b.name AS followee) ) ORDER BY follower, followee; ``` ``` ┌──────────┬──────────┐ │ follower │ followee │ ├──────────┼──────────┤ │ Ada │ Bo │ │ Ada │ Dee │ │ Bo │ Cleo │ │ Cleo │ Dee │ └──────────┴──────────┘ (4 rows) ``` 其价值在于多跳模式。链接两条边可以让你无需自连接就能查询“朋友的朋友”,而空括号 `()` 表示“我不关心具体名称的某个顶点”: ``` SELECT * FROM GRAPH_TABLE (social MATCH (a IS person WHERE a.name = 'Ada')-[IS follows]->()-[IS follows]->(c IS person) COLUMNS (a.name AS start, c.name AS friend_of_friend) ); ``` ``` ┌───────┬──────────────────┐ │ start │ friend_of_friend │ ├───────┼──────────────────┤ │ Ada │ Cleo │ └───────┴──────────────────┘ (1 row) ``` 这里没有引入新的执行引擎,这正是重点。`GRAPH_TABLE` 会被重写为普通的关系查询,因此你已知的查询规划器、统计信息和索引选择机制仍然适用: ``` EXPLAIN (COSTS OFF) SELECT * FROM GRAPH_TABLE (social MATCH (a IS person)-[IS follows]->(b IS person) COLUMNS (a.name AS follower, b.name AS followee) ); ``` ``` ┌───────────────────────────────────────────────────┐ │ QUERY PLAN │ ├───────────────────────────────────────────────────┤ │ Hash Join │ │ Hash Cond: (follows.followee = person_1.id) │ │ -> Hash Join │ │ Hash Cond: (follows.follower = person.id) │ │ -> Seq Scan on follows │ │ -> Hash │ │ -> Seq Scan on person │ │ -> Hash │ │ -> Seq Scan on person person_1 │ └───────────────────────────────────────────────────┘ (9 rows) ``` 在计划从图数据库迁移之前,你需要知道一个限制:此初始版本不支持可变长度路径。像 `-[IS follows]->{1,3}` 这样的量词可以解析但会被拒绝,错误信息为“element pattern quantifier is not supported”,因此模式必须明确写出每一跳。 - 文档:图查询(DGraph Queries)(https://www.postgresql.org/docs/19/queries-graph.html), 属性图(Property Graphs)(https://www.postgresql.org/docs/19/ddl-property-graphs.html), 创建属性图(CREATE PROPERTY GRAPH)(https://www.postgresql.org/docs/19/sql-create-property-graph.html) - 提交:2f094e7 (https://github.com/postgres/postgres/commit/2f094e7ac), c5b3253 (https://github.com/postgres/postgres/commit/c5b3253b8), a0dd070 (https://github.com/postgres/postgres/commit/a0dd0702e) - 作者:Peter Eisentraut, Ashutosh Bapat ## 时序更新与删除 (#temporal-updates-and-deletes) `UPDATE` 和 `DELETE` 语句新增的 `FOR PORTION OF` (https://www.postgresql.org/docs/19/sql-update.html) 子句允许操作范围列的一个片段。无需重写整个有效期,你只需指定一个子时段,PostgreSQL 会自动为你拆分行。 下表初始有一行数据:一个在 2026 年全年有效的价格。我们只修改七月份的价格,然后再次查询: ``` SELECT * FROM price ORDER BY valid_at; UPDATE price FOR PORTION OF valid_at FROM '2026-07-01' TO '2026-08-01' SET amount = 7.99; SELECT * FROM price ORDER BY valid_at; ``` ``` ┌────────┬─────────────────────────┬────────┐ │ sku │ valid_at │ amount │ ├────────┼─────────────────────────┼────────┤ │ widget │ [2026-01-01,2027-01-01) │ 9.99 │ └────────┴─────────────────────────┴────────┘ (1 row) UPDATE 1 ┌────────┬─────────────────────────┬────────┐ │ sku │ valid_at │ amount │ ├────────┼─────────────────────────┼────────┤ │ widget │ [2026-01-01,2026-07-01) │ 9.99 │ │ widget │ [2026-07-01,2026-08-01) │ 7.99 │ │ widget │ [2026-08-01,2027-01-01) │ 9.99 │ └────────┴─────────────────────────┴────────┘ (3 rows) ``` 输入一行,输出三行,未触及的时段保持原价。 `DELETE` 工作方式类似,是修剪而非拆分。此代码片段同样从那个未触及的全年行开始;删除从12月到无穷大的部分,保留了之前的部分: ``` SELECT * FROM price ORDER BY valid_at; DELETE FROM price FOR PORTION OF valid_at FROM '2026-12-01' TO NULL; SELECT * FROM price ORDER BY valid_at; ``` ``` ┌────────┬─────────────────────────┬────────┐ │ sku │ valid_at │ amount │ ├────────┼─────────────────────────┼────────┤ │ widget │ [2026-01-01,2027-01-01) │ 9.99 │ └────────┴─────────────────────────┴────────┘ (1 row) DELETE 1 ┌────────┬─────────────────────────┬────────┐ │ sku │ valid_at │ amount │ ├────────┼─────────────────────────┼────────┤ │ widget │ [2026-01-01,2026-12-01) │ 9.99 │ └────────┴─────────────────────────┴────────┘ (1 row) ``` `NULL` 作为边界意味着无界,因此 `TO NULL` 表示“从12月起”。此功能与关于时序表的新文档章节(https://www.postgresql.org/docs/19/ddl-temporal-tables.html)同时推出,如果你在范围列中保存历史记录,值得阅读该文档。 - 文档:时序表(Temporal Tables)(https://www.postgresql.org/docs/19/ddl-temporal-tables.html), UPDATE (https://www.postgresql.org/docs/19/sql-update.html) - 提交:8e72d91 (https://github.com/postgres/postgres/commit/8e72d914c), b6ccd30 (https://github.com/postgres/postgres/commit/b6ccd30d8) - 作者:Paul A. Jungwirth ## 返回冲突行的 Upsert (#upsert-that-returns-the-row-you-lost-to) `INSERT ... ON CONFLICT DO NOTHING ... RETURNING` 一直存在一个恼人的缺陷:发生冲突的行在结果中完全缺失,因此你无法区分“已存在”和“从未发生”。要修复这个问题,你可以使用 `ON CONFLICT DO UPDATE`,但这只在你愿意更新每个冲突行的情况下才有效。 PostgreSQL 19 新增了 `ON CONFLICT DO SELECT` (https://www.postgresql.org/docs/19/sql-insert.html),它返回*已存在*的行,而不对其做任何修改。此处 `widget` 已存在且 `qty = 7`: ``` INSERT INTO inventory VALUES ('widget', 1), ('gadget', 3) ON CONFLICT (sku) DO SELECT RETURNING sku, qty; ``` ``` ┌────────┬─────┐ │ sku │ qty │ ├────────┼─────┤ │ widget │ 7 │ │ gadget │ 3 │ └────────┴─────┘ (2 rows) INSERT 0 2 ``` 两行都返回了,`widget` 报告的 `7` 是表中已有的值,而不是我们尝试插入的 `1`。 `DO SELECT` 也接受锁定子句,因此 `FOR UPDATE` 可以在你决定如何处理这些行时锁定冲突行。它还提供了一种区分两种行的方法:锁定行会为其 `xmax` 打上标记,因此 `xmax = 0` 这个技巧 (https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns) 可以告诉你哪些行是真正插入的: ``` INSERT INTO inventory VALUES ('widget', 1), ('gadget', 3) ON CONFLICT (sku) DO SELECT FOR UPDATE RETURNING sku, qty, xmax = 0 AS was_inserted; ``` ``` ┌────────┬─────┬──────────────┐ │ sku │ qty │ was_inserted │ ├────────┼─────┼──────────────┤ │ widget │ 7 │ f │ │ gadget │ 3 │ t │ └────────┴─────┴──────────────┘ (2 rows) INSERT 0 2 ``` 锁定子句是实现这一点的关键:没有锁定就没有可标记的锁,冲突行的 `xmax` 也会保持为 `0`,每一行都会声称 `was_inserted = t`。任何锁定子句都可以,包括 `FOR SHARE`。 - 文档:INSERT (https://www.postgresql.org/docs/19/sql-insert.html) - 提交:8832709 (https://github.com/postgres/postgres/commit/88327092f) - 作者:Andreas Karlsson, Marko Tiikkaja, Viktor Holmberg ## 窗口函数可跳过 NULL 值 (#window-functions-can-skip-nulls) `lead()`、`lag()`、`first_value()`、`last_value()` 和 `nth_value()` 现在接受 SQL 标准的 `IGNORE NULLS` (https://www.postgresql.org/docs/19/functions-window.html) 子句(及其默认对应的 `RESPECT NULLS`)。 其经典用途是填充稀疏数据中的空隙。这里一个传感器仅在温度变化时报告数据,中间留下 NULL: ``` SELECT ts, temp, last_value(temp) IGNORE NULLS OVER (ORDER BY ts) AS carried, lag(temp) IGNORE NULLS OVER (ORDER BY ts) AS prev_reported FROM readings ORDER BY ts; ``` ``` ┌────┬──────┬─────────┬───────────────┐ │ ts │ temp │ carried │ prev_reported │ ├────┼──────┼─────────┼───────────────┤ │ 1 │ 20 │ 20 │ │ │ 2 │ │ 20 │ 20 │ │ 3 │ │ 20 │ 20 │ │ 4 │ 23 │ 23 │ 20 │ │ 5 │ │ 23 │ 23 │ └────┴──────┴─────────┴───────────────┘ (5 rows) ``` `carried` 是最近观测值向前填充(last-observation-carried-forward),而 `prev_reported` 是前一个*真实*读数,而非前一行。在版本 19 之前,这需要使用嵌套子查询并利用 `count(temp)` 进行分组的小技巧;现在,这只是一个关键字。 - 文档:窗口函数(Window Functions)(https://www.postgresql.org/docs/19/functions-window.html) - 提交:25a30bb (https://github.com/postgres/postgres/commit/25a30bbd4) - 作者:Oliver Ford, Tatsuo Ishii ## REPACK (#repack) `VACUUM FULL` 和 `CLUSTER` 功能几乎相同(重写表以回收空间),但名称容易混淆,且都无法避免 `ACCESS EXCLUSIVE` 锁。PostgreSQL 19 将它们统一到 `REPACK` (https://www.postgresql.org/docs/19/sql-repack.html) 下。 考虑一个删除了一半行的表: ``` SELECT pg_size_pretty(pg_table_size('events')) AS size; REPACK events; SELECT pg_size_pretty(pg_table_size('events')) AS size; ``` ``` ┌────────┐ │ size │ ├────────┤ │ 736 kB │ └────────┘ (1 row) REPACK ┌────────┐ │ size │ ├────────┤ │ 360 kB │ └────────┘ (1 row) ``` 重要部分是新的 `CONCURRENTLY` 选项,它可以在不获取 `ACCESS EXCLUSIVE` 锁的情况下重建表。其工作原理是解码重建过程中产生的变更并回放它们,因此*读取和写入*都可以继续运行: ``` -- No ACCESS EXCLUSIVE lock: readers and writers keep working. REPACK (CONCURRENTLY) events; ``` 此示例无法在此运行:解码要求 `wal_level` 为 `replica` 或更高,而沙箱环境运行在 `minimal` 级别。这也意味着 `CONCURRENTLY` 会占用一个复制槽;新的 `max_repack_replication_slots` 设置(默认值为 5)限制了可同时运行的数量。 `CLUSTER` 原有的功能(按索引物理排序行)现在写作 `REPACK ... USING INDEX`。添加 `VERBOSE` 会使命令报告如何重写表:要么是索引扫描,要么是顺序扫描后排序。 ``` -- Physically order the rows by an index (the old CLUSTER behavior). REPACK (VERBOSE) events USING INDEX events_pkey; ``` ``` INFO: repacking "public.events" using index scan on "events_pkey" INFO: "public.events": found 6 removable, 2500 nonremovable row versions in 87 pages DETAIL: 0 dead row versions cannot be removed yet. CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. REPACK ``` `VACUUM FULL` 和 `CLUSTER` 仍然可用,因此不会破坏任何现有功能;它们只是旧的写法。 - 文档:REPACK (https://www.postgresql.org/docs/19/sql-repack.html) - 提交:ac58465 (https://github.com/postgres/postgres/commit/ac58465e0), 28d534e (https://github.com/postgres/postgres/commit/28d534e2a), 8fb95a8 (https://github.com/postgres/postgres/commit/8fb95a8ab), e76d8c7 (https://github.com/postgres/postgres/commit/e76d8c749) - 作者:Antonin Houska, Mihail Nikalayeu, Álvaro Herrera ## 合并与拆分分区(在 Beta 3 后已回滚) (#merge-and-split-partitions-reverted-after-beta-3) **请注意:** 该功能已于 2026-08-27(beta 3 发布两周后)被回滚 (https://github.com/postgres/postgres/commit/3e8bcc864),“由于存在多个设计问题,在此发布周期内已来不及解决”。它不会包含在版本 19 中;最早将在版本 20 中发布。示例在 beta 3 上仍可运行,因此可将其视为预览。 重塑分区表过去需要手动执行 `DETACH`、创建表、`INSERT ... SELECT`、`ATTACH` 的复杂操作。新的 `ALTER TABLE ... MERGE PARTITIONS` (https://www.postgresql.org/docs/19/sql-altertable.html) 和 `ALTER TABLE ... SPLIT PARTITION` 命令可以一条语句完成,并自动为你移动行。 ``` SELECT tableoid::regclass AS partition, * FROM metrics ORDER BY id; ALTER TABLE metrics MERGE PARTITIONS (metrics_lo, metrics_hi) INTO metrics_all; SELECT tableoid::regclass AS partition, * FROM metrics ORDER BY id; ``` ``` ┌────────────┬────┬───┐ │ partition │ id │ v │ ├────────────┼────┼───┤ │ metrics_lo │ 1 │ a │ │ metrics_hi │ 5 │ b │ └────────────┴────┴───┘ (2 rows) ALTER TABLE ┌─────────────┬────┬───┐ │ partition │ id │ v │ ├─────────────┼────┼───┤ │ metrics_all │ 1 │ a │ │ metrics_all │ 5 │ b │ └─────────────┴────┴───┘ (2 rows) ``` 拆分是合并的逆操作。此示例从一个覆盖整个范围的单个过大的 `metrics` 表开始: ``` SELECT tableoid::regclass AS partition, * FROM metrics ORDER BY id; ALTER TABLE metrics SPLIT PARTITION metrics_all INTO ( PARTITION metrics_lo FOR VALUES FROM (1) TO (4), PARTITION metrics_hi FOR VALUES FROM (4) TO (7) ); SELECT tableoid::regclass AS partition, * FROM metrics ORDER BY id; ``` ``` ┌─────────────┬────┬───┐ │ partition │ id │ v │ ├─────────────┼────┼───┤ │ metrics_all │ 2 │ x │ │ metrics_all │ 6 │ y │ └─────────────┴────┴───┘ (2 rows) ALTER TABLE ┌────────────┬────┬───┐ │ partition │ id │ v │ ├────────────┼────┼───┤ │ metrics_lo │ 2 │ x │ │ metrics_hi │ 6 │ y │ └────────────┴────┴───┘ (2 rows) ``` - 文档:ALTER TABLE (https://www.postgresql.org/docs/19/sql-altertable.html) - 提交:3e8bcc8 (https://github.com/postgres/postgres/commit/3e8bcc864) [已回滚] - 作者:Julien Rouhaud, Aleksander Alekseev

相似文章

属性图

Lobsters Hottest

PostgreSQL 文档介绍了属性图(Property Graphs),这是一种 SQL/PGQ 特性,允许使用图模式匹配语法查询关系数据,并将其定义为基于表的只读视图。

展望 Postgres 19

Hacker News Top

PostgreSQL 19 beta 引入了关键特性,如 REPACK CONCURRENTLY、分区拆分与合并以及增强的逻辑复制,为生产数据库管理提供了实用改进。

Postgres by Example

Hacker News Top

一份使用带注释的SQL示例的PostgreSQL实践入门,涵盖从基础到高级主题。

期待 PostgreSQL 19:查询提示

Hacker News Top

PostgreSQL 19 通过新的 contrib 模块 pg_plan_advice 和 pg_stash_advice 引入了查询提示功能,结束了长期以来的社区争论,并为 DBA 提供了应对优化器边缘情况的应急方案。

期待 PostgreSQL 19:是时候了

Hacker News Top

PostgreSQL 19 终于将引入原生的时态表支持,遵循 SQL:2011 标准,取代过去使用排除约束的手动方法。本文解释了当前方法的局限性以及新功能备受期待的优点。