PostgreSQL 19 交互式导览
摘要
本文提供了一个关于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
相似文章
属性图
PostgreSQL 文档介绍了属性图(Property Graphs),这是一种 SQL/PGQ 特性,允许使用图模式匹配语法查询关系数据,并将其定义为基于表的只读视图。
展望 Postgres 19
PostgreSQL 19 beta 引入了关键特性,如 REPACK CONCURRENTLY、分区拆分与合并以及增强的逻辑复制,为生产数据库管理提供了实用改进。
Postgres by Example
一份使用带注释的SQL示例的PostgreSQL实践入门,涵盖从基础到高级主题。
期待 PostgreSQL 19:查询提示
PostgreSQL 19 通过新的 contrib 模块 pg_plan_advice 和 pg_stash_advice 引入了查询提示功能,结束了长期以来的社区争论,并为 DBA 提供了应对优化器边缘情况的应急方案。
期待 PostgreSQL 19:是时候了
PostgreSQL 19 终于将引入原生的时态表支持,遵循 SQL:2011 标准,取代过去使用排除约束的手动方法。本文解释了当前方法的局限性以及新功能备受期待的优点。