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 扫描方式选择流程

PostgreSQL 索引扫描方式选择流程 查询条件匹配索引 查询列是否全部在索引中? 是 Index Only Scan 仅扫描索引,无需回表 否 选择率 < 5%? 是 Index Scan 少量随机 I/O 回表 否 Bitmap Scan 位图排序后顺序 I/O 回表 选择率阈值受 random_page_cost / seq_page_cost 比值影响,SSD 环境下阈值可提升至 10%–15%

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)降低缓冲区压力。