PostgreSQL码农集散地

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”当护身符
  • 以后为了批量化、预取、吞吐优化,这种假设不再稳固

当旧前提不成立,系统就必须切换思路。

从第一性原理看,并发安全的办法只有两类:

  1. 不让别人改
  2. 允许别人改,但你必须可靠检测“这段窗口里世界有没有变过”

前者是“堵车”。
后者是“行车记录仪”。

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 文本层面的事,而是整个运行时协作模型的结果。

如果前提条件崩塌,观点也要跟着变

严肃讨论技术,不能只会喊结论,必须说清前提。

我这篇文章的核心判断,成立在两个前提上:

  1. PostgreSQL 后续确实会继续推进 hash AM 对 amgetbatch 的接入
  2. 批量扫描和预取需要一种不依赖长期持 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 团队优先交付“立刻能感知的性能提升”,还是这种先把并发语义和基础设施补齐的底层准备?欢迎留言聊聊。