PostgreSQL 19 性能飞跃:Bloom 索引 VACUUM 提速 30%,背后是 I/O 效率的革命
数据库优化的本质,不是盲目调参,而是让 I/O 模式匹配硬件特性。
你有没有遇到过这样的场景:一张大表上建了 Bloom 索引用于快速任意组合查询,但随着数据频繁更新,索引日渐臃肿,VACUUM 的时间越来越长,最终成为维护窗口的瓶颈。你尝试调整 maintenance_work_mem,效果有限;你考虑是否要放弃 Bloom 索引,但又舍不得它的查询加速能力。
2026 年 3 月,Michael Paquier 提交的 d841ca2 补丁,为 Bloom 索引的 VACUUM 操作带来了 30% 的性能提升。 这不仅仅是数字的变化,更是一场 I/O 效率的革命——将传统的同步循环读替换为 流式读取(streaming read) ,让磁盘 I/O 从“单线程”进化到“流水线”。
让我们回归 VACUUM 的本质。当你在一个表上执行 VACUUM 时,PostgreSQL 需要清理已死亡的行版本,并更新索引。对于 Bloom 索引,VACUUM 的核心工作之一是遍历索引的所有页,标记那些因对应表行死亡而失效的索引条目。
这个过程的 I/O 模式是:顺序读取索引的所有页。听起来简单,但传统的实现方式是同步循环——ReadBufferExtended() 一次读一页,处理完这一页再读下一页。这在机械硬盘时代是合理的,但在现代存储(SSD、NVMe)面前,却浪费了硬件的并行能力。
第一性原理告诉我们:现代存储设备擅长并发 I/O,一次发起多个读请求,让设备自己调度,比串行等待每个读完成要快得多。流式读取正是利用这一点:它允许 PostgreSQL 在后台预取后续页面,同时处理当前页面,形成 I/O 与 CPU 的流水线作业。
这个补丁的核心改动非常聚焦:将 blbulkdelete() 和 blvacuumcleanup() 函数中的同步 ReadBufferExtended() 循环,替换为流式读取接口。
技术细节(对 DBA 透明,但值得了解):
for 循环遍历每个块号,对每个块调用 ReadBufferExtended(),等待 I/O 完成,处理,然后下一个。read_stream_next_buffer() 获取下一个已准备好的缓冲区。内核在后台并行加载多个块,应用在处理当前块时,下一个块可能已经在内存中了。效果数据:根据提交信息,在与其他优化相同的测试条件下, 运行时间提升了约 30% 。更重要的是,I/O 操作次数大幅减少——因为流式读取可以合并物理 I/O,让操作系统和存储设备发挥最大效能。
假设你有一个 500GB 的表,上面有 Bloom 索引,每周需要执行一次 VACUUM 来回收空间和更新统计信息。传统方式需要 100 分钟。
在一个大型数据仓库环境中,可能有数十个 Bloom 索引,累计节省的时间将以百小时计。
这个优化不是孤例。提交者 Xuneng Zhou 在邮件列表中提及,他还将流式读取应用到了 pgstattuple 等模块的扫描路径中。此前,核心代码已经对 B-tree 索引的 VACUUM 进行了类似的流式读取优化(commit 6c228755),同样获得了显著的性能提升。
这表明:流式读取正在成为 PostgreSQL I/O 子系统的新范式,从核心索引到 contrib 模块,全面受益。
流式读取的核心前提是:你能提前知道要读取哪些块,并且这些块的读取顺序是确定的。当这个前提崩塌时,优化效果可能打折扣。
如果 VACUUM 过程中需要读取的块并不是连续的(例如,某些索引页被跳过),流式读取的预取效率会下降。但 Bloom 索引的 VACUUM 通常是全索引扫描,因此非常适合。
在单块机械硬盘上,并发 I/O 的收益有限,因为磁头只能服务一个请求。但即便如此,流式读取仍能减少操作系统调用次数,仍有小幅提升。30% 的收益主要是在 SSD 及更快的存储上测得的。
流式读取需要额外的内存来缓存预取的页面。如果 maintenance_work_mem 设置得过低,流式读取可能退化为同步模式。因此,确保 maintenance_work_mem 足够大(例如,至少能容纳几十个索引页)是发挥性能的前提。
这个优化专为 Bloom 索引设计。如果你的业务没有使用 Bloom 索引(比如只用 B-tree 或 GIN),这个优化不直接受益。但好消息是,流式读取的哲学正在渗透到更多模块,未来的 VACUUM 优化可能会惠及所有索引类型。
SELECT schemaname, tablename, indexname, indexdef
FROM pg_indexes
WHERE indexdef LIKE '%bloom%';
如果你看到输出,说明你正在使用 Bloom 索引。升级到 PostgreSQL 19 后,VACUUM 将自动受益。
在测试环境,对一张包含 Bloom 索引的表执行 VACUUM,并对比 PG18 和 PG19 的执行时间:
\timing on
VACUUM your_bloom_table;
记录时间。如果可能,用 pg_stat_user_tables 观察 last_vacuum 前后的变化。
为了最大化流式读取的效果,确保 maintenance_work_mem 足够大。一个经验法则是:让它至少能容纳 64 个索引页。对于默认 8KB 的页面,即 512KB。但通常建议设置为 1GB 或更高,特别是当你有多个大型索引时。
SET maintenance_work_mem = '2GB';
VACUUM your_bloom_table;
使用 pg_statio 视图观察优化前后的 I/O 变化:
SELECT * FROM pg_statio_user_indexes
WHERE indexname = 'your_bloom_index';
重点关注 blks_read 和 blks_hit 的比例。流式读取应能提高缓存命中率,减少实际物理读。
这个补丁再次印证了 PostgreSQL 社区对 I/O 子系统的持续投入。未来,我们很可能看到:
PostgreSQL 19 对 Bloom 索引 VACUUM 的优化,看似是一个小模块的小改动,实则是将现代 I/O 理念深植于数据库内核的又一例证。30% 的性能提升,来源于对硬件特性的深刻理解和巧妙的工程实现。
对于 DBA 而言,这意味着更短的维护窗口、更低的资源消耗、更平稳的业务运行。升级到 PostgreSQL 19,不仅是为了新功能,更是为了让你的硬件投资发挥出应有的效能。
从今天起,当你运行 VACUUM 时,Bloom 索引将不再是瓶颈。