索引扫描方式:顺序扫描、索引扫描、位图扫描、仅索引扫描
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)
位图扫描是索引扫描与顺序扫描的折中方案。它分为两个阶段:
- 位图构建:遍历索引,将匹配元组的 TID 按堆页号映射到位图中(每个 bit 代表一个堆页)。
- 位图扫描:按堆页物理顺序读取位图中标记的页面,逐行检查精确匹配。
- 优点:将随机 I/O 转换为顺序 I/O;支持多个索引条件的位图 AND/OR 组合。
- 缺点:需要额外内存存储位图;当返回行数接近全表时不如直接顺序扫描。
- 触发条件:返回行数较多(通常占表行数的 1%~10%),或 WHERE 条件涉及多个索引列。
关键区别:索引扫描每次回表都是随机 I/O;位图扫描先收集所有 TID,再按物理页顺序回表,将随机 I/O 转化为顺序 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 设置不合理。
8. 总结与最佳实践
- 优先追求仅索引扫描:通过创建覆盖索引(INCLUDE 子句)减少回表 I/O,是性能提升最显著的手段。
- 监控 Heap Fetches:在仅索引扫描执行计划中,Heap Fetches 应接近 0;若持续偏高,检查 autovacuum 配置。
- 合理使用位图扫描:当查询条件涉及多个索引列时,位图扫描可以组合多个索引,避免单索引选择性不足的问题。
- 避免过度优化:对于小表(通常小于 1000 页),顺序扫描可能比索引扫描更快,因为索引访问有额外开销。
- 定期更新统计信息:ANALYZE 命令确保优化器有准确的表行数和数据分布信息,避免选择错误的扫描方式。