PostgreSQL 升级后变慢:统计信息之外的 4 个原因(JIT、并行、扩展、参数)——pg_upgrade 不搬 postgresql.conf,也不升扩展
PostgreSQL 14 升到 17 后变慢,ANALYZE 只修了统计信息这一半。我们在测试库(自造数据,300 万行)上实测:pg_upgrade 2.36 秒完成,但旧集群的 work_mem=64MB / random_page_cost=1.1 / jit=off / max_parallel_workers_per_gather=4 全部回到 initdb 默认 4MB / 4 / on / 2,pg_stat_statements 停在 1.9(17 默认 1.11)。同参数下 6 条对照查询 17.11 比 14.24 快 4–11%;但参数回到默认后,排序从 1518ms 变 2480ms、单条并行查询从 38ms 变 61ms。JIT 在大查询里反而快 17%、中等 join 慢 2–9%;random_page_cost 在本次测试中没有改变计划。附切换前只读核对 SQL。
测试库实测(PostgreSQL 14.24 → 17.11,自造数据 t 表 300 万行 283MB,8 vCPU,2026-11-09):常见说法是「升级完 ANALYZE 一下就好」,我不同意——ANALYZE 只修统计信息,pg_upgrade 不迁移 postgresql.conf,也不升级扩展。我们的判断:升级变慢多数是配置漂移,不是新版本变慢;切换前先 diff 新旧两边的 pg_settings 和 pg_extension,再 ANALYZE、再放流量。4 个原因实测:① 参数:work_mem 4MB 时排序走 external merge(磁盘 149784kB),64MB 时内存 top-N,快 35–39%;但哈希聚合 64MB 反而慢 10–26%。② 并行:默认 2 个 worker 比旧集群的 4 个慢约 60%(单条),16 并发下并行不加分。③ JIT:40 个 CASE 的大查询 jit=on 快 17%,2–6 表 join 慢 2–9%。④ 扩展:pg_stat_statements 停在 1.9(17 默认 1.11),要手动 ALTER EXTENSION UPDATE;预加载 pg_stat_statements / auto_explain 的开销三轮复测差异在噪声范围内,不下结论。
适用对象
- 我的判断:PostgreSQL 升级后变慢,多数是配置漂移而不是新版本变慢。同一组参数下 6 条对照查询 17.11 都比 14.24 快 4–11%(唯一例外是 work_mem=64MB 的哈希聚合,17.11 慢 9%);更大的变慢来自 pg_upgrade 后回到 initdb 默认的参数。
- pg_upgrade 搬数据,不搬 postgresql.conf:测试库旧集群 work_mem / random_page_cost / jit / max_parallel_workers_per_gather = 64MB / 1.1 / off / 4,升级后新集群是 4MB / 4 / on / 2。官方文档原话是可能需要手动调整新集群的 postgresql.conf 和 postgresql.auto.conf 去匹配旧集群。
- ANALYZE 只修统计:升级后 pg_stats 0 行,同一条查询 12.47ms、估算 11031 行;vacuumdb --analyze-in-stages 0.53 秒后 2.08ms、估算 1842 行。这一步必须做,但做完参数和扩展仍是旧问题。
- 扩展也不会自动升级:pg_stat_statements 停在 1.9,新版本默认 1.11,要手动 ALTER EXTENSION pg_stat_statements UPDATE;PG17 把 blk_read_time 改名为 shared_blk_read_time,监控 SQL 要一起改。
- JIT 不是一律变慢:大查询(cost 156 万,40 个 CASE)jit=on 快 17%;中等 join(cost 14 万–43 万)慢 2–9%。如果旧集群关了 JIT,升级后它会被默认打开,要按自己的慢查询验证再决定。
- random_page_cost 4 vs 1.1 在本次测试的 3% / 8% / 15% 选择率下都没有改变计划(都是 Bitmap Heap Scan),耗时差异方向不一致,不能据此说它是升级变慢的原因。
「ANALYZE 一下就好」只修了一半:pg_upgrade 搬数据,不搬配置,不升扩展
写这篇前我列了 5 个角度:给一年前的自己的建议、常见说法哪里不对、换个定义、对企业为什么重要、用搬家类比。最后选了第二个,因为它最能直接改变升级 runbook 的顺序。
我先做了一次真实的 pg_upgrade(14.24 → 17.11,--copy 模式,374MB):2.36 秒完成,输出最后一句是「Optimizer statistics are not transferred by pg_upgrade」,提示跑 vacuumdb --analyze-in-stages。它没提示的是另外两件事:新集群用的是 17 自己 initdb 出来的 postgresql.conf,旧集群里调过的参数一个都没带过来;扩展的 SQL 对象版本原样保留,不会自动升到新版本。
换个定义更好理解:升级后变慢 = 配置漂移,不是新版本变慢。下面每一节都先给「旧集群值 → 升级后默认值」,再给这一项在测试库上的实测影响。
| 项目 | PG14.24 旧集群 | pg_upgrade 后的 PG17.11 | ANALYZE 能修吗 |
|---|---|---|---|
| pg_stats 行数(app.orders) | 4 | 0 | 能:vacuumdb --analyze-in-stages 0.53 秒 |
| 同一条查询耗时 / 估算行数 | — | 12.47ms / 11031 行 → ANALYZE 后 2.08ms / 1842 行 | 能 |
| work_mem | 64MB | 4MB(默认) | 不能 |
| random_page_cost | 1.1 | 4(默认) | 不能 |
| jit | off | on(默认) | 不能 |
| max_parallel_workers_per_gather | 4 | 2(默认) | 不能 |
| pg_stat_statements 扩展版本 | 1.9 | 1.9(17 可用 1.11) | 不能:要 ALTER EXTENSION … UPDATE |
| pg_trgm / btree_gin | 1.6 / 1.3 | 1.6 / 1.3(无需升级) | — |
同一组参数下,17.11 并不比 14.24 慢
要说「是配置漂移不是新版本」,先要排除新版本本身变慢。同一台机器、同一份数据、同一组参数(jit=off,并行 0,除特别说明外),每条查询 3–5 次取中位数:
也有例外:work_mem=64MB 的哈希聚合,17.11 是 2470.4ms,14.24 是 2256.5ms,17 慢 9%(下一节细说)。这组数只说明在这台测试机、这几类查询上 17.11 总体没有变慢,不代表所有查询都会更快;你的业务 SQL 仍要在升级前后各跑一遍回归。
| 查询 | 14.24 | 17.11 | 变化 |
|---|---|---|---|
| 全表聚合(cost 约 7.4 万,不触发 JIT) | 192.5 | 172.8 | −10% |
| 分组聚合(cost 约 16.6 万) | 797.4 | 747.4 | −6% |
| 40 个 CASE 的宽聚合(cost 约 156 万) | 5336.2 | 4768.4 | −11% |
| 2 表 join(work_mem 64MB) | 519.1 | 497.8 | −4% |
| 6 表 join(work_mem 64MB) | 2053.4 | 1929.3 | −6% |
| 单条扫描,并行 0 | 185.6 | 167.2 | −10% |
参数回到默认:work_mem 64MB → 4MB,排序掉到磁盘;但调大不是对所有查询都好
ORDER BY d LIMIT 20 万这类大排序最直接:work_mem=4MB 时计划里是 Sort Method: external merge,Disk 149784kB;64MB 时变成内存里的 top-N heapsort,临时文件写入 0 块。14.24 上 2538.6 → 1645.5ms,17.11 上 2479.6 → 1517.6ms。换句话说,如果旧集群是 64MB,升级后不改回来,这条排序在 17 上会从 1518ms 变成 2480ms,慢 63%,而 ANALYZE 对它没有任何作用。
但我要如实写另一半:同一张表上的哈希聚合(GROUP BY 高基数列),work_mem 调大反而更慢——14.24 上 2047.0 → 2256.5ms,17.11 上 1955.5 → 2470.4ms。批次数从 129 降到 9(14)、65 降到 5(17),但 64MB 时仍然溢出磁盘。所以我的建议不是「把 work_mem 调大」,而是「把旧集群验证过的值带回来,再用自己的慢查询验证」;每个排序/哈希节点都可能用到 work_mem,并发高时总内存会成倍放大。
random_page_cost 我也测了:4 vs 1.1,在约 3%、8%、15% 三种选择率下,14 和 17 的计划全部是 Bitmap Heap Scan,没有翻转。耗时有的快有的慢(如 14.24 的 8% 区间 90.9 vs 121.3ms,17.11 同区间 88.1 vs 83.9ms),方向不一致,数据又都在缓存里,我不把它算作本次测试里的变慢原因。它仍然应该带回旧值,只是别指望它解释大部分变慢。
| 查询 / 版本 | 4MB | 64MB | 计划变化 |
|---|---|---|---|
| 大排序 / 14.24 | 2538.6 | 1645.5 | external merge Disk 149784kB → 内存 top-N |
| 大排序 / 17.11 | 2479.6 | 1517.6 | external merge Disk 149768kB → 内存 top-N |
| 哈希聚合 / 14.24 | 2047.0 | 2256.5(更慢) | 129 批 → 9 批,仍溢出 |
| 哈希聚合 / 17.11 | 1955.5 | 2470.4(更慢) | 65 批 → 5 批,仍溢出 |
并行度回到 2:单条查询慢约 60%,但高并发下并行本来就不加分
max_parallel_workers_per_gather 0 / 2 / 4,同一条扫描:14.24 是 185.6 / 70.7 / 44.9ms,17.11 是 167.2 / 61.2 / 38.0ms。旧集群设的是 4,升级后默认 2,17.11 上这条查询从 38.0ms 变成 61.2ms——新版本更快,但因为少了 2 个 worker,反而比升级前的 14(44.9ms)慢了 36%。这类变慢最容易被误判成「17 有性能回退」。
反过来也要看:pgbench 16 个客户端并发跑同一类查询(8 vCPU),并行 0 vs 2,14.24 是 50.2 vs 47.7 TPS,17.11 是 55.4 vs 54.7 TPS——CPU 已经被并发占满时,并行不加分甚至略降。所以判断标准是看你的慢查询是「单条大查询」还是「大量并发小查询」,而不是一律调大。
JIT 被默认打开:大查询反而快 17%,中等 join 慢 2–9%
常见说法是「升级后变慢就关 JIT」。我测下来不能这么一概而论。JIT 是否触发看查询总估算成本:超过 jit_above_cost(默认 10 万)才编译,超过 50 万才做内联和优化。
40 个 CASE 的宽聚合(cost 约 156 万,触发内联和优化):jit=on 时 14.24 从 5336.2 降到 4434.0ms,17.11 从 4768.4 降到 3935.6ms,都快了 17%,JIT 编译本身花了 214–216ms。2 表和 6 表 join(cost 14 万–43 万,只做表达式编译):14.24 上 519.1 → 560.1ms(+8%)、2053.4 → 2124.0ms(+3%);17.11 上 497.8 → 508.7ms(+2%)、1929.3 → 2105.0ms(+9%)。JIT 编译耗时 18–47ms;17.11 6 表 join 慢了 176ms,比编译时间本身大,说明这组 3 次中位数里也有波动。cost 7.4 万的小查询根本不触发 JIT,开关前后 192.5 vs 191.1ms。
我的处理办法:不要全局 jit=off 了事。先看 pg_stat_statements 里 jit_functions、jit_generation_time 等列(扩展升到 1.10+ 才有),找到「cost 在十几万到几十万、执行只有几百毫秒」的那类查询,它们最容易被 JIT 拖慢;要么提高 jit_above_cost,要么只对相关角色或会话 SET jit = off。旧集群如果本来就是 jit=off 且运行稳定,切换时先保持 off,升级稳定后再单独评估。
| 查询(估算 cost) | 14.24 off → on | 17.11 off → on | JIT 编译耗时 |
|---|---|---|---|
| 宽聚合 40 个 CASE(156 万) | 5336.2 → 4434.0(−17%) | 4768.4 → 3935.6(−17%) | 214–216ms,含内联+优化 |
| 分组聚合(16.6 万) | 797.4 → 774.0 | 747.4 → 725.2 | 5.5–5.6ms |
| 2 表 join(14.2 万) | 519.1 → 560.1(+8%) | 497.8 → 508.7(+2%) | 18.0–19.6ms |
| 6 表 join(43.1 万) | 2053.4 → 2124.0(+3%) | 1929.3 → 2105.0(+9%) | 25.7–46.7ms |
| 全表聚合(7.4 万,不触发) | 192.5 → 191.1 | 172.8 → 171.0 | 0 |
扩展不会跟着升级:pg_stat_statements 停在 1.9;预加载扩展的开销本次测试在噪声范围内,不下结论
pg_upgrade 后在新集群查 pg_extension:pg_stat_statements 是 1.9,而 17 自带的默认版本是 1.11;pg_trgm 1.6、btree_gin 1.3 两边相同,不用升。执行 ALTER EXTENSION pg_stat_statements UPDATE 后变成 1.11。pg_upgrade 不会替你做这一步,扩展停在旧版本时,新版本才有的列和函数用不了。
升级扩展时要一起改监控 SQL:PostgreSQL 17 发布说明写明 pg_stat_statements 把 blk_read_time / blk_write_time 改名为 shared_blk_read_time / shared_blk_write_time。按旧列名写的 Grafana 面板或巡检脚本,在 ALTER EXTENSION 之后会直接报列不存在。
很多人会怀疑「是不是 shared_preload_libraries 里的扩展拖慢了新版本」。我用 pgbench -S(只读主键查询,scale 20,8 客户端,每轮 15 秒)在 14.24 和 17.11 上各测 6 种预加载配置,每种 3 轮。首轮单次测试时 14.24 开 pg_stat_statements 低了 20%、17.11 反而高了 7%,明显是噪声,所以又补了三轮复测。
三轮结果:pg_stat_statements 默认配置的中位数和不加载扩展相比,14.24 是 396.4k vs 391.5k tps,17.11 是 396.5k vs 398.6k tps,差距在 1% 左右;track_planning 和 auto_explain 各配置的中位数在 −15% 到 +4% 之间,但同一配置 3 轮之间最大相差约 3 成(如 14.24 只加载 auto_explain 时 303.9k–401.4k),而且不单调——17.11 上开了 log_analyze + log_timing=off 反而比只加载 auto_explain 更快。所以本次测试差异在噪声范围内,不下结论。能确定的只有一点:每条查询约 0.02ms 的只读负载下,pg_stat_statements 默认配置没有测出可见开销。
生产上我的做法不变:pg_stat_statements 保留(排查升级后变慢离不开它),auto_explain 的 log_analyze 先配 log_timing=off 和 sample_rate,在预发环境用自己的负载压一轮再上。
| 配置 | PG14.24 | PG17.11 |
|---|---|---|
| 不预加载任何扩展(基线) | 391.5k / 397.6k / 384.8k(中位 391.5k) | 410.0k / 398.6k / 390.2k(中位 398.6k) |
| pg_stat_statements(默认 track=top) | 342.4k / 416.0k / 396.4k(中位 396.4k) | 396.5k / 388.0k / 401.3k(中位 396.5k) |
| + track_planning=on | 351.9k / 405.7k / 369.4k(中位 369.4k) | 388.8k / 371.4k / 347.3k(中位 371.4k) |
| + auto_explain,log_min_duration=10s(不触发) | 303.9k / 401.4k / 334.2k(中位 334.2k) | 350.4k / 366.3k / 363.4k(中位 363.4k) |
| + log_analyze=on、log_timing=on | 334.7k / 358.4k / 378.6k(中位 358.4k) | 370.3k / 347.1k / 391.7k(中位 370.3k) |
| + log_analyze=on、log_timing=off | 360.3k / 344.7k / 364.7k(中位 360.3k) | 415.3k / 414.0k / 399.7k(中位 414.0k) |
升级后连接打满的报错措辞也变了,按原文匹配的告警会失效
同一批测试实例上,我把 max_connections 设成 8、superuser_reserved_connections 设成 2 来触发连接打满:14.24 上普通角色连到第 7 个时报 remaining connection slots are reserved for non-replication superuser connections;17.11(另设 reserved_connections=2)上普通角色连 4 个后报 remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role。全满时两版都是 sorry, too many clients already。
如果你的日志告警是按 14 的原文匹配的,升级后连接打满不会再触发。改成按 SQLSTATE 53300 匹配最稳,细节见 PostgreSQL 报错速查里的 sorry, too many clients already 页。
切换前 4 步:diff 参数、diff 扩展、ANALYZE、用慢查询回归
下面的 SQL 都是只读的,在旧集群和新集群各跑一次,导出后 diff。第一条把所有不是默认值的参数列出来:SELECT name, setting, unit, source FROM pg_settings WHERE source NOT IN ('default','override') ORDER BY name;。第二条看扩展:SELECT e.extname, e.extversion AS installed, a.default_version FROM pg_extension e JOIN pg_available_extensions a ON a.name = e.extname ORDER BY 1;(installed 和 default_version 不同的就是待升级)。
参数要区分「旧值是有意调过的」和「只是历史遗留」:我的做法是先原样带回,切换稳定后再逐个评估,而不是在切换当天同时改参数和升版本——两件事叠在一起,一旦变慢就分不清是谁的问题。
我建议的升级切换顺序
- 01
1
diff pg_settings
旧集群 source 非 default 的参数逐条带到新集群 postgresql.conf / postgresql.auto.conf,重点 work_mem、random_page_cost、jit、max_parallel_workers_per_gather、shared_preload_libraries。
- 02
2
diff pg_extension
installed ≠ default_version 的扩展列出来,按顺序 ALTER EXTENSION … UPDATE,同时改用到新旧列名的监控 SQL。
- 03
3
ANALYZE 后再放流量
vacuumdb --all --analyze-in-stages;本次 374MB 测试库 0.53 秒,生产按库大小预留窗口。
- 04
4
用自己的慢查询回归
pg_stat_statements Top-20 在新旧两边各跑 EXPLAIN (ANALYZE, BUFFERS),对比计划和 JIT、并行 worker 数。
免费 · 留邮箱领检查单
PostgreSQL 14 停更升级检查单(Excel 可直接打开)
- PG14 2026-11-12 停更,升 17 / 18 前后 19 项检查:只读预检、--check、回滚、补统计、回归
- 每项写清怎么查、通过标准、不处理会怎样(附测试库实测数字)
- 配合站上的只读预检 SQL 使用,每个业务库各跑一次
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
PostgreSQL 升级后变慢,跑了 ANALYZE 还是慢,可能是什么原因?
常见是 4 个和统计信息无关的原因:pg_upgrade 不迁移 postgresql.conf,work_mem、random_page_cost、jit、并行度回到新版本默认值;扩展(如 pg_stat_statements)停在旧版本。本次测试库上旧集群 64MB / 1.1 / off / 4,升级后变成 4MB / 4 / on / 2。
pg_upgrade 会迁移 postgresql.conf 吗?
不会。新集群使用自己 initdb 生成的配置文件,官方文档提示可能需要手动调整新集群的 postgresql.conf 和 postgresql.auto.conf 去匹配旧集群。
升级后要不要关掉 JIT?
不要一刀切。本次测试库上 cost 156 万的大查询开 JIT 快 17%,cost 14 万–43 万的 join 慢 2–9%。先找出被拖慢的那类查询,再提高 jit_above_cost 或按角色/会话关闭。
pg_upgrade 后扩展需要手动升级吗?
需要。本次 pg_stat_statements 升级后仍是 1.9,17 默认 1.11,执行 ALTER EXTENSION pg_stat_statements UPDATE 才变成 1.11。PG17 还把 blk_read_time 改名为 shared_blk_read_time,监控 SQL 要一起改。
PostgreSQL 17 比 14 慢吗?
本次测试库上同一组参数下,6 条对照查询 17.11 都比 14.24 快 4–11%,例外是 work_mem=64MB 的哈希聚合慢 9%。升级后更大的变慢来自参数回到默认等配置漂移,你的业务查询仍需要回归验证。
random_page_cost 改成 1.1 能解决升级后变慢吗?
本次测试中没有:4 和 1.1 在 3%、8%、15% 三种选择率下计划都没变,耗时差异方向不一致。它应该带回旧值,但不要指望它解释大部分变慢。
下一步
推荐动作
你提供新旧集群的 pg_settings / pg_extension 导出和 pg_stat_statements Top-20(可脱敏),我们逐项判断哪些参数该带回、哪些扩展要升级、哪些慢查询受 JIT 或并行影响,给出切换顺序;生产变更由你方授权执行。也可以先买 ¥59 PG 性能排查学习包自查,或用免费 PG14 升级体检工具先过一遍。