一、全表扫描 vs 索引扫描的抉择标准
关键分水岭因素:
-
数据选择比例
-
经验法则:当需要检索超过表总行数的 15-20% 时,全表扫描通常更优
-
原因:索引扫描需要"回表"操作,当命中大量记录时,随机I/O开销会超过顺序扫描
-
-
数据物理分布
-
数据是否已按查询键值物理排序?
-
如果是"索引组织表",分水岭会变化
-
-
内存缓冲区大小
- 如果整个表可以放入内存缓冲区,全表扫描代价极低
-
索引类型
-
聚簇索引 vs 非聚簇索引
-
覆盖索引可避免回表,提高分水岭值
-
具体场景对比:
sql
-- 场景1:索引更优(选择少量数据)
SELECT * FROM users WHERE user_id = 123; -- user_id是主键
-- 场景2:全表扫描更优(选择大量数据)
SELECT * FROM orders WHERE status = 'completed'; -- 90%订单已完成
-- 场景3:覆盖索引避免回表
SELECT user_id, name FROM users WHERE age > 30; -- (age, name)索引
二、查询实现原理与I/O分析
1. 全表扫描(Table Scan)
实现流程:
从数据文件头开始顺序读取
按页(Block/Page)加载到内存
逐行应用WHERE条件过滤
返回匹配行
I/O次数计算:
总页数 = 表大小 / 页大小(通常8KB或16KB)
假设表有1000个数据页:
理想情况(内存充足):1000次顺序I/O
最坏情况(内存不足):可能有多次重复加载
2. 索引扫描(Index Scan)
以B+树索引为例:
实现流程(非聚簇索引):
从索引根节点开始(常驻内存)
二分查找找到叶子节点:1-3次I/O
遍历叶子节点获取主键列表:n次I/O
根据主键回表获取数据:m次随机I/O
I/O示例:查询命中100条记录
索引遍历:2-3次I/O(根节点→中间节点→叶子节点)
回表操作:最多100次随机I/O(如果记录分散在不同页)
三、具体案例分析
案例1:主键查询
sql
SELECT * FROM products WHERE id = 100;
执行原理:
从索引根节点开始(通常内存中)
向下遍历到叶子节点:1-2次I/O
叶子节点包含完整数据(聚簇索引)
直接返回数据
总计:1-2次I/O,极高效
案例2:范围查询+排序
sql
SELECT * FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31'
ORDER BY order_date
LIMIT 100;
执行原理:
在order_date索引中找到起始位置
顺序扫描索引叶子节点(物理有序)
获取100个主键
回表获取完整数据(100次随机I/O)
优化方案:使用覆盖索引 (order_date, customer_id, amount)
四、核心指标总结表
| 场景 | 推荐方式 | I/O次数估算 | 说明 |
|---|---|---|---|
| 查询<5%数据 | 索引扫描 | 索引I/O + 少量随机I/O | 优势明显 |
| 查询15-30%数据 | 需要测试 | 依赖数据分布 | 转折区域 |
| 查询>30%数据 | 全表扫描 | 顺序读取所有数据页 | 更稳定 |
| 排序/分组查询 | 索引优先 | 可能避免排序操作 | 利用索引有序性 |
| COUNT(*)全表 | 最小索引 | 扫描最小索引树 | 比全表快 |
五、优化决策流程图
开始查询优化 ↓ 是否需要排序/分组? → 是 → 考虑索引优化排序 ↓ 否 估算选择比例 ↓ < 15% ? → 是 → 使用索引扫描 ↓ 否 数据是否热点? → 是 → 可能缓存在内存 ↓ 否 考虑覆盖索引 → 是 → 使用覆盖索引 ↓ 否 全表扫描更优