来源:https://tapoueh.org/blog/2026/07/sql-improvements-in-postgresql-1118-a-personal-selection/
PostgreSQL 11--18 中的 SQL 改进:个人精选
PostgreSQL 在 2018 年至 2025 年间发布了七个主要版本,每年一个,雷打不动,每个版本的更新日志都包含 150 到 200 项用户可见的变更。每个版本的覆盖范围都很广------性能、复制、管理和安全------但每个版本也都推动了 SQL 层的发展,填补了标准中的空白,添加了缺失的功能,或清理了长期存在的粗糙边缘。在为《The Art of PostgreSQL》的新版本工作时,我不得不将它们全部编目;以下是我在重写示例时反复使用的功能精选。希望它在那个语境之外也能有所帮助------按主题组织,并附上每个功能引入的版本。
▸
本文中的所有查询都针对 Lab 运行,这是一个免费数据集捆绑包,包含 F1 数据库、地理政治数据、音乐数据等------因此你可以亲自尝试每个示例。Lab 只需一个 docker compose up 即可启动:它会启动 PostgreSQL,加载所有数据集,并提供一个在你的浏览器中运行的查询 UI Web 应用,支持 EXPLAIN 计划图,并为地理查询的 PostGIS 结果提供 SVG 渲染。
为了了解社区实际将精力投入何处,Hewlett-Packard Enterprise Japan 的 Noriyoshi Shinoda 从 9.4 版本开始,为每个主要版本发布了一份详尽的"PostgreSQL 新特性及示例"系列------每个版本都用可运行的代码示例编录了所有用户可见的变更。统计他在 PG 11--18 中的分类,可以清晰地看出贡献者的优先事项:
| 版本 | SQL | 性能 | 管理与运维 | 复制 | 安全 | 其他 |
|---|---|---|---|---|---|---|
| PG 11 | 5 | 6 | 5 | 3 | 4 | 3 |
| PG 12 | 4 | 6 | 7 | 3 | 3 | 5 |
| PG 13 | 7 | 8 | 10 | 6 | 5 | 6 |
| PG 14 | 4 | 7 | 6 | 5 | 3 | 5 |
| PG 15 | 2 | 5 | 6 | 5 | 3 | 8 |
| PG 16 | 4 | 6 | 8 | 5 | 7 | 8 |
| PG 17 | 5 | 7 | 8 | 8 | 4 | 6 |
| PG 18 | 4 | 3 | 4 | 3 | 2 | 5 |
| 总计 | 35 | 48 | 54 | 38 | 31 | 46 |
各版本按类别划分的新特性数量(Shinoda 统计)
SQL 是其中最小的类别。在整个期间,管理与运维遥遥领先,其次是性能------这反映了谁在为项目提供资金:亚马逊(Aurora)、谷歌(AlloyDB)和微软(Azure)都对 PostgreSQL 易于在云规模下操作、扩展和复制有着强烈的商业兴趣。复制与高可用性显示出最陡峭的增长轨迹,从 PG 11 的 3 个特性攀升至 PG 17 的 8 个,反映了相同的商业优先事项。本文的框架刻意以 SQL 为中心:执行策略、查询语法、数据类型、模式建模和 DML。管理、复制和身份验证方面的改进也是实实在在的;其中一些我觉得特别值得了解的内容收集在文末。
目录
- 查询执行
- 窗口帧 GROUPS 模式和 EXCLUDE(PG 11)
- 聚合与集合操作
- FETCH FIRST n ROWS WITH TIES(PG 13)
- ANY_VALUE()(PG 16)
- WITH 查询与递归
- CTE 物化提示(PG 12)
- 递归 CTE 中的 SEARCH 和 CYCLE(PG 14)
- 数据类型
- 内置 gen_random_uuid()(PG 13)
- uuidv7()------时间可排序的 UUID(PG 18)
- 多范围(Multi-Ranges)和 range_agg()(PG 14)
- 内置 C.UTF-8 排序规则(PG 17)
- 增量排序(Incremental Sort)(PG 13)
- JSON
- SQL/JSON 路径语言(PG 12)
- JSONB 下标语法(PG 14)
- IS JSON 谓词(PG 16)
- SQL/JSON 构造函数和 JSON_TABLE(PG 16--17)
- 模式建模
- 生成存储列(PG 12)
- 生成虚拟列(PG 18)
- 唯一约束中的 NULLS NOT DISTINCT(PG 15)
- 分区改进(PG 12--19)
- DML
- MERGE(PG 15),在 PG 17 中扩展
- RETURNING OLD 和 NEW(PG 18)
- 数据加载
- COPY ON_ERROR IGNORE 和 REJECT_LIMIT(PG 17 / PG 18)
- 扩展
- pg_trgm:升级到 PG 18 后重建索引
- 超越 SQL:一些值得了解的运维改进
本文涵盖的七个版本中,每个版本都添加了一项里程碑式的 SQL 功能;下表列出了该功能及其发布日期和官方发布说明中用户可见变更的大致数量。
| 版本 | 发布日期 | 约变更数 | SQL 头条 |
|---|---|---|---|
| PG 11 | 2018年10月 | 180 | 存储过程(CREATE PROCEDURE / CALL),JIT 编译,覆盖索引(INCLUDE) |
| PG 12 | 2019年10月 | 200 | 生成列,JSON 路径语言(@? / @@),CTE 内联,REINDEX CONCURRENTLY |
| PG 13 | 2020年9月 | 150 | 增量排序,B-tree 去重,内置 gen_random_uuid() |
| PG 14 | 2021年9月 | 200 | 多范围类型,JSONB 下标语法(doc'key'),CTE 中的 SEARCH/CYCLE |
| PG 15 | 2022年10月 | 180 | SQL MERGE,NULLS NOT DISTINCT,逻辑复制中的行/列过滤器 |
| PG 16 | 2023年9月 | 150 | 备库上的逻辑复制,pg_stat_io,ANY_VALUE(),IS JSON 谓词 |
| PG 17 | 2024年9月 | 180 | JSON_TABLE(),增量备份,COPY ON_ERROR IGNORE,内置 C.UTF-8 排序规则 |
| PG 18 | 2025年9月 | 150 | 虚拟生成列,uuidv7(),RETURNING OLD/NEW,异步 I/O |
变更计数为近似值(±15%),涵盖每个版本发布说明中的用户可见项目;不包括小版本错误修复。
以下是我从这七个版本中个人精选的 SQL 层面改进------我认为对编写高效 PostgreSQL 查询至关重要的功能,按主题分组。在 AI 时代,了解 PostgreSQL 能做什么比以往任何时候都更重要:如果你不知道窗口函数、范围查询或多范围数据类型的存在,你就无法要求 AI 帮你编写它们。这里有一篇关于 SQL 和 AI 的更详细的文章。
查询执行
窗口帧 GROUPS 模式和 EXCLUDE(PG 11)
ROWS 帧模式按物理行计数;RANGE 按值距离计数。PostgreSQL 11 添加了第三种模式 GROUPS,它按对等组计数------即共享相同 ORDER BY 值的连续行。这是处理排名或平局数据的自然单位。
三种窗口帧规范在 7 行结果集上展示:UNBOUNDED PRECEDING 到 CURRENT ROW(累计总和),1 PRECEDING 到 1 FOLLOWING(滑动窗口),以及 CURRENT ROW 到 UNBOUNDED FOLLOWING(剩余总和)。
PostgreSQL 11 还添加了 EXCLUDE 子句,作为同一 SQL:2011 合规性推动的一部分------EXCLUDE CURRENT ROW、EXCLUDE TIES 或 EXCLUDE GROUP------从帧中移除特定行。EXCLUDE TIES 将当前行保留在帧中,但移除共享相同 ORDER BY 值的其他行。单场比赛不适合展示------每个积分位置都有唯一的积分值,因此不会出现平局。2007 赛季的积分榜是合适的数据集:汉密尔顿和阿隆索都以 109 分结束赛季,这是 F1 历史上最接近的总冠军争夺。
sql
select surname,
sum(results.points) as season_pts,
sum(sum(results.points)) over (
order by sum(results.points) desc
rows between unbounded preceding and current row
) as running,
sum(sum(results.points)) over (
order by sum(results.points) desc
rows between unbounded preceding and current row
exclude ties
) as running_excl_ties
from results
join drivers using(driverid)
join races using(raceid)
where extract(year from races.date) = 2007
group by driverid, drivers.surname
order by season_pts desc;
姓氏 | 赛季积分 | 累计 | 排除平局累计
--------------+------------+---------+------------------
Räikkönen | 110 | 110 | 110
Hamilton | 109 | 219 | 219
Alonso | 109 | 328 | 219
Massa | 94 | 422 | 422
Heidfeld | 61 | 483 | 483
Kubica | 39 | 522 | 522
Kovalainen | 30 | 552 | 552
Fisichella | 21 | 573 | 573
Rosberg | 20 | 593 | 593
Coulthard | 14 | 607 | 607
Wurz | 13 | 620 | 620
Webber | 10 | 630 | 630
Trulli | 8 | 638 | 638
Button | 6 | 644 | 644
Vettel | 6 | 650 | 644
R Schumacher | 5 | 655 | 649
Sato | 4 | 659 | 653
Liuzzi | 3 | 662 | 656
Davidson | 2 | 664 | 658
Sutil | 1 | 665 | 659
(20 行)
汉密尔顿和阿隆索在 running_excl_ties 中都显示为 219------莱科宁的 110 加上他们自己的 109,而平局对手的 109 被排除在帧外。巴顿和维特尔同积 6 分,也出于同样的原因都显示为 644。
GROUPS 和 EXCLUDE 都是标准 SQL,现在在 PostgreSQL 中可用。
聚合与集合操作
FETCH FIRST n ROWS WITH TIES(PG 13)
LIMIT n 截断为恰好 n 行;当截断点落在平局内时,哪些行幸存是任意的。FETCH FIRST n ROWS WITH TIES 将结果扩展,包含与最后保留行平局的每一行:
sql
-- 2007 赛季所有并列第二名的车手:
select surname, sum(points) as pts
from results join races using(raceid) join drivers using(driverid)
where extract(year from races.date) = 2007
group by driverid, surname
order by pts desc
fetch first 2 rows with ties;
姓氏 | pts
-----------+-----
Räikkönen | 110
Alonso | 109
Hamilton | 109
(3 行)
莱科宁以 110 分获胜;阿隆索和汉密尔顿都得了 109 分,因此第二名的平局被包含在内,即使它使结果超过了两行。WITH TIES 是 SQL 标准语法,仅适用于 FETCH FIRST;LIMIT n 没有平局扩展。
ANY_VALUE()(PG 16)
当一列不属于 GROUP BY 键时,SQL 要求将其包装在聚合函数中。min() 或 max() 带有查询并不需要的隐式排序。any_value(col) 使意图明确:从组中返回一个任意代表,不保证顺序:
sql
select constructors.name as constructor,
any_value(drivers.surname) as a_driver, -- 代表,非 min/max
count(distinct driverid) as num_drivers,
sum(results.points) as total_points
from results
join races using(raceid)
join drivers using(driverid)
join constructors using(constructorid)
where extract(year from races.date) = 2017
group by constructorid, constructors.name
order by total_points desc
limit 5;
制造商 | 车手 | 车手数 | 总积分
------------+-----------+-------------+--------------
Mercedes | Hamilton | 2 | 503
Ferrari | Räikkönen | 2 | 385
Red Bull | Ricciardo | 2 | 270
Force India | Pérez | 2 | 133
Williams | Massa | 3 | 65
(5 行)
该功能在 SQL:2023 中标准化,从 PostgreSQL 16 开始可用。
WITH 查询与递归
CTE 物化提示(PG 12)
在 PostgreSQL 12 之前,每个 CTE 都是一个优化屏障:规划器将其物化一次并直接使用结果,即使将其内联为子查询本可以启用谓词下推。PG 12 改变了默认行为:非递归、非易变的 CTE 现在由规划器内联。
两个关键词提供显式控制:
sql
-- 强制物化(稳定快照,防止重复求值)
with accidents as materialized (
select season, count(*) as n from results
join races using(raceid) join status using(statusid)
where status = 'Accident'
group by season
)
...
-- 让规划器内联并下推谓词
with top_drivers as not materialized (
select driverid, sum(points) as pts
from results group by driverid
)
select drivers.surname, pts
from top_drivers
join drivers using(driverid)
where pts > 500
order by pts desc
limit 6;
姓氏 | pts
------------+--------
Hamilton | 2528
Vettel | 2355
Alonso | 1842
Rosberg | 1594.5
Schumacher | 1566
Räikkönen | 1498
(6 行)
这一变更悄无声息地改变了一大批现有查询的性能。当 CTE 有副作用或被多次引用时,MATERIALIZED 是正确的选择。当规划器需要看到 CTE 外部的过滤器时,NOT MATERIALIZED 是正确的选择。
递归 CTE 中的 SEARCH 和 CYCLE(PG 14)
递归 CTE 按照 PostgreSQL 工作表产生的任何内部顺序访问行。会出现两个问题:规划器按什么顺序访问每行?当图中有循环时会发生什么?
SEARCH 控制遍历顺序。SEARCH DEPTH FIRST BY col SET order_col 产生前序深度优先遍历;SEARCH BREADTH FIRST BY col SET order_col 产生逐层广度优先遍历。在 PG 14 之前,实现同样的效果需要在递归项中手动添加一个整数计数器。
CYCLE 检测并停止在已访问的行上。CYCLE col SET is_cycle USING path 添加一个 is_cycle 布尔值和一个 path 列,跟踪迄今为止看到的值序列。当递归项会重新访问一个值时,is_cycle 被设为真,该行不会反馈到下一轮。
sql
with recursive borders(isocode, name, depth) as (
select isocode, name, 0 from geoname.country where isocode = 250
union all
select c.isocode, c.name, b.depth + 1
from geoname.neighbour n
join geoname.country c on c.isocode = n.neighbour
join borders b on b.isocode = n.isocode
where b.depth < 4
)
cycle isocode set is_cycle using path
select depth, isocode, name from borders
where not is_cycle
order by depth, name;
depth | isocode | name
-------+---------+---------------
0 | 250 | France
1 | 20 | Andorra
1 | 56 | Belgium
1 | 276 | Germany
1 | 380 | Italy
1 | 442 | Luxembourg
1 | 492 | Monaco
1 | 724 | Spain
1 | 756 | Switzerland
...
(36 行)
将同一个 CTE 包装在汇总中,显示每跳可到达的不同国家数量:
sql
select depth, count(distinct isocode) as countries
from borders
where not is_cycle
group by depth
order by depth;
depth | countries
-------+-----------
0 | 1
1 | 8
2 | 19
3 | 26
4 | 43
(5 行)
从法国出发 1-4 个边境跳转可到达的国家,按深度着色。使用 PostGIS 从 geoname 数据集生成的地图。
在 PG 14 之前,等价实现需要 WHERE NOT id = ANY(path) 并手动构造数组------在相同节点对之间有多条边的图上容易出错且脆弱。
数据类型
内置 gen_random_uuid()(PG 13)
从 PostgreSQL 13 开始,gen_random_uuid() 是一个内置函数,无需安装 uuid-ossp 扩展即可生成加密随机的 UUID v4:
sql
select gen_random_uuid();
-- fbb850cc-dd26-4904-96ef-15ad8dfaff07
uuidv7()------时间可排序的 UUID(PG 18)
PostgreSQL 18 添加了 uuidv7(),用于生成时间可排序的 UUID v7。前 48 位编码当前毫秒时间戳,因此按顺序插入的行在 UUID 列上也按字典序排序------这对 B-tree 索引局部性和集群写入有显著优势:
sql
select uuidv7() as v7, gen_random_uuid() as v4;
对于使用 UUID 作为主键的新模式,如果预期有大量插入负载或范围扫描,uuidv7() 是更好的选择。
多范围(Multi-Ranges)和 range_agg()(PG 14)
数轴上的三种范围类型运算符:&&(重叠)、@>(包含)和 -|-(相邻)。
每种范围类型都获得了对应的多范围类型:int4multirange、datemultirange、tstzmultirange 等等------存储在一个列值中的有序、不重叠的子范围集合。
其核心问题是带间隙的成员资格。John Frusciante 曾三次担任红辣椒乐队的吉他手。他于 1988 年加入,1992 年离开,1998 年回归,2009 年再次离开,并于 2019 年重新加入。一个普通的 UNIQUE (band_id, composer_id) 约束会禁止第二段任期。而一个带有 GiST 排除约束的 tstzrange 列正好可以处理这种情况:
sql
create table composer.bandmember (
id serial primary key,
band_id int not null references composer.band(id),
composer_id int not null references composer.composer(id),
active tstzrange not null,
exclude using gist (band_id with =, composer_id with =, active with &&)
);
EXCLUDE USING GIST 约束表明:没有两行具有相同的 (band_id, composer_id) 可以拥有重叠的 active 范围。Frusciante 的三行不重叠记录共存;任何与三者之一重叠的第四行将在写入时被拒绝,无需触发器。
sql
select b.name as band,
c.name as member,
lower(bm.active)::date as joined,
case when upper(bm.active) = 'infinity' then null
else upper(bm.active)::date end as departed,
(upper(bm.active) = 'infinity') as current
from composer.band b
join composer.bandmember bm on bm.band_id = b.id
join composer.composer c on c.id = bm.composer_id
where b.name = 'Red Hot Chili Peppers'
order by lower(bm.active);
乐队 │ 成员 │ 加入日期 │ 离开日期 │ 当前
═══════════════════════╪══════════════════╪════════════╪════════════╪═════════
Red Hot Chili Peppers │ Anthony Kiedis │ 1983-01-01 │ │ t
Red Hot Chili Peppers │ Flea │ 1983-01-01 │ │ t
Red Hot Chili Peppers │ Chad Smith │ 1988-01-01 │ │ t
Red Hot Chili Peppers │ John Frusciante │ 1988-01-01 │ 1992-05-07 │ f
Red Hot Chili Peppers │ Dave Navarro │ 1993-09-01 │ 1998-04-01 │ f
Red Hot Chili Peppers │ John Frusciante │ 1998-04-01 │ 2009-12-31 │ f
Red Hot Chili Peppers │ John Frusciante │ 2019-12-15 │ │ t
(7 行)
@>(包含)运算符询问:这个范围是否包含这个点?一个运算符就能回答"这个人在这个时间点是成员吗?":
sql
select b.name as band,
c.name as member
from composer.band b
join composer.bandmember bm on bm.band_id = b.id
join composer.composer c on c.id = bm.composer_id
where bm.active @> '1985-07-13 12:00:00+00'::timestamptz
order by b.name, c.name;
乐队 │ 成员
═════════════════════════╪══════════════════
King Diamond │ King Diamond
King Diamond │ Mikkey Dee
Metallica │ Cliff Burton
Metallica │ James Hetfield
Metallica │ Kirk Hammett
Metallica │ Lars Ulrich
Motörhead │ Lemmy Kilmister
Motörhead │ Phil Campbell
Motörhead │ Pete Gill
Motörhead │ Würzel
Queen │ Brian May
Queen │ Freddie Mercury
Queen │ John Deacon
Queen │ Roger Taylor
Red Hot Chili Peppers │ Anthony Kiedis
Red Hot Chili Peppers │ Flea
Scorpions │ Klaus Meine
Scorpions │ Matthias Jabs
Scorpions │ Rudolf Schenker
(19 行)
日期是 1985 年 7 月 13 日------Live Aid 演唱会。John Frusciante 还未加入红辣椒;他在 1988 年 1 月才加入。Phil Taylor 也不在 Motörhead:他的第一段任期于 1984 年 5 月结束,第二段直到 1987 年 6 月才开始。