索引扫描方式:顺序扫描、索引扫描、位图扫描、仅索引扫描

PostgreSQL 在执行查询计划时,会根据表大小、索引结构、查询条件选择性以及统计信息,从四种扫描方式中选择最优方案。理解这四种扫描方式的原理与适用场景,是索引优化的基础。

1. 四种扫描方式概览

扫描方式 英文名称 核心机制 适用场景
顺序扫描 Seq Scan 逐页读取整个表的堆文件 全表数据量大、查询无过滤条件、或过滤条件选择性差(返回行占比高)
索引扫描 Index Scan 先遍历索引树找到匹配元组指针(TID),再回表读取堆页 选择性好(返回行占比低)、索引列上有过滤条件
位图扫描 Bitmap Scan 先通过索引生成位图(TID 集合),再按物理顺序批量回表 选择性中等、返回行较多但非全表、多个索引条件可组合
仅索引扫描 Index Only Scan 直接从索引页获取数据,无需回表 查询列全部包含在索引中,且可见性映射(VM)标记为全可见

2. 顺序扫描(Seq Scan)

顺序扫描是 PostgreSQL 最基础的扫描方式。执行器从表的第一个数据块开始,依次读取所有堆页,逐行检查是否满足过滤条件。

  • 优点:无索引维护开销,适合大范围数据读取;磁盘预读友好,顺序 I/O 效率高。
  • 缺点:必须扫描全部数据块,即使只返回少量行也会产生大量 I/O。
  • 触发条件:查询无 WHERE 条件;WHERE 条件无法使用索引;优化器估算返回行数超过表行数的 5%~10%(阈值受 random_page_cost 和 seq_page_cost 影响)。
优化提示:如果频繁对大表执行顺序扫描,应考虑增加索引或调整 random_page_cost(默认 4)与 effective_cache_size,让优化器更倾向于索引扫描。

3. 索引扫描(Index Scan)

索引扫描分为两步:先在 B-Tree 索引中定位到匹配的叶子节点,获取元组指针(TID,即页号+偏移量);再根据 TID 回表读取对应的堆页,获取完整行数据。

  • 优点:只读取少量索引页和堆页,I/O 量远小于顺序扫描。
  • 缺点:回表操作是随机 I/O,当返回行数较多时性能下降明显。
  • 触发条件:WHERE 条件命中索引列,且优化器认为返回行数较少。
-- 查看索引扫描执行计划
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE order_id = 12345;
-- 输出示例:
-- Index Scan using orders_pkey on orders  (cost=0.28..8.29 rows=1 width=36)
--   Index Cond: (order_id = 12345)
--   Buffers: shared hit=3

4. 位图扫描(Bitmap Scan)

位图扫描是索引扫描与顺序扫描的折中方案。它分为两个阶段:

  1. 位图构建:遍历索引,将匹配元组的 TID 按堆页号映射到位图中(每个 bit 代表一个堆页)。
  2. 位图扫描:按堆页物理顺序读取位图中标记的页面,逐行检查精确匹配。
  • 优点:将随机 I/O 转换为顺序 I/O;支持多个索引条件的位图 AND/OR 组合。
  • 缺点:需要额外内存存储位图;当返回行数接近全表时不如直接顺序扫描。
  • 触发条件:返回行数较多(通常占表行数的 1%~10%),或 WHERE 条件涉及多个索引列。
关键区别:索引扫描每次回表都是随机 I/O;位图扫描先收集所有 TID,再按物理页顺序回表,将随机 I/O 转化为顺序 I/O,大幅减少磁盘寻道时间。
四种索引扫描方式对比流程图 SQL 查询 查询优化器 (基于统计信息与成本估算) 顺序扫描 Seq Scan 索引扫描 Index Scan 位图扫描 Bitmap Scan 仅索引扫描 Index Only Scan 逐页读取堆文件 无索引开销 顺序 I/O 全表扫描 遍历索引树 获取 TID 回表随机 I/O 少量回表 构建位图 按页顺序回表 随机→顺序 I/O 中等选择性 仅读取索引页 无需回表 依赖 VM 可见性 零回表 I/O 选择依据 选择性好(返回行少)→ 索引扫描 选择性中等 → 位图扫描 选择性差(返回行多)→ 顺序扫描

5. 仅索引扫描(Index Only Scan)

仅索引扫描是 PostgreSQL 9.2 引入的重要优化。当查询所需的所有列都包含在索引中时,执行器可以直接从索引页获取数据,完全跳过堆表访问。

  • 前提条件:索引必须包含查询涉及的所有列(覆盖索引);对应的堆页在可见性映射(VM)中标记为“全可见”,即该页上没有未提交的事务或死元组。
  • 优点:零回表 I/O,性能接近内存索引查找。
  • 缺点:如果 VM 标记不全,仍需回表验证可见性;索引体积可能因包含多余列而增大。
-- 创建覆盖索引实现仅索引扫描
CREATE INDEX idx_orders_covering ON orders (order_id) INCLUDE (status, amount);

-- 查询仅命中索引列
EXPLAIN (ANALYZE, BUFFERS) 
SELECT order_id, status, amount FROM orders WHERE order_id BETWEEN 1000 AND 2000;
-- 输出示例:
-- Index Only Scan using idx_orders_covering on orders  (cost=0.28..12.45 rows=100 width=20)
--   Index Cond: ((order_id >= 1000) AND (order_id <= 2000))
--   Heap Fetches: 0  ← 关键指标:0 表示无需回表
注意:Heap Fetches 字段表示仅索引扫描中因 VM 不可用而被迫回表的次数。如果该值持续偏高,应检查 autovacuum 是否及时清理死元组并更新 VM。

6. 四种扫描方式的选择策略

PostgreSQL 优化器基于成本模型选择扫描方式,主要影响因素包括:

因素 对扫描方式的影响
查询选择性 选择性越高(返回行越少),越倾向索引扫描或仅索引扫描
表大小 小表(通常小于 shared_buffers 的 10%)倾向顺序扫描
索引列覆盖度 查询列全部在索引中时,优先考虑仅索引扫描
VM 可见性 VM 标记全可见的页面越多,仅索引扫描越可靠
random_page_cost 该值越高,优化器越倾向于顺序扫描和位图扫描
多个索引条件 多个独立索引可组合时,位图扫描可合并多个位图

7. 实战:通过执行计划识别扫描方式

-- 强制顺序扫描(测试用)
SET enable_seqscan = ON;
SET enable_indexscan = OFF;
SET enable_bitmapscan = OFF;

EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE status = 'shipped';

-- 强制索引扫描
SET enable_seqscan = OFF;
SET enable_indexscan = ON;
SET enable_bitmapscan = OFF;

EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE status = 'shipped';

-- 查看实际扫描方式与成本差异
-- 注意:生产环境不要随意修改这些参数,仅用于诊断
诊断技巧:在 EXPLAIN ANALYZE 输出中,关注 "actual time" 与 "rows" 的比值。如果索引扫描的 actual time 远高于优化器估算值,可能是统计信息过时或 random_page_cost 设置不合理。
扫描方式选择决策树 查询条件分析 是否有可用索引? 顺序扫描 查询列是否全在索引中? 仅索引扫描 选择性如何? 索引扫描 位图扫描 顺序扫描 决策依据说明 选择性好:返回行数 / 总行数 < 1%(估算) 选择性中等:返回行数 / 总行数 1%~10% 选择性差:返回行数 / 总行数 > 10% 实际阈值受 random_page_cost、effective_cache_size 等参数影响

8. 总结与最佳实践

  • 优先追求仅索引扫描:通过创建覆盖索引(INCLUDE 子句)减少回表 I/O,是性能提升最显著的手段。
  • 监控 Heap Fetches:在仅索引扫描执行计划中,Heap Fetches 应接近 0;若持续偏高,检查 autovacuum 配置。
  • 合理使用位图扫描:当查询条件涉及多个索引列时,位图扫描可以组合多个索引,避免单索引选择性不足的问题。
  • 避免过度优化:对于小表(通常小于 1000 页),顺序扫描可能比索引扫描更快,因为索引访问有额外开销。
  • 定期更新统计信息:ANALYZE 命令确保优化器有准确的表行数和数据分布信息,避免选择错误的扫描方式。