PostgreSQL码农集散地

PG 19 哈希索引大改动,改写 VACUUM 的 I/O 逻辑

本期播客

PG 动哈希索引了:不是“小优化”,而是改写 VACUUM 的 I/O 逻辑

很多 DBA 以为,哈希索引的性能瓶颈在“查得快不快”。
错。真正在生产上拖垮它的,往往是“删得慢、清得脏、VACUUM 抖”。

2026 年 3 月 16 日,PostgreSQL 提交了一个很值得 DBA 重视的 commit:bfa3c4f106b1fb858ead1c8f05332f09d34f664a。表面上看,它只是把 hashbulkdelete() 改成了 streaming read。实际上,它释放的是一个更明确的信号:

PostgreSQL 社区已经不满足于“功能正确”,而是在系统性地把维护路径改造成“可预取、可批量、可控缓存污染”的现代 I/O 路径。

先说结论:

这次优化的价值,不在于哈希索引 suddenly 变成了主流索引,而在于 PostgreSQL 终于开始认真处理“索引维护本身也是核心负载”这件事。

为什么这么说?

因为 hashbulkdelete() 干的不是查询,而是 VACUUM 里的索引清理。
而 PostgreSQL 官方文档写得很清楚:

  • VACUUM 要周期性回收 dead tuples,尤其是高更新表。
  • VACUUM 会显著增加 I/O 流量,甚至影响其他活跃会话。
  • INDEX_CLEANUP 是否执行,会直接影响索引中 dead tuples 的堆积与后续性能。

这意味着什么?

对 DBA 来说,索引维护从来不是“后台小事”,而是吞吐、抖动、缓存命中率和业务尾延迟的一部分。

过去,哈希索引在 bulk deletion 这条路上,本质上更像“边走边读”:处理当前 bucket,再去读下一个 bucket。
这类模式的问题非常典型:

  • 预取不足
  • I/O 合并差
  • 更容易形成同步等待
  • 更容易把不该污染的页面扔进 shared buffers

而这次 commit 的核心,就是让 hashbulkdelete() 也走上 streaming read 路径:
处理当前 bucket 的同时,预取后续 bucket。

这不是语法糖,这是 I/O 模型变化。

PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!

图片

第一性原理:为什么这件事成立?

数据库里的 bulk maintenance,本质是一个“可预测访问模式”的问题。

如果未来将要访问哪些块,大体是可预知的,那么最优策略通常不是“用到再读”,而是:

  • 提前发起读请求
  • 尽量合并 I/O
  • 用维护专用策略减少 shared buffers 污染
  • 把 CPU 等 I/O 的时间,改成 CPU 和 I/O 重叠

这正是 streaming read 的意义。

PostgreSQL 18 官方已经给过更大的背景:
新 AIO 子系统在顺序扫描、bitmap heap scan、vacuum 等场景中,测试显示可达到 2 到 3 倍性能提升。
这里必须强调:这 2 到 3 倍不是这个单一 hash index commit 的官方数字,而是 PostgreSQL 18 整体 AIO/流式读取方向的官方公开结果。
所以,对这个 commit 更严谨的判断是:

它不是孤立提速,而是在把 hash index 的 VACUUM 清理,也纳入 PostgreSQL 已经验证过的大方向。

为什么偏偏是哈希索引,更需要这类优化?

因为哈希索引天生有两个鲜明特征:

  1. 它只适合等值查询
  2. 它对数据分布和溢出链极其敏感

官方文档明确指出:

  • Hash index 只支持 =。
  • 它只保存 4 字节 hash 值,所以在 UUID、URL 等长键场景下,可能比 B-tree 小很多。
  • 但如果分布不均,bucket 会挂上 overflow pages,扫描这些链条时,块访问数可能反而比 B-tree 更差。
  • Hash index 不会自动收缩;想真正缩小,通常还得 REINDEX。

这就引出一个经常被误判的问题:

很多人以为哈希索引的问题是“查得不够快”,其实大量生产事故里,先出问题的是“维护成本失控”。

特别是这些场景:

  • 高频 UPDATE / DELETE
  • 大表,索引明显超过内存
  • 哈希桶分布不够均匀,overflow page 较多
  • VACUUM 已经是业务抖动来源之一

在这些前提下,bulk deletion 如果还是“同步、零散、无预取”地读,性能吃亏几乎是必然的。

这次 commit 真正改变 DBA 哪个判断?

我认为它至少改变三个判断。

第一,哈希索引不再只是“查询路径”的话题,而是“维护路径”也开始被补课。

过去很多 DBA 不愿意碰 hash index,不只是因为功能边界窄,还因为它一旦进入高 churn 场景,维护收益和风险不够透明。
现在社区的动作说明:他们不是在给 hash index 做 cosmetic patch,而是在补齐它最关键的运维短板之一。

第二,PostgreSQL 正在把 VACUUM 从“必要之恶”变成“可工程化优化对象”。

这几年 PostgreSQL 对 VACUUM 的投入非常连续。
官方文档已经暴露了很多 DBA 关心的控制面:

  • INDEX_CLEANUP
  • PARALLEL
  • BUFFER_USAGE_LIMIT
  • pg_stat_progress_vacuum

现在连具体索引方法内部的 bulk-delete 读取方式都开始改造,说明一个趋势:

未来 DBA 调优,不能只盯 SQL 和执行计划,还得盯 maintenance path 的 I/O 形态。

第三,这不是“哈希索引全面翻身”,而是“适用场景更清晰了”。

这点必须说得尖锐一点:

谁要是因为这个 commit 就开始鼓吹“以后等值查询都用 hash index”,那就是拿补丁当信仰。

官方文档写得很明确,哈希索引最适合的是:

  • 大表
  • 等值查询
  • 键值较长
  • 近似唯一或低碰撞
  • 溢出链可控

如果这些条件不成立,尤其是:

  • 数据高度倾斜
  • 某些值重复度极高
  • bucket overflow 明显
  • 表增长很快,前台 split 成本高

那哈希索引可能依然不是最佳选择,甚至会比 B-tree 更差。

什么时候你应该真正关心这个 commit?

如果你满足下面几个条件,这个 commit 值得关注:

  • 你在生产里真的用了 hash index
  • 这些索引承载高更新、高删除 workload
  • autovacuum 或手工 vacuum 已经带来明显 I/O 压力
  • 索引规模已经明显超过 shared buffers
  • 存储层对预取、合并 I/O、异步读取比较敏感

这时候,这个优化的意义不是“多快 5% 或 10%”这么简单。
它更大的价值是:

把一段原本容易造成同步等待和缓存污染的维护路径,改造成更接近现代存储友好的访问方式。

对 DBA 来说,这通常意味着三件事:

  • VACUUM 运行时间更稳
  • 对前台查询缓存干扰更小
  • 在 I/O 紧张时,尾延迟更可控

但如果前提崩塌,观点也必须变

严谨一点说,这篇文章的核心观点依赖几个前提:

  • 你的哈希索引 bulk-delete 确实是 I/O 主导
  • bucket 访问模式足够可预测,预取能发挥作用
  • 索引足够大,不能全靠缓存吃掉
  • 你的 workload 确实会触发 index cleanup

如果这些前提崩了,结论就要改。

比如:

如果索引很小,基本常驻内存
那 streaming read 的收益可能非常有限,甚至感知不明显。

如果 dead tuples 很少,VACUUM 常常跳过 index cleanup
那这个 commit 对你几乎没有直接体感。

如果真正的问题是数据分布严重倾斜,overflow chain 过长
那优化 bulk-delete 只是缓解症状,根因仍然是索引方法选型不合适。
此时更合理的观点应是:

与其指望 VACUUM 更聪明,不如先承认这个列未必适合 hash index。

如果你面临的是 wraparound 风险,甚至不得不优先“先跑完 vacuum”
那你应先保命,再谈局部最优。
也就是说,生死线前,吞吐优先;脱离危险后,再谈维护质量。

DBA 最该记住的一句话

这次提交不是在告诉你“hash index 更神了”,而是在告诉你:PostgreSQL 正在把索引维护从“被动清垃圾”升级成“主动设计 I/O 路径”。

这背后的含义非常现实:

以后判断一个数据库版本值不值得升,不该只看 SQL 性能跑分,
还要看它怎么处理:

  • vacuum 的 I/O
  • 索引清理的缓存污染
  • 后台维护与前台业务的争抢
  • 大表场景下的尾延迟

真正成熟的数据库优化,从来不是只把查询做快。
而是把“清理自己”的代价,也做低。


你怎么看?

你在生产上还会主动使用 hash index 吗?
你更看重它在长键等值查询下的体积优势,还是更担心 overflow chain 和维护成本?
评论区聊聊:这个 commit 会让你重新评估 hash index,还是依然坚持“能用 B-tree 就别碰 hash”?