PostgreSQL 19 Beta 2 深度解析:JIT 默认关闭、并行 autovacuum 与异步 I/O 自治,这次大版本动了多少"祖传默认值"
2026 年 7 月 16 日,PostgreSQL 全球开发组发布了 PostgreSQL 19 Beta 2。如果你只是扫了一眼新闻标题,可能会觉得"又是一年一度的大版本,无非加点特性"。但如果你把 Release Notes 从头到尾读一遍,会发现 PG 19 是近几年对默认行为动刀最狠的一个版本:
- JIT 默认关闭了——这个从 PG 12 开始默认开启的特性,被官方亲手关掉;
- TOAST 默认压缩算法从 pglz 换成了 lz4——祖传压缩算法退位;
max_locks_per_transaction默认值从 64 翻倍到 128;standard_conforming_strings被强制永远为 on——一个存在了近 20 年的兼容开关被焊死;- RADIUS 认证直接被删除;
- MD5 认证成功也要给你弹警告。
与此同时,性能侧的进化也是实打实的:autovacuum 终于支持并行 worker、异步 I/O worker 数量可以自动伸缩、COPY FROM 用上了 SIMD、排序换成了 radix sort、NOT IN 可以转 ANTI JOIN……
这篇文章我会站在一个"要负责把生产库从 PG 17/18 升到 19"的工程师视角,把这些变化背后的逻辑、对你的实际影响、以及需要提前做的功课,逐条拆开讲清楚。全文较长,建议收藏后配合官方文档食用。
说明:本文基于 PostgreSQL 19 Beta 2 及官方 Release Notes(截至 2026-06-17 的快照)撰写,正式版(GA)发布前个别特性仍可能调整或回退,上生产前请以 GA 版 Release Notes 为准。
一、为什么"关掉 JIT"反而是个好决定
先说最有争议的一条:PG 19 把 JIT 默认关闭了(由 Jelte Fennema-Nio 提交)。
1.1 JIT 在 PostgreSQL 里到底干了什么
PostgreSQL 从 11 引入基于 LLVM 的 JIT 编译,12 开始默认开启。它主要加速三类操作:
- 表达式求值(WHERE 子句、目标列表、聚合表达式)
- 元组变形(tuple deforming,把磁盘上的行格式解包成内存字段)
- 部分内联优化
理论上,对于大型分析查询,把解释执行的表达式编译成机器码,收益可以很可观。问题出在什么时候触发 JIT的决策机制上。
1.2 成本模型不可靠,是这次关闭的直接原因
JIT 是否启用由优化器的代价估算驱动:当查询预估总成本超过 jit_above_cost(默认 100000)时启用 JIT,超过 jit_optimize_above_cost 时做深度优化。官方在 Release Notes 里的原话很直白:"this costing has been determined to be unreliable"——这套成本判定被认定不可靠。
实际生产中,这个机制的翻车场景太常见了:
-- 一个典型的翻车现场:分区表 + 统计信息略有偏差
EXPLAIN (ANALYZE, TIMING)
SELECT order_date, sum(amount)
FROM orders -- 512 个分区
WHERE order_date >= '2026-01-01'
GROUP BY order_date;
优化器对分区表的成本估算容易虚高,一旦超过阈值就触发 JIT。而 JIT 编译本身是有开销的——每个查询、每个 worker 进程都要走一遍 LLVM 编译。结果就是:一个本来 200ms 能跑完的查询,光 JIT 编译就花了 800ms。在 OLTP 混合负载里,这种"负优化"出现的频率远比社区预期的高。
这些年 DBA 圈子里流传的"PostgreSQL 性能急救三板斧",第一斧往往就是 SET jit = off。现在官方等于承认了这一点。
1.3 你该怎么办
如果你的业务是 OLTP 为主:什么都不用做,升级后默认行为对你更友好,尾延迟大概率会更稳定。
如果你有重型分析查询(大表聚合、复杂表达式、长时间运行的报表),需要显式打开:
-- 全局打开(不推荐,除非是纯分析库)
ALTER SYSTEM SET jit = on;
-- 更推荐:只给分析角色/分析会话打开
ALTER ROLE report_user SET jit = on;
-- 或者在具体的重查询前打开
SET LOCAL jit = on;
SELECT /* 大型分析查询 */ ...;
一个实用的判断方法:跑一遍你的慢查询集合,用 EXPLAIN (ANALYZE, TIMING) 对比 jit=on/off 两种情况。PG 19 顺便还改进了计时精度(新增 timing_clock_source 参数,Lukas Fittl / Andres Freund / David Geier 的工作),EXPLAIN ANALYZE 的计时开销更低、更准,做这种对比测试正合适。
二、autovacuum 并行化:十几年的单线程终于破了
这是我认为 PG 19 对大库运维最有价值的特性:autovacuum 支持并行 worker(Daniil Davydov 提交)。
2.1 过去的痛点
autovacuum 的并发模型一直是"多个 worker,各扫各的表"——autovacuum_max_workers 控制同时能处理多少张表,但单张表的 vacuum 始终是单进程的(PG 13 起手动 VACUUM (PARALLEL n) 可以并行处理索引,但 autovacuum 用不上)。
结果就是经典困境:一张 2TB 的核心大表,autovacuum 单线程慢慢磨,磨到一半被更高优先级的事务冲突打断,死元组越积越多,表膨胀 → 查询变慢 → XID 回卷风险逼近,最后 DBA 只能半夜手动 VACUUM (PARALLEL 8)。
2.2 PG 19 的做法
现在 autovacuum 可以给单张表的索引清理阶段启用并行 worker,两个层级控制:
-- 实例级:给 autovacuum 的并行 worker 池设置上限
ALTER SYSTEM SET autovacuum_max_parallel_workers = 4;
SELECT pg_reload_conf();
-- 表级:指定这张表 autovacuum 时用几个并行 worker
ALTER TABLE orders SET (autovacuum_parallel_workers = 4);
设计上很克制:默认不开启,需要你显式给大表标注。这符合 vacuum 的工程现实——绝大多数小表根本不需要并行,无脑全局并行只会浪费 worker、加剧 I/O 争抢。
2.3 配套观测能力也补齐了
光有并行还不够,PG 19 同时补了一批 autovacuum 的观测视图,这块以前是出了名的黑盒:
pg_stat_autovacuum_scores(Sami Imseih):暴露每张表的 autovacuum 触发评分细节——终于可以直接回答"为什么这张表还没被 vacuum"这个千古难题了,不用再手动算n_dead_tup和阈值公式;pg_stat_progress_vacuum新增started_by和mode列:能看到这次 vacuum 是谁发起的(手动/autovacuum)、激进程度如何;pg_stat_progress_analyze新增started_by:同理。
推荐一个升级后就可以加进巡检脚本的查询:
-- 找出膨胀压力大但 autovacuum 迟迟没轮到的表
SELECT s.relname,
s.n_dead_tup,
s.last_autovacuum,
p.phase,
p.started_by,
p.mode
FROM pg_stat_user_tables s
LEFT JOIN pg_stat_progress_vacuum p ON p.relid = s.relid
WHERE s.n_dead_tup > 100000
ORDER BY s.n_dead_tup DESC
LIMIT 20;
2.4 实践建议
- 只给索引多、体量大的表设置
autovacuum_parallel_workers(索引清理是并行收益的主要来源); autovacuum_max_parallel_workers别设太大,它和查询并行、逻辑复制 worker 共享系统资源;- 升级后先用默认值跑一到两周,观察
pg_stat_autovacuum_scores,用数据决定给哪几张表开并行。
三、异步 I/O 进入"自治"阶段
PG 18 引入了真正的异步 I/O 框架(io_method = worker / io_uring),PG 19 在这条线上继续推进,核心是两件事。
3.1 I/O worker 数量自动伸缩
PG 18 里 io_workers 是个静态值——设少了 I/O 排队,设多了空耗资源,而负载是波动的。PG 19(Thomas Munro)让 io_method = worker 模式下 worker 数量按需自动增减,新增四个参数:
io_min_workers = 1 # 最少保留的 I/O worker
io_max_workers = 8 # 上限
io_worker_idle_timeout = 60s # 空闲多久后回收
io_worker_launch_interval = 500ms # 扩容节流间隔
这个设计模式很眼熟——就是连接池/线程池的 min/max + idle timeout 思路,终于用到了数据库内核的 I/O 子系统上。对云环境尤其友好:夜间批量任务把 I/O worker 顶到上限,白天 OLTP 负载时自动缩回,不需要人工干预。
3.2 大请求的预读调度优化
Andres Freund 对异步 I/O 的 read-ahead 调度做了三个 commit 的持续优化,大顺序读(全表扫描、vacuum、批量导出)在高延迟存储(典型如云盘/EBS)上的吞吐会有可感知的提升。
结合 PG 19 其他两个 I/O 相关改进——hash 索引批量删除和 GIN 索引 vacuum 改用 streaming read(Xuneng Zhou),可以看出社区的明确路线:把所有顺序访问路径逐步迁移到异步流式读取框架上。PG 17 是 buffer manager 流式化,18 是 AIO 框架落地,19 是覆盖面扩大 + 自治化。这条线索比任何单个特性都值得关注,因为它意味着 PostgreSQL 在云存储(高延迟、高并发)上的性能天花板在系统性抬升。
四、优化器:NOT IN 终于不再是性能地雷
每个 PostgreSQL 老手都被 NOT IN 坑过。PG 19 的优化器改进里,Richard Guo 一个人贡献了十几个 commit,其中最有实感的是这条:
4.1 NOT IN → ANTI JOIN
-- 经典写法,PG 18 及以前的性能地雷
SELECT * FROM users u
WHERE u.id NOT IN (SELECT user_id FROM blacklist);
在 PG 18 及以前,这种写法通常被规划成 hashed SubPlan,在子查询结果集大的时候性能急剧退化,而且由于 NULL 语义问题无法转成 ANTI JOIN(NOT IN 遇到 NULL 的三值逻辑陷阱:子查询里只要有一个 NULL,整个条件就永远不为真)。
PG 19 的做法是:当优化器能证明两侧都不含 NULL 时(比如列有 NOT NULL 约束),自动把 NOT IN 转换为 ANTI JOIN。ANTI JOIN 可以走 hash/merge,还能配合另一个新优化——ANTI JOIN 内侧唯一时可用 Memoize(同样是 Richard Guo)。
实践启示很直接:给该 NOT NULL 的列加上 NOT NULL 约束。在 PG 19 里,约束不只是数据质量工具,它直接解锁优化器的转换路径。这条原则同样适用于本版本的一系列 NULL 相关简化:IS DISTINCT FROM 在可证非空时被简化为普通等值比较、COALESCE() 跳过不必要的参数求值、IS TRUE/FALSE 化简为普通布尔表达式——全部依赖"优化器能证明非空"这个前提。
4.2 聚合下推:join 前先聚合
另一个值得单独说的是部分聚合下推到 join 之前(Richard Guo, Antonin Houska):
SELECT c.region, sum(o.amount)
FROM orders o
JOIN customers c ON c.id = o.customer_id
GROUP BY c.region;
以前的执行顺序是先 join 出全量中间结果再聚合;现在优化器可以先对 orders 按 customer_id 做部分聚合,把可能上亿行的 join 输入压缩到客户数量级,再去 join。这是分析型数据库的经典优化(eager aggregation),落地到 PG 意味着中型分析场景下少一个"必须上 OLAP 引擎"的理由。
此外还有一批底层性能改进值得知道名字:radix sort 排序加速(John Naylor)、COPY FROM 的 text/CSV 解析用 SIMD 加速(Nazir Bilal Yavuz, Shinya Kato)、外键约束检查性能优化(Junwang Zhao, Amit Langote 等)、NOTIFY 只唤醒真正监听对应 channel 的 backend(Joel Jacobson,重度使用 LISTEN/NOTIFY 做消息分发的系统会明显受益——以前 NOTIFY 几乎会唤醒所有 backend,纯属惊群)。
五、TOAST 默认压缩换成 lz4:一次迟到的默认值修正
PG 14 就引入了 lz4 作为 TOAST 可选压缩算法,但默认一直是上世纪风格的 pglz。PG 19 终于把默认值改了(Euler Taveira):
SHOW default_toast_compression;
-- PG 19: lz4
lz4 对比 pglz:压缩速度快数倍,解压快一个量级,压缩率大体相当或略低。对于存 JSON 文档、长文本的业务(现在谁不是呢),写入和读取大字段的 CPU 开销会直接下降。
两个注意点:
- 已有数据不会自动重压缩。旧行还是 pglz,只有新写入/更新的走 lz4。想主动转换可以
VACUUM FULL或逻辑迁移(代价自行评估); - 编译时需要
--with-lz4,主流发行版打包默认都带,自编译的注意检查。
顺带提醒:可以用 pg_column_compression() 函数抽查线上大字段实际用的压缩算法,评估混布状态:
SELECT pg_column_compression(payload) AS algo, count(*)
FROM events TABLESAMPLE SYSTEM (1)
GROUP BY 1;
六、安全与兼容性:这些"破坏性变更"会咬人
PG 19 的不兼容清单比往年更长,挑真正会咬人的说。
6.1 认证体系继续收紧
- MD5 认证成功后会打警告(Nathan Bossart)。MD5 密码在 PG 18 已标记废弃,19 开始每次认证成功都会往日志里写 warning(可用
md5_password_warnings关掉,但别)。正确姿势是尽快迁移到 SCRAM-SHA-256:
-- 检查还有谁在用 MD5 密码
SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';
-- 迁移:确认 password_encryption 已是 scram-sha-256,然后让用户重设密码
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
\password someuser
- RADIUS 认证被直接删除(Thomas Munro),理由是 PG 只支持 RADIUS over UDP,这在安全上"unfixably insecure"(无法修复的不安全)。还在用 RADIUS 的企业环境需要转向 LDAP/Kerberos/证书认证。
- 新增
password_expiration_warning_threshold(Gilles Darold, Nathan Bossart):密码临期提醒,默认提前 7 天警告。 - 数据库名/角色名/表空间名禁止包含回车换行:为了堵注入类安全问题,pg_upgrade 也会拒绝带这类名字的集群升级。
6.2 三个容易踩坑的行为变更
其一,standard_conforming_strings 强制为 on(Tom Lane)。这个开关控制反斜杠是否为转义字符,2006 年(PG 8.2)引入,默认 on 也已经 15 年了。PG 19 直接焊死。影响面:用老版本 pg_dump 且 standard_conforming_strings = off 导出的备份文件,可能无法正确导入 PG 19。结论:升级迁移一律用 PG 19 自带的 pg_dump/pg_dumpall 导出,这本来就是最佳实践,现在是硬性要求。
其二,max_locks_per_transaction 默认 64 → 128,但注意 Release Notes 的措辞:锁的内存分配方式变了,新值 128 的实际容量约等于旧版的 64。也就是说如果你以前调过这个参数(比如为了大量分区表设成 256),升级后要按翻倍逻辑重新审视,直接沿用旧数值等于变相砍半。
其三,JSON 行为修正:json_array() 构造无行输入时从返回 NULL 改为返回空数组 [](Richard Guo)。语义上更合理,但如果你的应用逻辑依赖旧的 NULL 判断,需要排查。类似的还有 postgres_fdw 现在会把事务的 READ ONLY / DEFERRABLE 状态传递到远端会话——只读事务再也不能通过 FDW "偷偷"改远端数据了。
6.3 inet/cidr 的 btree_gist 索引:升级硬闸门
一个冷门但致命的点:btree_gist 扩展里 inet/cidr 的 opclass 被确认存在丢行 bug(可能漏掉本该返回的行),PG 19 将这两个类型的默认 GiST opclass 换成内置实现,并且 pg_upgrade 会直接拒绝升级带有 btree_gist inet/cidr 索引的集群。升级前自查:
SELECT indexrelid::regclass
FROM pg_index i
JOIN pg_opclass oc ON oc.oid = ANY(i.indclass)
WHERE oc.opcname IN ('inet_ops') AND oc.opcmethod = (
SELECT oid FROM pg_am WHERE amname = 'gist'
);
-- 命中的索引需要先 DROP,升级后用新 opclass 重建
七、可观测性大礼包:DBA 的巡检脚本该更新了
PG 19 新增的系统视图数量可观,值得整体过一遍:
| 新视图/新列 | 用途 |
|---|---|
pg_stat_lock | 按锁类型统计的全局锁数据,配合 pg_stat_get_lock() |
pg_stat_recovery | 恢复/复制回放状态一目了然 |
pg_stat_autovacuum_scores | 每表 autovacuum 评分,膨胀排查神器 |
pg_dsm_registry_allocations | 动态共享内存分配明细 |
pg_stat_replication_slots.mem_exceeded_count | 逻辑解码内存超限次数,判断 logical_decoding_work_mem 是否该调 |
pg_stat_all_tables.stats_reset 等 | 表/索引/序列级统计重置时间,监控系统终于能识别"统计被清零"了 |
其中 pg_stat_lock 值得展开:以前分析锁争用只能靠采样 pg_locks 快照,抓不到累计趋势。现在有了按锁类型的累计统计,可以直接把"每分钟 relation 锁等待增量"做成监控指标。
逻辑复制侧,pg_stat_subscription_stats 拆分出 sync_table_error_count 和 sync_seq_error_count(注意:旧列名 sync_error_count 被重命名,监控 SQL 需要改),新增 update_deleted 列统计因并发删除被忽略的更新——做双向复制/冲突排查的会懂这有多重要。
八、升级 Checklist:从 17/18 到 19
综合全文,给一份可执行的升级前清单:
必查(不做会升级失败或出错):
- 排查 btree_gist 的 inet/cidr 索引,先删后升级再重建;
- 排查数据库/角色/表空间名中的回车换行符;
- 还在用 MULE_INTERNAL 编码的库(罕见)需要 dump/restore 换编码;
- 确认没有依赖 RADIUS 认证的 pg_hba 条目。
必调(默认值变更):
5. 分析负载显式开 JIT(按角色或会话粒度);
6. 曾手动调过 max_locks_per_transaction 的,按新的翻倍语义重算;
7. 监控/告警 SQL 里的 sync_error_count 改名。
建议做(吃到新版本红利):
8. 给大表配置 autovacuum_parallel_workers;
9. io_method = worker 的实例配置 io_min/max_workers 自动伸缩;
10. 给业务上不可能为 NULL 的列补 NOT NULL 约束,解锁优化器新转换;
11. 巡检脚本接入 pg_stat_autovacuum_scores、pg_stat_lock、pg_stat_recovery;
12. 评估存量 TOAST 数据是否值得重压缩为 lz4。
九、总结:一个敢于"认错"的大版本
回看 PG 19,我觉得它最大的特点不是某个炫技特性,而是一种工程态度:敢于修正历史默认值。
JIT 默认开启五个大版本之后,社区用数据承认了成本模型不可靠,关掉;pglz 服役二十多年,换成 lz4;standard_conforming_strings 的历史包袱,焊死;不安全的 RADIUS,删除。每一条单看都会"得罪"一部分存量用户,但每一条都让新用户拿到的默认配置更接近最佳实践。
数据库是所有基础软件里最保守的品类,而 PostgreSQL 能保持每年一个大版本、并且持续有勇气清理旧账,这可能比任何单项性能提升都更能解释它为什么连年蝉联"年度数据库"。
PG 19 正式版预计按惯例在今年秋季 GA。Beta 2 已经可以下载测试,强烈建议用你的真实负载(尤其是关掉 JIT 后的分析查询、大表 autovacuum)提前跑一轮——发现问题现在报告,还来得及在 GA 前修掉。这也是参与开源最实在的方式。
参考资料
- PostgreSQL 19 Beta 2 官方公告(postgresql.org,2026-07-16)
- PostgreSQL 19 Release Notes(官方文档 E.1 节)
- PostgreSQL 官方文档:Routine Vacuuming / JIT / TOAST 章节