一、答案概要
当有两个单独索引 INDEX(a) 和 INDEX(b),执行查询 WHERE a=xx AND b=xx 时:
MySQL会根据成本估算选择其中一个索引,通常选择:
- 区分度更高(选择性更好)的索引
- 过滤效果更强(返回行数更少)的索引
- 在特定条件下可能使用索引合并(Index Merge)同时使用两个索引
二、优化器决策原理
1. 成本估算模型
MySQL优化器会计算每个索引的查询成本,选择成本最低的方案。
成本计算公式(简化版):
总成本 = 索引扫描成本 + 回表成本 + CPU成本
索引扫描成本 = 需要读取的索引页数量
回表成本 = 需要回表的行数 × 单次回表成本
2. 决策过程
-- 有两个索引:INDEX(a), INDEX(b)
SELECT * FROM table WHERE a=1 AND b=2;
优化器分析步骤:
步骤1:评估 INDEX(a)
- 预估 a=1 的行数:1000行(假设)
- 索引扫描成本:10
- 回表成本:1000 × 0.2 = 200
- 二次过滤b=2成本:1000 × 0.01 = 10
- 总成本:220
步骤2:评估 INDEX(b)
- 预估 b=2 的行数:100行(假设)
- 索引扫描成本:5
- 回表成本:100 × 0.2 = 20
- 二次过滤a=1成本:100 × 0.01 = 1
- 总成本:26
步骤3:选择成本最低的方案
→ 选择 INDEX(b),因为成本更低
3. 关键影响因素
(1) 索引区分度(选择性)
选择性公式:
选择性 = COUNT(DISTINCT column) / COUNT(*)
- 选择性越高(接近1),区分度越好
- 区分度好的索引过滤效果更强
示例:
-- 表有1000万行数据
-- a列有100个不同值 → 选择性 = 100/10000000 = 0.00001
-- b列有1万个不同值 → 选择性 = 10000/10000000 = 0.001
-- 优化器倾向选择 INDEX(b)
(2) 数据分布
即使区分度相同,实际数据分布也会影响选择。
-- 假设 a 和 b 的选择性相同
-- 但查询条件 a=1 匹配 1000行
-- 查询条件 b=2 匹配 10行
-- 优化器会选择 INDEX(b),因为过滤效果更好
(3) 统计信息准确性
MySQL依赖表的统计信息做决策:
-- 查看统计信息
SHOW INDEX FROM table;
-- 更新统计信息(如果优化器选择异常)
ANALYZE TABLE table;
三、索引合并(Index Merge)
1. 什么时候使用索引合并
在某些情况下,优化器可能同时使用两个索引:
EXPLAIN SELECT * FROM table WHERE a=1 AND b=2;
-- 可能的执行计划:
type: index_merge
key: idx_a,idx_b
Extra: Using intersect(idx_a,idx_b); Using where
2. 索引合并的类型
(1) Intersection(交集)
用于 AND 条件:
WHERE a=1 AND b=2
-- 执行步骤:
-- 1. 从 idx_a 获取 a=1 的 rowid 集合 → {1,3,5,7,9}
-- 2. 从 idx_b 获取 b=2 的 rowid 集合 → {2,3,6,7,10}
-- 3. 求交集 → {3,7}
-- 4. 根据 rowid 回表
(2) Union(并集)
用于 OR 条件:
WHERE a=1 OR b=2
-- 执行步骤:
-- 1. 从 idx_a 获取 a=1 的 rowid
-- 2. 从 idx_b 获取 b=2 的 rowid
-- 3. 求并集(去重)
-- 4. 根据 rowid 回表
3. 索引合并的触发条件
✅ 触发条件:
- 每个索引都能独立过滤大量数据
- 求交集/并集的成本低于单索引扫描
- 优化器版本支持(MySQL 5.0+)
❌ 不触发条件:
- 其中一个索引过滤效果很差
- 联合索引已存在(优先使用联合索引)
- 数据量太小(全表扫描更快)
四、实战场景分析
场景1:明显的选择性差异
-- 表数据:1000万行
-- gender: 只有 'M'/'F' 两个值(选择性 0.0000002)
-- user_id: 1000万个不同值(选择性 1.0)
SELECT * FROM users WHERE gender='M' AND user_id=12345;
-- 优化器必然选择 INDEX(user_id)
-- 因为 user_id 直接定位到1行,gender再过滤成本极低
场景2:区分度接近
-- age: 80个不同值(选择性 0.001)
-- city: 300个不同值(选择性 0.004)
SELECT * FROM users WHERE age=25 AND city='Beijing';
-- 优化器倾向选择 INDEX(city)
-- 但需要看实际数据分布:
-- - 如果 age=25 的人很少,可能选 INDEX(age)
-- - 如果 city='Beijing' 的人很少,可能选 INDEX(city)
场景3:统计信息过时
-- 问题:最近删除了90%的 age=25 的数据
-- 但统计信息未更新,优化器以为 age=25 还有很多行
-- 优化器可能做出错误选择
-- 解决:ANALYZE TABLE users;
场景4:强制指定索引
-- 如果优化器选择不理想,可以强制指定
SELECT * FROM table FORCE INDEX(idx_b)
WHERE a=1 AND b=2;
-- 或提示优化器
SELECT * FROM table USE INDEX(idx_b)
WHERE a=1 AND b=2;
五、性能对比
单索引 vs 索引合并 vs 联合索引
| 方案 | 执行过程 | 性能 | 适用场景 |
|---|---|---|---|
| 单索引 | 1次索引扫描 + 回表 + 二次过滤 | 中等 | 单列区分度足够高 |
| 索引合并 | 2次索引扫描 + 集合运算 + 回表 | 较低 | 两列都需要过滤大量数据 |
| 联合索引 | 1次索引扫描 + 回表 | 最优 | 经常组合查询 |
结论:联合索引 INDEX(a,b) 通常优于两个单独索引。
六、优化建议
1. 创建联合索引
-- 不推荐
INDEX(a), INDEX(b)
-- 推荐(如果经常组合查询)
INDEX(a, b)
原因:
- 联合索引包含两列信息,可以同时过滤
- 避免索引合并的额外开销
- 可能实现索引覆盖(无需回表)
2. 保留必要的单列索引
-- 如果有这些查询模式:
WHERE a=x AND b=y -- 高频
WHERE a=x -- 中频
WHERE b=y -- 低频
-- 索引方案:
INDEX(a, b) -- 覆盖前两种查询
INDEX(b) -- 单独查b时使用
3. 监控和调优
-- 查看索引使用情况
SELECT * FROM sys.schema_unused_indexes;
-- 查看慢查询
SELECT * FROM mysql.slow_log WHERE query_time > 1;
-- 分析具体查询
EXPLAIN FORMAT=JSON SELECT * FROM table WHERE a=1 AND b=2;
七、常见误区
误区1:认为会同时使用两个索引
❌ 错误:MySQL总是同时使用 INDEX(a) 和 INDEX(b)
✅ 正确:通常只选一个,除非触发索引合并
误区2:认为索引合并很高效
❌ 错误:索引合并是最优方案
✅ 正确:索引合并是妥协方案,联合索引更优
误区3:盲目创建联合索引
❌ 错误:为所有字段组合都创建联合索引
✅ 正确:根据查询频率和数据分布合理设计
八、面试总结
对于 WHERE a=xx AND b=xx 在有 INDEX(a) 和 INDEX(b) 的情况下:
优化器选择依据:
- 成本估算:计算每个索引的扫描、回表、过滤成本
- 选择性:倾向选择区分度更高的索引
- 数据分布:实际匹配行数更重要
- 索引合并:特定条件下可能同时使用两个索引
实际表现:
- 通常选择区分度更高或过滤效果更好的索引
- 使用另一个条件在回表后进行二次过滤
- 索引合并性能不如联合索引
优化建议:
- 高频组合查询应创建联合索引
INDEX(a,b) - 使用
EXPLAIN查看实际选择 - 定期
ANALYZE TABLE更新统计信息 - 必要时使用
FORCE INDEX强制指定
这道题考查对MySQL优化器成本模型和索引选择机制的理解。