MySQL 复合索引:为什么不满足最左匹配有时也能走索引?(MySQL 8.x 新特性详解)
# 前言
很多刚学 MySQL 的同学都会听到一句话:
复合索引必须满足最左匹配原则,否则索引不会生效。
但如果你在 MySQL 8.x 的环境中测试,会发现一个奇怪现象:
CREATE INDEX idx_user_age_city ON user(age, city);
查询:
SELECT * FROM user WHERE city = 'beijing';
居然也可能走索引!
这和我们熟悉的 最左匹配原则似乎矛盾。
实际上:
最左匹配原则仍然成立,但 MySQL 8.x 引入了一些优化机制,使得“看起来违反最左匹配”的查询仍然可以利用索引。
本文会从 底层原理 + 实际例子 + 执行计划,带你彻底理解这个问题。
# 一、什么是复合索引
复合索引(Composite Index)指:
一个索引包含多个列
例如:
CREATE INDEX idx_user_age_city ON user(age, city);
索引顺序是:
(age, city)
在 B+Tree 中的排序结构是:
(age1, city1)
(age1, city2)
(age1, city3)
(age2, city1)
(age2, city2)
(age3, city1)
2
3
4
5
6
可以理解为:
先按 age 排序
age 相同再按 city 排序
2
# 二、最左匹配原则(经典规则)
MySQL 使用复合索引时必须遵循:
从最左列开始连续匹配
索引:
(age, city, gender)
可以使用索引
age
age + city
age + city + gender
2
3
例如:
SELECT * FROM user WHERE age = 20;
SELECT * FROM user WHERE age = 20 AND city='shanghai';
SELECT * FROM user WHERE age = 20 AND city='shanghai' AND gender='male';
2
3
4
5
无法使用索引(经典理解)
SELECT * FROM user WHERE city='shanghai';
因为:
缺少最左列 age
所以传统结论:
不会走索引
但在 MySQL 8.x 里,有时会发现:
type: index
或者
Using index
这是为什么?
# 三、MySQL 8.x 的优化:Index Skip Scan
MySQL 8.0 引入了一个优化:
Index Skip Scan(索引跳跃扫描)
它允许:
跳过最左列,直接利用后面的列查询
例如索引:
(age, city)
查询:
SELECT * FROM user WHERE city = 'beijing';
MySQL 会做什么?
逻辑类似:
遍历所有 age 值
再在每个 age 内查 city='beijing'
2
伪代码:
for age in distinct(age):
search (age, 'beijing')
2
也就是说:
(age1, beijing)
(age2, beijing)
(age3, beijing)
2
3
逐个扫描。
# 四、Index Skip Scan 的执行示意
假设索引:
(age, city)
索引结构:
(18, beijing)
(18, shanghai)
(19, beijing)
(20, shanghai)
(21, beijing)
2
3
4
5
查询:
SELECT * FROM user WHERE city='beijing';
MySQL 实际执行:
查找 (18, beijing)
查找 (19, beijing)
查找 (20, beijing)
查找 (21, beijing)
2
3
4
这就是:
跳过 age,只利用 city
# 五、什么时候 MySQL 会使用 Skip Scan
MySQL 不会无脑使用这个优化。
通常满足:
条件1:最左列基数很小
例如:
gender (2个值)
status (3个值)
2
例如索引:
(status, create_time)
查询:
WHERE create_time > '2025-01-01'
MySQL 可以:
status=0 + create_time
status=1 + create_time
status=2 + create_time
2
3
扫描 3 次即可。
条件2:统计信息允许
MySQL 会估算:
扫描成本
VS
全表扫描
2
3
如果 Skip Scan 更快才会使用。
条件3:索引不是特别大
如果最左列基数很大,例如:
user_id
那就不会使用。
# 六、EXPLAIN 示例
表:
CREATE TABLE user(
id INT PRIMARY KEY,
age INT,
city VARCHAR(20),
INDEX idx_age_city(age, city)
);
2
3
4
5
6
查询:
EXPLAIN SELECT * FROM user WHERE city='beijing';
可能看到:
type: range
key: idx_age_city
Extra: Using where
2
3
说明:
MySQL 使用了复合索引
即使:
没有 age 条件
# 七、Index Condition Pushdown(ICP)
另一个容易误解的优化是:
Index Condition Pushdown
ICP 作用是:
先用索引过滤
再回表
2
例如:
索引:
(age, city)
查询:
SELECT * FROM user WHERE age > 20 AND city='beijing';
流程:
扫描 age>20 的索引
在索引层判断 city='beijing'
减少回表
2
3
这也是:
索引看起来“用了更多列”
但实际上:
仍然满足最左匹配
# 八、覆盖索引(另一个常见原因)
如果查询字段全部在索引中:
SELECT city FROM user WHERE city='beijing';
即使索引:
(age, city)
MySQL 也可能:
直接扫描索引
执行计划:
type: index
Extra: Using index
2
这叫:
覆盖索引扫描
不是严格的索引查找。
# 九、总结:为什么不满足最左匹配也走索引
在 MySQL 8.x 中常见原因有 3 个:
| 原因 | 解释 |
|---|---|
| Index Skip Scan | 跳过最左列扫描 |
| 覆盖索引扫描 | 直接扫描索引 |
| ICP 优化 | 在索引层过滤 |
因此:
最左匹配原则没有失效,只是 MySQL 更聪明了。
# 十、生产环境最佳实践
1 不依赖 Skip Scan
Skip Scan 只是优化:
性能不可控
生产索引仍然建议:
WHERE 常用字段放最左
例如:
(city, age)
如果查询:
WHERE city
2 高选择性字段放前
原则:
区分度高的列放前
例如:
user_id > city > gender
3 使用 EXPLAIN 验证
查看:
type
key
rows
extra
2
3
4
避免:
type = ALL
# 十一、一句话总结
记住一句核心原则:
复合索引仍然遵循最左匹配,只是 MySQL 8.x 通过 Skip Scan、ICP、覆盖索引等优化,让部分“不满足最左匹配”的查询也能利用索引。