MySQL 复合索引:为什么不满足最左匹配有时也能走索引?(MySQL 8.x 新特性详解)

2026/3/9 笔记MySQL

# 前言

很多刚学 MySQL 的同学都会听到一句话:

复合索引必须满足最左匹配原则,否则索引不会生效。

但如果你在 MySQL 8.x 的环境中测试,会发现一个奇怪现象:

CREATE INDEX idx_user_age_city ON user(age, city);
1

查询:

SELECT * FROM user WHERE city = 'beijing';
1

居然也可能走索引!

这和我们熟悉的 最左匹配原则似乎矛盾。

实际上:

最左匹配原则仍然成立,但 MySQL 8.x 引入了一些优化机制,使得“看起来违反最左匹配”的查询仍然可以利用索引。

本文会从 底层原理 + 实际例子 + 执行计划,带你彻底理解这个问题。


# 一、什么是复合索引

复合索引(Composite Index)指:

一个索引包含多个列

例如:

CREATE INDEX idx_user_age_city ON user(age, city);
1

索引顺序是:

(age, city)
1

在 B+Tree 中的排序结构是:

(age1, city1)
(age1, city2)
(age1, city3)
(age2, city1)
(age2, city2)
(age3, city1)
1
2
3
4
5
6

可以理解为:

先按 age 排序
age 相同再按 city 排序
1
2

# 二、最左匹配原则(经典规则)

MySQL 使用复合索引时必须遵循:

从最左列开始连续匹配

索引:

(age, city, gender)
1

可以使用索引

age
age + city
age + city + gender
1
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';
1
2
3
4
5

无法使用索引(经典理解)

SELECT * FROM user WHERE city='shanghai';
1

因为:

缺少最左列 age
1

所以传统结论:

不会走索引

但在 MySQL 8.x 里,有时会发现:

type: index
1

或者

Using index
1

这是为什么?


# 三、MySQL 8.x 的优化:Index Skip Scan

MySQL 8.0 引入了一个优化:

Index Skip Scan(索引跳跃扫描)

它允许:

跳过最左列,直接利用后面的列查询

例如索引:

(age, city)
1

查询:

SELECT * FROM user WHERE city = 'beijing';
1

MySQL 会做什么?

逻辑类似:

遍历所有 age 值
    再在每个 age 内查 city='beijing'
1
2

伪代码:

for age in distinct(age):
    search (age, 'beijing')
1
2

也就是说:

(age1, beijing)
(age2, beijing)
(age3, beijing)
1
2
3

逐个扫描。


# 四、Index Skip Scan 的执行示意

假设索引:

(age, city)
1

索引结构:

(18, beijing)
(18, shanghai)
(19, beijing)
(20, shanghai)
(21, beijing)
1
2
3
4
5

查询:

SELECT * FROM user WHERE city='beijing';
1

MySQL 实际执行:

查找 (18, beijing)
查找 (19, beijing)
查找 (20, beijing)
查找 (21, beijing)
1
2
3
4

这就是:

跳过 age,只利用 city


# 五、什么时候 MySQL 会使用 Skip Scan

MySQL 不会无脑使用这个优化。

通常满足:

条件1:最左列基数很小

例如:

gender (2个值)
status (3个值)
1
2

例如索引:

(status, create_time)
1

查询:

WHERE create_time > '2025-01-01'
1

MySQL 可以:

status=0 + create_time
status=1 + create_time
status=2 + create_time
1
2
3

扫描 3 次即可。


条件2:统计信息允许

MySQL 会估算:

扫描成本
VS
全表扫描
1
2
3

如果 Skip Scan 更快才会使用。


条件3:索引不是特别大

如果最左列基数很大,例如:

user_id
1

那就不会使用。


# 六、EXPLAIN 示例

表:

CREATE TABLE user(
    id INT PRIMARY KEY,
    age INT,
    city VARCHAR(20),
    INDEX idx_age_city(age, city)
);
1
2
3
4
5
6

查询:

EXPLAIN SELECT * FROM user WHERE city='beijing';
1

可能看到:

type: range
key: idx_age_city
Extra: Using where
1
2
3

说明:

MySQL 使用了复合索引
1

即使:

没有 age 条件
1

# 七、Index Condition Pushdown(ICP)

另一个容易误解的优化是:

Index Condition Pushdown

ICP 作用是:

先用索引过滤
再回表
1
2

例如:

索引:

(age, city)
1

查询:

SELECT * FROM user WHERE age > 20 AND city='beijing';
1

流程:

扫描 age>20 的索引
在索引层判断 city='beijing'
减少回表
1
2
3

这也是:

索引看起来“用了更多列”

但实际上:

仍然满足最左匹配
1

# 八、覆盖索引(另一个常见原因)

如果查询字段全部在索引中:

SELECT city FROM user WHERE city='beijing';
1

即使索引:

(age, city)
1

MySQL 也可能:

直接扫描索引
1

执行计划:

type: index
Extra: Using index
1
2

这叫:

覆盖索引扫描

不是严格的索引查找。


# 九、总结:为什么不满足最左匹配也走索引

在 MySQL 8.x 中常见原因有 3 个:

原因 解释
Index Skip Scan 跳过最左列扫描
覆盖索引扫描 直接扫描索引
ICP 优化 在索引层过滤

因此:

最左匹配原则没有失效,只是 MySQL 更聪明了。


# 十、生产环境最佳实践

1 不依赖 Skip Scan

Skip Scan 只是优化:

性能不可控
1

生产索引仍然建议:

WHERE 常用字段放最左
1

例如:

(city, age)
1

如果查询:

WHERE city
1

2 高选择性字段放前

原则:

区分度高的列放前
1

例如:

user_id > city > gender
1

3 使用 EXPLAIN 验证

查看:

type
key
rows
extra
1
2
3
4

避免:

type = ALL
1

# 十一、一句话总结

记住一句核心原则:

复合索引仍然遵循最左匹配,只是 MySQL 8.x 通过 Skip Scan、ICP、覆盖索引等优化,让部分“不满足最左匹配”的查询也能利用索引。