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 已经验证过的大方向。
为什么偏偏是哈希索引,更需要这类优化?
因为哈希索引天生有两个鲜明特征:
它只适合等值查询 它对数据分布和溢出链极其敏感
官方文档明确指出:
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_CLEANUPPARALLELBUFFER_USAGE_LIMITpg_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”?