PostgreSQL Index Only Scan 为什么还会回堆:看 Heap Fetches 与可见性地图
同一条查询在非覆盖索引、覆盖 INCLUDE 索引、可见性地图变脏与 VACUUM 之后的 EXPLAIN (ANALYZE, BUFFERS) 对比,附 PostgreSQL 17.11 实测。
有了 INCLUDE 覆盖索引还不够:Heap Fetches>0 说明可见性地图没罩住,仍要回堆确认;VACUUM 后 Fetches 到 0,共享缓冲命中从八百多块掉到二十多块。
适用对象
- Index Only Scan ≠ 零堆访问:先看 Heap Fetches。
- INCLUDE 把 SELECT 列放进索引,避免为投影回堆;可见性仍靠可见性地图。
- UPDATE/DELETE 会打脏 all-visible 位;VACUUM 后 Fetches 才回到 0。
- 对比时固定开 BUFFERS,shared hit 比单看耗时更能说明读了多少块。
只有 uid 索引时:Bitmap Heap Scan
先建 `INDEX idx_orders_uid (uid)`。计划是 Bitmap Index Scan → Bitmap Heap Scan:Heap Blocks=1076,Buffers shared hit=1080,Execution Time 约 1.33 ms。
这是典型的“索引定位 + 回堆取列”。要投影 id/amount/status,堆访问避不开。
INCLUDE 之后:Index Only Scan,再看 Heap Fetches
换成 `CREATE INDEX idx_orders_uid_covering ON orders (uid) INCLUDE (id, amount, status)`,并 VACUUM 更新可见性地图后:Index Only Scan,Heap Fetches=0,Buffers shared hit=23,Execution Time 约 0.24–0.29 ms。
为了复现“有 Index Only Scan 但仍回堆”,我对 uid=7 的行做了无业务含义的 `UPDATE ... SET amount = amount`,脏掉相关页面的 all-visible 位,并临时关掉 bitmap/seqscan 迫使走 Index Only Scan:Heap Fetches=1734,Buffers shared hit=871,Execution Time 约 1.28 ms。
再 VACUUM:Heap Fetches 回到 0,Buffers shared hit=23,Execution Time 约 0.24 ms。同一条覆盖索引,可见性地图状态能把读块数差出几十倍。
| 状态 | 扫描方式 | Heap Fetches / 堆块 | shared hit | Execution Time |
|---|---|---|---|---|
| 仅 idx(uid) | Bitmap Heap Scan | Heap Blocks=1076 | 1080 | 约 1.33 ms |
| 覆盖 + 可见性脏 | Index Only Scan | Heap Fetches=1734 | 871 | 约 1.28 ms |
| 覆盖 + VACUUM 后 | Index Only Scan | Heap Fetches=0 | 23 | 约 0.24 ms |
看到 Index Only Scan 之后还要问的三句话
计划节点名字好看不够,把下面三句问完再决定要不要加索引或催 VACUUM。
Index Only Scan 核对
- 01
01
SELECT 列是否都在索引里
缺一列就会退化或被迫回堆取投影列;用 INCLUDE 补齐只读列。
- 02
02
Heap Fetches 是不是 0
>0 就去查最近是否大量 UPDATE/DELETE,以及 autovacuum 是否跟上。
- 03
03
对照 BUFFERS
shared hit/read 从上千降到几十,说明回堆压力真的下去了。
- 04
04
生产用 CONCURRENTLY
CREATE INDEX CONCURRENTLY,并评估写入放大;VACUUM 是运维动作,不是应用里随手跑的。
- 05
05
别为了演示关 GUC
本文为复现 Fetches 临时关过 enable_bitmapscan;生产排障不要依赖关扫描路径。
演示脚本含 DROP/CREATE INDEX、UPDATE、VACUUM,仅测试库使用。
演示脚本与本次样例 CSV
演示 shell 会依次跑非覆盖、覆盖+VACUUM、脏可见性、再 VACUUM,并打印 EXPLAIN (ANALYZE, BUFFERS)。样例 CSV 记录了本文测试库的关键指标。
本节判断
- 适用:PostgreSQL 13–17(已在 17.11 测试;INCLUDE 需 11+)。
- 演示脚本会改索引并 VACUUM,只在测试库跑。
- 数据:自造 orders 20 万行;文中数字非客户数据。
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
为什么我已经 INCLUDE 了,计划还是 Bitmap Heap Scan?
优化器按代价选路径。可见性地图很脏时,Index Only Scan 的预期堆取成本可能高于 Bitmap Heap。先 VACUUM/看 autovacuum,再比计划。
Heap Fetches 略大于返回行数正常吗?
可能。更新产生的旧版本、HOT 与索引条目状态会导致额外确认。关键看数量级:1734 行查询出现上千 Fetches,和 0 是两回事。
和 MySQL 的 Using index 是一回事吗?
目标类似(少回表),机制不同。MySQL 看 Extra/TREE;PostgreSQL 还要额外盯 Heap Fetches 与可见性地图。
下一步
推荐动作
你提供 EXPLAIN (ANALYZE, BUFFERS) 文本(可脱敏),我们标出 Heap Fetches/缓冲读与索引改动优先级;VACUUM 与 DDL 由你方授权执行。