4. 索引扫描方式:Index Scan、Index Only Scan、Bitmap Scan 的工作原理与代价模型
PostgreSQL 的查询规划器会根据表统计信息、索引结构、查询条件以及系统配置,在多种索引扫描方式中选择代价最低的执行路径。理解这三种核心扫描方式的工作原理与代价模型,是进行索引优化与缓存异步任务编排调优的基础。
4.1 三种扫描方式的工作原理
| 扫描方式 | 核心机制 | 访问路径 | 适用场景 |
|---|---|---|---|
| Index Scan | 遍历索引条目,对每个匹配的条目通过 CTID 回表访问堆表(Heap)获取完整行数据。 | 索引 → 堆表(随机 I/O) | 查询返回行数较少(通常 < 5% 表行数),且需要访问非索引列。 |
| Index Only Scan | 仅扫描索引页,直接从索引条目中获取查询所需的所有列,无需回表。 | 仅索引(顺序 I/O + 可见性检查) | 查询列全部包含在索引中,且索引页中对应元组对当前事务可见。 |
| Bitmap Scan | 先扫描索引构建位图(Bitmap),将匹配的 CTID 按堆页物理顺序排序,再批量回表读取。 | 索引 → 位图 → 堆表(顺序 I/O 为主) | 查询返回行数较多(通常 5%–20% 表行数),且需要回表访问非索引列。 |
关键区别:Index Scan 每次回表都是随机 I/O;Index Only Scan 完全避免回表;Bitmap Scan 通过位图将随机 I/O 转换为顺序 I/O,大幅降低 I/O 代价。
4.2 代价模型核心公式
PostgreSQL 的代价模型以 顺序页读取代价(seq_page_cost) 和 随机页读取代价(random_page_cost) 为基础。默认值分别为 1.0 和 4.0,意味着一次随机 I/O 的代价相当于四次顺序 I/O。
4.2.1 Index Scan 代价
启动代价 = 索引高度 × random_page_cost
总代价 = 启动代价 + (索引扫描行数 × cpu_index_tuple_cost)
+ (回表行数 × random_page_cost)
+ (回表行数 × cpu_tuple_cost)
- 索引高度:B-Tree 索引的层级数,通常为 2–4。
- 回表行数:等于满足条件的行数,每次回表都产生一次随机 I/O。
- cpu_index_tuple_cost:处理每条索引元组的 CPU 代价(默认 0.005)。
- cpu_tuple_cost:处理每条堆表元组的 CPU 代价(默认 0.01)。
4.2.2 Index Only Scan 代价
启动代价 = 索引高度 × random_page_cost
总代价 = 启动代价 + (索引扫描行数 × cpu_index_tuple_cost)
+ (可见性检查失败的回表率 × 回表行数 × random_page_cost)
+ (索引扫描行数 × cpu_tuple_cost)
- 可见性检查失败的回表率:当索引页中部分元组对当前事务不可见时,仍需回表确认。该比例由
pg_class.relallvisible与pg_index.indisvisible共同决定。 - 理想情况下,回表率为 0,代价仅为索引扫描的 CPU 代价。
4.2.3 Bitmap Scan 代价
Bitmap Index Scan 代价 = 索引高度 × random_page_cost
+ (索引扫描行数 × cpu_index_tuple_cost)
Bitmap Heap Scan 代价 = (堆页数 × seq_page_cost)
+ (回表行数 × cpu_tuple_cost)
总代价 = Bitmap Index Scan 代价 + Bitmap Heap Scan 代价
- 堆页数:位图合并后需要访问的堆页数量,通常远小于回表行数。
- 位图构建阶段将随机 I/O 的 CTID 按堆页排序,使得回表阶段变为顺序 I/O。
调优提示:当
random_page_cost 设置过高(如 SSD 环境下仍使用默认 4.0),规划器会过度倾向于 Index Only Scan 或 Bitmap Scan,可能导致次优计划。建议 SSD 环境将 random_page_cost 调整为 1.0–1.5。
4.3 扫描方式选择流程
4.4 代价对比案例
假设一张 1000 万行订单表 orders,在 user_id 列上建有 B-Tree 索引。查询条件为 WHERE user_id = 12345,匹配行数分别为 100 行、5 万行、50 万行三种场景。
| 匹配行数 | 选择率 | Index Scan 代价 | Index Only Scan 代价 | Bitmap Scan 代价 | 规划器选择 |
|---|---|---|---|---|---|
| 100 行 | 0.001% | ~450 | ~50(需覆盖索引) | ~800 | Index Scan / Index Only Scan |
| 5 万行 | 0.5% | ~22,000 | ~250(需覆盖索引) | ~3,500 | Bitmap Scan |
| 50 万行 | 5% | ~220,000 | ~2,500(需覆盖索引) | ~28,000 | Bitmap Scan / 全表扫描 |
注意:Index Only Scan 虽然代价最低,但要求查询列全部在索引中,且索引页可见性良好。如果索引频繁更新导致可见性检查失败率高,实际代价可能接近 Index Scan。
4.5 与缓存异步任务编排的联动
在缓存异步任务编排场景中,索引扫描方式的选择直接影响缓存命中率与异步任务调度策略:
- Index Only Scan:仅访问索引页,对共享缓冲区的压力最小,适合高频小查询的缓存预热。
- Bitmap Scan:回表阶段按堆页顺序读取,能最大化利用预读(prefetch)机制,适合异步批量加载任务。
- Index Scan:随机 I/O 密集,容易导致缓存抖动,应尽量避免在缓存异步任务的高峰期执行。
编排建议:在异步任务中,优先使用 Bitmap Scan 配合
effective_io_concurrency 参数,利用位图排序特性实现顺序 I/O,减少缓存竞争。对于高频点查,通过覆盖索引(Index Only Scan)降低缓冲区压力。