一、答案概要

当有两个单独索引 INDEX(a)INDEX(b),执行查询 WHERE a=xx AND b=xx 时:

MySQL会根据成本估算选择其中一个索引,通常选择

  1. 区分度更高(选择性更好)的索引
  2. 过滤效果更强(返回行数更少)的索引
  3. 在特定条件下可能使用索引合并(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) 的情况下:

优化器选择依据

  1. 成本估算:计算每个索引的扫描、回表、过滤成本
  2. 选择性:倾向选择区分度更高的索引
  3. 数据分布:实际匹配行数更重要
  4. 索引合并:特定条件下可能同时使用两个索引

实际表现

  • 通常选择区分度更高过滤效果更好的索引
  • 使用另一个条件在回表后进行二次过滤
  • 索引合并性能不如联合索引

优化建议

  • 高频组合查询应创建联合索引 INDEX(a,b)
  • 使用 EXPLAIN 查看实际选择
  • 定期 ANALYZE TABLE 更新统计信息
  • 必要时使用 FORCE INDEX 强制指定

这道题考查对MySQL优化器成本模型和索引选择机制的理解。