PG 19 为 Hash 索引 IO 预取修地基
本期播客
PG 19 为 Hash 索引 IO 预取修地基
很多人以为,数据库优化就是多建索引、少写烂 SQL。
错。真正决定数据库上限的,往往不是“跑得快”,而是“并发下还能不能一直跑对”。
PostgreSQL 这个新 commit,表面只是给 hash index AM 加了 fake LSN,本质上却是在为下一代批量扫描和 I/O prefetch 先修桥、再放车。
先说结论:
这不是一个“Hash 索引立刻变快”的 commit。
这是一个“为了未来能安全变快,先把并发正确性地基打牢”的 commit。
对应提交是 e5836f7b7d9a2949414ae022c74c18070202d429,标题很朴素:
Add fake LSN support to hash index AM.
但朴素标题下面,藏的是 PostgreSQL 内核工程最值钱的一类工作:
先解决并发语义,再谈性能红利。
一句话看懂:它到底干了什么?
这次改动的核心是:
让 Hash 索引在 logged 和 unlogged relation 上,都能可靠判断某个索引页在一个危险窗口里有没有被并发改过。
这个窗口,提交说明说得非常具体:
页面先被 _hash_readpage读出来之后扫描流程可能会在 _hash_kill_items里,把一些known-dead items标成LP_DEAD
问题就出在这中间。
如果这段时间里页面被并发修改了,
而扫描器还按“旧世界”的理解去标 LP_DEAD,
那这个优化就不再只是优化,而可能变成错误。
所以这次提交做的事,不是让 hash 索引“马上更快”,
而是让它以后在更激进的扫描方式下,仍然知道自己什么时候还能安全优化,什么时候必须收手。
为什么 LP_DEAD 这件事,远比很多人想得重要?
很多应用开发者对索引扫描的理解,停留在“索引帮我找到 tuple”这一层。
但 PostgreSQL 的索引扫描没这么被动。
它还会顺手做一件非常关键的事:
把已经确认对应 heap tuple 已死的索引项标成 LP_DEAD。
这样后续扫描再碰到这些项时,就能减少不必要的 heap 访问。
这不是锦上添花,而是 PostgreSQL 长期把无效访问成本往下压的一种经典手法。
但这里有一个不能退让的前提条件:
你今天准备标死的那个索引项,必须还是你先前看到的那个旧对象。
如果这个前提崩了,会怎样?
VACUUM 在并发推进 页面可能被改写 heap TID 可能进入危险的回收/重用语境 扫描器却还按旧观察结果去动手
那优化就会越界,
从“减少 heap 访问”滑向“错误地处理并发状态”。
这就是数据库内核最残酷的一条铁律:
性能优化一旦脱离正确性前提,就不是优化,而是事故预备队。
第一性原理:你不能总指望“我一直占着 pin,所以别人别动”
过去,某些索引访问方法能依赖一种很朴素的思路:
我把 buffer pin 一直握在手里,别人就不容易在危险窗口里干坏事。
这是一种“互斥式安全感”。
但 PostgreSQL 这次提交已经把未来方向说穿了:
Hash index AM 后面要接 amgetbatch,目标是支持 I/O prefetch。
而批量扫描/预取这类设计,会改变 index AM 对页面 buffer pin 的持有方式。
说得更直白一点:
以前你可以靠“长时间攥住 pin”当护身符 以后为了批量化、预取、吞吐优化,这种假设不再稳固
当旧前提不成立,系统就必须切换思路。
从第一性原理看,并发安全的办法只有两类:
不让别人改 允许别人改,但你必须可靠检测“这段窗口里世界有没有变过”
前者是“堵车”。
后者是“行车记录仪”。
PostgreSQL 这次明显选择了第二条路。
也就是:不再把长期占有资源当唯一安全手段,而是引入标准化的变化检测机制。
fake LSN,为什么是这条路上的关键拼图?
这次 commit 的关键词就是 fake LSN。
它干的不是凭空造概念,而是非常工程化的一步:
在所有 hash AM 中“会写 WAL record 的 critical section”里,如果 relation 本身不需要 WAL,也照样给相关页面推进一个 fake LSN。
这有什么用?
因为未来扫描器要判断:
从读到页面,到稍后准备标记
LP_DEAD这段时间里,这页到底有没有变过?
判断方法不是猜,也不是赌,
而是看:
页面的 LSN 有没有前进。
如果前进了,说明这页在危险窗口里发生过变化。
那就不能再拿旧观察结果继续做“理所当然”的优化动作。
而真正高明的地方在于:
这套判断现在不仅适用于正常 logged relation,
也适用于 unlogged relation。
为什么这点重要?
因为 unlogged relation 不依赖普通 WAL 机制。
如果没有 fake LSN,这套基于“LSN 是否推进”的并发变化检测,在 unlogged 场景就站不住。
那未来同一套扫描安全模型就会出现断层。
所以这次 commit 的本质,不是给 hash 多一个“漂亮字段”,
而是让 同一种并发判断逻辑,跨 logged / unlogged 两类关系都能成立。
这不是拍脑袋发明,社区其实已经给过路线图
如果只看这一个 commit,有人会觉得这是不是“为了新接口硬补一层东西”。
不是。
提交说明里自己就把谱系交代清楚了:
新方案重度参考了 nbtree dropPin的设计点名提到 commit 2ed5b87f还类比了 commit 8a879119,那个提交曾让 nbtree 用 fake LSN 改进 dropPin 行为
这意味着什么?
意味着 PostgreSQL 社区不是临时起意,
而是在把已经在其他索引访问方法上形成的成熟思路,迁移、统一、标准化到 hash AM。
这类“先在一个 AM 上走通,再推广到另一个 AM”的做法,本身就是非常强的权威背书。
因为它说明:
问题不是个例 风险模型是共性的 解决路径也不是拍脑袋,而是可复用的内核经验
这比拿某个线上事故讲故事,更有说服力。
因为它不是轶事,而是设计演进。
DBA 最该看懂的,不是“能涨多少 TPS”,而是“哪类收益值得等待”
很多 DBA 看到这种 commit,第一反应是:
“所以我线上 hash 索引今天会更快吗?”
严谨答案是:
大概率不会有立刻可感知的独立性能红利。
原因官方自己已经说得非常直白:
this is not an independently useful enhancement
翻译成人话就是:
脱离后续的 amgetbatch / prefetch 演进,这次提交本身不是终端用户可以单独感知的独立增强。
但这并不意味着它不重要。
恰恰相反,它属于最典型的“架构准备金”:
今天不兑现 headline 明天却可能是新能力上线的必要前置条件
如果没有这类基础设施,
后续一旦想在 hash 索引扫描里做批量化和预取,
就会立刻撞上并发正确性墙。
换句话说:
不是它今天直接带来多大收益,而是没有它,明天那笔收益根本不敢拿。
这才是 DBA 应该重视它的原因。
对应用开发者,这件事也不是“内核团队自嗨”
很多应用开发者会有一种错觉:
“索引扫描是数据库内部实现细节,跟我没关系。”
这话只对一半。
因为你的业务负载,决定了数据库内核要不要频繁面对这些问题:
扫描时是否伴随大量 heap 可见性判断 是否经常命中 known-dead index items VACUUM 是否持续并发推进 页面修改、回收、分裂是不是高频背景噪音
也就是说,数据库不是一个静态查询机。
它是一个边服务业务、边进行自我维护、自我清理、自我修正的运行时系统。
你看到的一次查询,背后未必只是在“查数据”。
它还可能同时叠着:
heap 校验 索引项死标记 并发页面变化判断 与 VACUUM 的协作和博弈
所以这次 commit 虽然写的是 hash AM,
但它提醒每个开发者一件更大的事:
数据库性能,从来不只是 SQL 文本层面的事,而是整个运行时协作模型的结果。
如果前提条件崩塌,观点也要跟着变
严肃讨论技术,不能只会喊结论,必须说清前提。
我这篇文章的核心判断,成立在两个前提上:
PostgreSQL 后续确实会继续推进 hash AM 对 amgetbatch的接入批量扫描和预取需要一种不依赖长期持 pin 的标准化安全模型
在这两个前提下,这次 commit 的价值非常明确:
它是在为未来能力打地基。
但如果前提条件崩塌呢?
假设未来 hash AM 根本没有真正采用这条批量扫描/预取路线,
那官方这句 “not an independently useful enhancement” 就必须被原样尊重。
那时最合理的评价就不是“重大能力前奏”,而是:
一次没有单独用户价值兑现的基础设施补丁。
这不是打脸,而是技术讨论该有的纪律:
前提变,结论就得变。
PostgreSQL 社区真正牛逼的地方:先修桥,再谈车速
今天很多数据库宣传,最爱讲“更快了”。
但成熟内核团队最看重的,往往不是先把油门踩到底,而是先确认桥不会塌。
这次 hash fake LSN commit 就非常典型:
不是直接交付用户侧性能 headline 不是营销意义上的“重大特性” 而是先把并发语义补齐 再为批量扫描和 I/O prefetch 创造可安全落地的条件
这种 commit 的价值,恰恰在于它不浮夸。
真正危险的优化,从来不是“做得不够猛”,
而是“在错误并发假设上做得太猛”。
PostgreSQL 社区这次释放的信号很清楚:
底层收益可以晚一点兑现,但并发正确性不能欠账。
结尾
很多数据库事故,不是因为没有优化,
而是因为把优化建立在错误的并发假设上。
这个 commit 的价值,就在于它提醒所有 DBA、架构师和开发者:
内核级性能红利,永远要先经过正确性审计。
先证明未来的快是安全的,
然后那种快,才值得上线。
正确性校验说明
这篇文章的核心技术判断,我已经按命题拆分后交给 DeepWiki 基于 postgres/postgres 仓库知识做了校验。
DeepWiki 对下列关键结论给出了“准确”认可:
这次提交的核心不是立刻提速,而是为 hash AM 在 logged / unlogged relation 上提供可靠的页面并发变化检测基础 危险窗口确实发生在 _hash_readpage与_hash_kill_items之间LP_DEAD标记与并发页面变化 / TID recycling 风险存在直接正确性关系未来 amgetbatch/ I/O prefetch 路线会弱化“长期持有 buffer pin 作为互锁”的旧假设本次提交通过在相关 critical section 中为非 WAL 场景设置 fake LSN,让基于 LSN 推进的变化检测在两类 relation 上都成立 这次提交官方明确认定“不是 independently useful enhancement”,因此不应被误读为已兑现的独立性能增强
你怎么看?
如果只能二选一,你更希望 PostgreSQL 团队优先交付“立刻能感知的性能提升”,还是这种先把并发语义和基础设施补齐的底层准备?欢迎留言聊聊。