做数据库选型和升级规划,PostgreSQL 13、14、15这三个版本到底改了什么,是很多团队绕不开的问题。三个大版本虽然发布时间只隔两年,但差异一点都不小:13像是在12的底子上做局部性能补强,14把连接能力和索引维护的瓶颈一次性打通,15则引入了MERGE、逻辑复制行列过滤等能力,同时把安全默认值收紧了。如果你正准备从老版本升级,或者要在新项目里定版,这篇文章会把三者的差异拆开讲透,并且给出升级路径和避坑经验。
这里不是给你贴官方release note,而是站在实际使用的角度,把每个改动背后的瓶颈、收益和使用场景讲清楚。适合DBA、后端开发、架构师,以及所有正在纠结要不要升级、升级到哪个版本的人。
1. 三个版本,先看整体差异和选型思路
1.1 版本时间线与定位
PostgreSQL 13于2020年9月发布,14于2021年9月发布,15于2022年10月发布,节奏上保持了一年一个大版本。用一句话概括三个版本各自的性格:13是优化型,把12时代遗留的索引膨胀、vacuum低效、UUID依赖扩展等问题集中处理了一遍;14是性能型,重点解决大连接数场景的内存开销、索引WAL放大、子事务性能这些偏底层的硬骨头;15是能力型,把SQL标准特性、逻辑复制的灵活度、安全默认值拉到了一个新的高度。
1.2 一张表看清版本之间的核心差异
| 功能方向 | PostgreSQL 13 | PostgreSQL 14 | PostgreSQL 15 |
|---|---|---|---|
| 索引空间 | B-tree索引去重 | B-tree WAL写入量下降 | 唯一索引支持NULLS NOT DISTINCT |
| 查询性能 | 增量排序 | 聚合与PL/pgSQL提速 | 内存排序最高提升约2倍 |
| 并发能力 | 并行vacuum | 大连接数内存优化 | 并行vacuum进一步成熟 |
| SQL能力 | gen_random_uuid()内置 | JSONB下标、multirange | MERGE、标准正则函数 |
| 逻辑复制 | 分区表发布/订阅 | 基础能力增强 | 行过滤、列列表 |
| 安全与默认值 | —— | —— | public schema权限收紧、wal_compression默认开启 |
从这个表能看出,版本之间不是单纯堆功能,而是有明显侧重。如果还在12或更早,跳到14或15的收益会非常大;如果已经在13,至少升到14才能拿到并发方面的核心收益;新项目我通常建议直接选15或更新的版本,省得后期再折腾一次升级。
2. PostgreSQL 13:稳定底座上的性能补强
2.1 B-tree索引去重:让索引“减脂”
B-tree索引去重是13里含金量最高的一项改动。在12及更早版本中,每次插入重复键值时,B-tree的叶子节点都会为每个键值单独存储一条索引项,键值本身要重复写一遍。假如某张表的某列只有几个枚举值,索引里就会堆满完全相同的键值,空间浪费严重,索引膨胀,扫描和写入都变慢。
13引入的去重机制会把同一个键值合并成一个条目,后面挂一个tid列表。这样重复键值只需要存储一次,索引体积大幅下降,插入时的WAL写入量也随之减少。官方发布说明和不少社区测试都提到,重复度高的列上,索引体积减少一半以上是很常见的。要注意的是,deduplicate_items参数默认开启,对于唯一性很高的索引没有明显收益,但也不会带来什么副作用,一般不需要手动去动它。
2.2 增量排序:让小范围排序省下大内存
增量排序解决的是一个很隐蔽的浪费:查询计划在拿到已经部分有序的输入后,还会按照老办法sort整个结果集。比如已经通过索引扫描让结果按a字段有序,再需要ORDER BY a, b时,老逻辑依然会从头到尾排一遍。
13的Incremental Sort能识别这种预排序状态,把结果集切分成多个已经有序的批次,只对剩下的键做排序,然后逐个批次合并输出。它的内存占用比全量排序小,响应时间也更短。在EXPLAIN里看到Incremental Sort节点时,说明优化器已经认定它能降低成本。对ORDER BY ... LIMIT n这类查询,效果尤其明显,因为排序过程可以在提前拿到足够行数时直接截断,不必处理完整结果集。
2.3 并行vacuum和崩溃恢复加速
vacuum对PostgreSQL来说几乎等同于日常保洁。13之前,大表的vacuum只能由一个后台进程扫全表,更新频繁的大表经常出现清理不过来、表膨胀越来越严重的情况。13开始支持VACUUM (PARALLEL n),也可以配置autovacuum参数让多个worker并行扫描堆表上的dead tuple,vacuum耗时明显缩短。对7x24跑着的业务,这是实打实的维护收益。
另一个偏底层的优化是崩溃恢复阶段的WAL重放。13改进了重放过程中的行查找逻辑,恢复时间更短。虽然平时感受不到,但一旦真出现宕机,少几十分钟恢复时间就是救命级别的差异。
2.4 内置UUID和分区表逻辑复制
开发人员最直接的感受是gen_random_uuid()终于内置了。以前要生成UUID,得先CREATE EXTENSION pgcrypto,否则就只能另找方案。13之后直接SELECT gen_random_uuid()就能用,少一个依赖,部署简化很多。
逻辑复制方面,13允许直接把分区表当作发布/订阅对象。之前只能逐个发布子分区,运维非常繁琐。现在建了分区表,发布订阅可以以根分区为单位,增删子分区自动同步。对分库分表改造、多区域数据汇聚这类场景非常实用。
3. PostgreSQL 14:并发、索引与开发体验的全面升级
3.1 大连接场景的底层重构
14最值得关注的是连接数扩展。很多团队在连接数超过两三千时,会明显看到内存飙升、上下文切换加剧,甚至触发OOM。14通过动态共享内存和更紧凑的数据结构,显著降低了每个后端进程的固定内存开销。社区里的实测反馈是,相同配置下能支撑的连接数明显上了一个台阶,尤其是空闲连接的消耗下降非常明显。
这里要说明一点:连接数的优化不等于可以无限加连接。连接池依然是标准做法,14解决的是资源上限问题,让max_connections从“不敢调大”变成“可以调大”,但必须结合shared_buffers、work_mem这些参数统筹考虑,别把内存全吃光。升级到14之后,我习惯先调低单个连接的预估内存,再逐步压测连接数上限,而不是直接把max_connections翻倍。
3.2 B-tree写放大削减与vacuum增强
14在索引维护方面的升级同样重要。B-tree索引在做删除和更新时,会生成大量WAL日志,14把这一块的写入量大幅降了下来。官方发布说明里明确提到了这个变化,实际效果是高频更新表的WAL归档和从库网络传输压力都小了不少,磁盘寿命也能省一点。
vacuum方面,13的并行vacuum还只针对堆表扫描,14把并行能力延伸到了索引清理阶段。大表的vacuum整体更快,这也间接缓解了表膨胀问题。如果你管理的库有大量频繁update的表,从13升到14之后,autovacuum的工作压力会明显减轻。
3.3 子事务与PL/pgSQL性能
嵌套事务(SAVEPOINT)用得多的话,14会带来一个不小的性能惊喜。之前的版本里,子事务记录是线性扫描,一次大事务里来回开几千个子事务,CPU开销很夸张。14把子事务查找改成哈希查找,避免了之前最主要的性能瓶颈。批量任务里大量使用savepoint做部分回滚的,升级后体感很明显。
PL/pgSQL函数的执行性能也有提升。服务端大量使用存储过程或复杂表达式函数时,能感知到脚本运行时间变短。对存量系统来说,这不需要改代码,升级后白拿的性能优化是最舒服的。
3.4 JSONB下标、multirange与pipeline模式
开发体验层面,14引入了JSONB下标赋值语法。以前更新JSONB字段要写jsonb_set,SQL很啰嗦:
UPDATE users SET profile = jsonb_set(profile, '{nickname}', '"foo"');14起可以直接写成:
UPDATE users SET profile['nickname'] = '"foo"';JSONB用得多的系统,代码可读性提升非常明显。
multirange类型适合表达一段不连续的区间集合,比如排班表的多个空闲时段、价格区间组合。14开始支持multirange类型以及相关的范围操作符,这类数据建模变得更自然。
libpq管道模式(pipeline mode)是给底层客户端用的,允许连续发送多个查询,不等前一个响应就发下一个。对短查询密集、网络往返开销大的场景,比如批量取数、循环写入,能有效减少客户端和服务端的交互次数。
4. PostgreSQL 15:SQL能力与安全默认的拐点
4.1 MERGE:一条SQL搞定增量同步
15里最亮眼的功能是MERGE,它让PostgreSQL在SQL标准的一致性上往前走了一大步。过去的UPSERT只能写INSERT ... ON CONFLICT,做简单的新增或更新还行,但遇到“匹配了更新,匹配不上则插入,或者反过来删除”这类复杂逻辑,就没法一条SQL完成。
一个典型例子是数据同步:
MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED AND s.active = false THEN DELETE WHEN MATCHED THEN UPDATE SET t.value = s.value WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value);这对数据同步、ETL、类CDC任务都是好消息,一个语句就能完成增量处理。但要注意,MERGE和ON CONFLICT不是替代关系。简单UPSERT场景下,ON CONFLICT依然更轻量、更高效,MERGE适合条件分支多的场景,别一上来就把所有写法都换成MERGE。
4.2 public schema权限收紧:升级最容易踩的迁移坑
15把public schema的默认权限改掉了。在此之前,新建数据库后,PUBLIC角色默认对public schema有CREATE和USAGE权限,任何人都能在public schema里建表。15开始,public schema的CREATE和USAGE权限默认只授给数据库owner。
如果是从13或14用pg_upgrade升上来的,旧库的授权还在,通常不会立即出问题。但如果是新建的库,或者用新版本做的逻辑导入,应用账号就会遇到permission denied for schema public。在实际项目里,这往往表现为升级后应用突然建不了表、加不了字段,排查半天才发现权限问题。
解决办法是显式授权:
GRANT CREATE, USAGE ON SCHEMA public TO myapp;多人共用一个库的项目,升级前一定要梳理清楚账号权限,别等应用报错再救火。
4.3 逻辑复制增加行过滤与列列表
15的逻辑复制有两个非常实用的功能:行过滤和列列表。以前逻辑复制只有整表复制一个选项,想只同步某些行或者某些列,只能靠订阅端触发器或另建视图,复杂度很高。
15起可以在发布端直接定义规则:
CREATE PUBLICATION pub_orders FOR TABLE orders (id, status, create_time) WHERE (status IN ('paid', 'shipped'));这样订阅端只收到符合条件并且需要的字段,数据量、网络传输和订阅端存储都会省很多。15还支持在发布端对分区表设置行过滤,对分库分表场景更友好。要注意的是,行列过滤是在发布端生成的,所以发布端和订阅端的版本需要都支持这个特性,跨大版本混合使用时需要提前确认。
4.4 排序与COPY性能提升:大数据量处理更香
15在性能上也有官方明确提到的两点。第一,内存排序性能提升,官方说最多能到2倍。带LIMIT的排序查询、大数据量排序场景最能感受到差别。第二,COPY的导入性能提升明显,有测试显示大约能提升30%。
实际效果和硬件、数据分布相关,但每天要灌大批量文件的场景,即便只提升20%,对跑批作业的调度排班也有价值。升级后建议把最关键的批量导入SQL用EXPLAIN重新跑一遍,确认执行路径变化。COPY依然是二进制版本之间公用的通用导入方式,这个性能提升能让数据初始化、灾备恢复都跟着受益。
4.5 正则函数、NULLS NOT DISTINCT与监控细节
15增加了一批SQL标准正则函数,包括regexp_count、regexp_instr、regexp_like、regexp_substr等。以前要在SQL里做文本匹配、提取和计数,需要写substring加正则或自定义函数,现在可以直接用函数实现,比如:
SELECT regexp_count('abc123def456', '[0-9]+'); SELECT regexp_instr('abc123def456', '[0-9]+'); SELECT regexp_like('abc', 'a.c');NULLS NOT DISTINCT允许在唯一约束或唯一索引里把NULL视为“相同值”。以前一个列如果有多个NULL,唯一索引不会有任何冲突,15之后可以显式声明:
CREATE UNIQUE INDEX idx_account_no ON accounts (account_no) NULLS NOT DISTINCT;对业务上要求某个可选字段必须唯一的场景,这是很实用的补丁。
监控方面,15新增了pg_stat_io视图,能看到不同后端类型的读写次数和耗时,排查IO瓶颈时比之前只能看pg_stat_bgwriter细致得多。另外wal_compression参数在15里默认开启,对WAL体积大的系统能省一部分磁盘和网络开销,代价是增加少量CPU开销。如果机器CPU本身就很紧张,升级后要注意观察一下这个参数带来的变化。
5. 从13到15:升级路径与实战避坑
5.1 直接升级还是分步升?
如果源库是13版本,想到15,官方pg_upgrade是支持直接跨版本升级的,也就是13到15一步到位。不过中间隔了一个大版本,我建议至少在测试环境完整跑一遍升级流程再上生产。
pg_upgrade的典型流程是:用新版本initdb初始化一个空数据目录,然后通过新旧两个版本的bin目录把数据文件直接转换过去。速度快、停服窗口短。如果愿意,可以加上--link选项用硬链接方式,升级期间几乎不复制数据文件,但升级后旧数据目录不能动。
# 初始化新库 /usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/15/data # 检查是否可升级 /usr/lib/postgresql/15/bin/pg_upgrade \ -b /usr/lib/postgresql/13/bin \ -B /usr/lib/postgresql/15/bin \ -d /var/lib/postgresql/13/data \ -D /var/lib/postgresql/15/data \ --check # 正式升级,加 --link 可硬链接加速 /usr/lib/postgresql/15/bin/pg_upgrade \ -b /usr/lib/postgresql/13/bin \ -B /usr/lib/postgresql/15/bin \ -d /var/lib/postgresql/13/data \ -D /var/lib/postgresql/15/data \ --link另一种方式是逻辑复制滚动升级:目标机初始化15实例,源库创建发布,目标库订阅,全部追上后切换流量。这个方案的优点是停服时间短,还能顺带做数据校验;缺点也很明显,逻辑复制不复制序列,某些DDL和特殊类型需要额外处理,准备成本高。对严格要求不停服的业务,可以优先考虑这条路。
5.2 升级前必做的检查清单
我会在升级前按这个顺序过一遍:
- 备份:
pg_dump做逻辑备份,pg_basebackup做物理备份,尽量都留一份。备份不是流程仪式,是最后一道保险。 - 小版本对齐:源库先升到13最新的小版本,避免带着旧小版本的已知问题做跨大版本升级。
- 插件检查:第三方扩展和老的C函数是不是兼容目标版本。比如某些版本不支持的插件,在新版本编译不过,就要提前替换或评估影响。
- 权限检查:尤其关注15的public schema变化。虽然升级时旧库授权还在,但新建逻辑库时一定会碰到。
- 查询回归:把核心业务的慢查询日志、批处理SQL收集起来,在staging环境用新版本重跑,对比执行计划和耗时。
pg_upgrade --check:先跑一次检查模式,确认没有明显的文件权限、配置或插件冲突。
这条清单看着繁琐,但它能避免绝大多数升级翻车。我见过太多次升级失败是因为插件不兼容或者权限变化,而不是版本本身的bug。
5.3 场景化踩坑记录
几个我在实际维护中遇到过、也帮别人排查过的问题:
场景一:升级后应用无法在public schema建表。新版本新建的库,public schema默认只有owner能写。应用账号用普通用户连接后报permission denied for schema public。处理方式就是前面说的,显式GRANT,最好在数据库初始化脚本里就写好,别依赖默认值。
场景二:逻辑复制行列过滤没生效。如果你用15的订阅端去订阅13或14的发布端,行列过滤是用不了的,因为过滤在发布端生成。想让过滤生效,发布端必须升级到15。跨版本混用时不要只看订阅端的功能列表,要两端都确认版本。
场景三:并行vacuum在资源紧张的环境打满CPU。13开始支持并行vacuum后,如果机器本身CPU核数多但磁盘IO弱,配合默认autovacuum配置可能把资源吃满。建议在低峰期先观察,可以把autovacuum_max_workers或autovacuum_vacuum_cost_delay调整到合适值。
场景四:执行计划变化导致慢查询。版本升级后,因为统计信息、成本参数、优化器行为变化,同一条SQL可能走不同执行计划。升级前收集慢查询,升级后逐个对比,是必须的功课。别上了生产才发现某个SQL从索引扫描变成了全表扫描。
6. 写在后面的选型经验
6.1 按场景选版本
是不是必须追上最新版本?不一定。如果业务稳定、负载不高、代码里还在大量使用老式写法,13完全够用。但如果是新项目,我会直接选择15或更新的版本,因为MERGE、逻辑复制的行列过滤这些能力,后期靠补丁是补不来的,这次不升,早晚要升。
如果是因为性能和并发要升级,14是一个很明显的水岭。从13升到14的收益,通常比12升到13更突出,大连接数、B-tree索引维护、子事务这三块都是底层硬优化。
6.2 我测试版本差异的小习惯
每一个版本我都会准备一个测试实例,把三样东西跑一遍:核心业务的慢查询、批量导入任务、一张大表的vacuum耗时。对比执行计划、WAL生成量、导入耗时和vacuum时长。版本差异这东西,官方文档只能给方向,硬件和业务形态不同,最终都得用自己的数据说话。
最后一件事:升级窗口一定要避开业务高峰期,哪怕staging环境测过一百遍,生产环境的版本切换也要预留回滚时间。数据库版本升级不是安装一个新软件包,它影响的是所有往数据库里写数据的系统。宁可慢一点,也别在错误的时间窗口冒险。