上一篇我们讲了 MySQL 索引为什么能加快查询,以及 InnoDB 为什么常用 B+Tree。
现在来看一个实际开发中更常见的问题。
假设有一张用户表:
sql
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT,
city VARCHAR(50)
);
我们经常需要查询:
sql
SELECT *
FROM user
WHERE name = '张三'
AND age = 20;
这时候应该怎么建索引?
text
给 name 建一个索引?
给 age 建一个索引?
还是给 name + age 一起建索引?
这就涉及 MySQL 中非常重要的:
联合索引 和最左匹配原则。
1. 什么是联合索引?
普通索引通常只包含一个字段:
sql
CREATE INDEX idx_name
ON user(name);
而联合索引,也叫复合索引,可以包含多个字段:
sql
CREATE INDEX idx_name_age
ON user(name, age);
它的意思是:
把
name和age作为一组索引键,建立一棵 B+Tree。
注意,不是:
text
name 一棵 B+Tree
+
age 一棵 B+Tree
而是:
text
(name, age)
↓
一棵 B+Tree
这两个字段共同决定索引中的排序方式。
2. 联合索引到底怎么排序?
假设表中有这些数据:
| name | age |
|---|---|
| 李四 | 22 |
| 张三 | 25 |
| 王五 | 20 |
| 张三 | 18 |
| 李四 | 30 |
| 张三 | 20 |
建立:
sql
CREATE INDEX idx_name_age
ON user(name, age);
以后,可以把索引的逻辑顺序理解成:
text
李四, 22
李四, 30
张三, 18
张三, 20
张三, 25
王五, 20
这里为了方便理解,假设字符串按照上面展示的顺序排列;真实 MySQL 中字符串的比较顺序由字符集和排序规则决定。
最重要的是:
先按照 name 排序,name 相同的时候,再按照 age 排序。
也就是:
text
第一排序字段:name
第二排序字段:age
这就是联合索引最核心的结构。
3. 为什么 name = '张三' 可以使用联合索引?
假设执行:
sql
SELECT *
FROM user
WHERE name = '张三';
因为索引首先按照 name 排序:
text
李四, 22
李四, 30
张三, 18 ←
张三, 20 ←
张三, 25 ←
王五, 20
所有:
text
name = '张三'
的记录在索引中形成一个连续范围。
数据库可以通过 B+Tree 快速定位到这个范围,然后顺序扫描。
所以:
sql
WHERE name = '张三'
可以很好地利用:
text
(name, age)
这个联合索引。
4. 为什么 name + age 一起查询更适合?
现在执行:
sql
SELECT *
FROM user
WHERE name = '张三'
AND age = 20;
索引顺序是:
text
张三, 18
张三, 20
张三, 25
数据库先通过:
text
name = '张三'
找到对应范围。
然后在这个范围内继续利用:
text
age = 20
缩小查找范围。
可以理解成:
text
B+Tree
↓
定位 name = 张三
↓
继续定位 age = 20
↓
找到目标记录
所以:
sql
WHERE name = '张三'
AND age = 20
通常可以充分利用:
text
idx_name_age
的两个索引列。
5. 那只查询 age 为什么不一样?
现在执行:
sql
SELECT *
FROM user
WHERE age = 20;
索引还是:
text
李四, 22
李四, 30
张三, 18
张三, 20
王五, 20
你会发现:
text
age = 20
的数据并没有因为 age 相同,就集中在一个连续的大范围里。
因为索引首先按照:
text
name
排序。
只有:
text
name 相同
的时候,才继续按照:
text
age
排序。
所以只知道:
text
age = 20
数据库无法像查询 name 那样,直接定位到一个简单、连续的索引范围。
这就是最左匹配原则的由来。
6. 什么是最左匹配原则?
对于联合索引:
text
(a, b, c)
可以简单理解成:
索引首先按照 a 排序,a 相同再按照 b 排序,a、b 都相同再按照 c 排序。
因此,最容易利用这个索引的查询是:
text
a
a + b
a + b + c
例如:
sql
CREATE INDEX idx_a_b_c
ON test(a, b, c);
常见情况:
| 查询条件 | 是否符合最左前缀 |
|---|---|
a = 1 |
是 |
a = 1 AND b = 2 |
是 |
a = 1 AND b = 2 AND c = 3 |
是 |
b = 2 |
不符合 |
c = 3 |
不符合 |
b = 2 AND c = 3 |
不符合 |
a = 1 AND c = 3 |
只具备 a 这个连续前缀 |
这里要特别注意:
"不符合最左前缀"不等于"数据库绝对不会使用这个索引"。 MySQL 可能进行索引扫描、覆盖索引扫描,或者在特定版本和条件下使用其他优化方式。最左匹配主要描述的是联合索引能否有效用于按键值定位和缩小查找范围。
7. 为什么叫最左?
因为联合索引的字段顺序是:
text
(a, b, c)
而不是:
text
(c, b, a)
例如:
sql
CREATE INDEX idx_name_age_city
ON user(name, age, city);
逻辑排序:
text
先 name
name 相同,再 age
name、age 都相同,再 city
所以:
text
name
是最左边。
接着:
text
name + age
是连续前缀。
再接着:
text
name + age + city
也是连续前缀。
但:
text
age + city
跳过了最左边的 name,就不再具备同样的定位条件。
8. WHERE 条件顺序会影响最左匹配吗?
这是一个非常常见的误区。
假设有:
sql
CREATE INDEX idx_name_age
ON user(name, age);
下面两个 SQL:
sql
SELECT *
FROM user
WHERE name = '张三'
AND age = 20;
以及:
sql
SELECT *
FROM user
WHERE age = 20
AND name = '张三';
很多人会认为第二个 SQL:
text
age 写在前面
所以不符合最左匹配。
其实不是。
最左匹配看的是索引字段顺序,不是 SQL 中 WHERE 条件的书写顺序。
MySQL 优化器可以重新组织这些等值条件。
所以这两个 SQL 在通常情况下都可以利用:
text
(name, age)
联合索引。
真正重要的是:
text
索引定义:
(name, age)
而不是你把:
text
name = ...
写在 WHERE 的第几行。
9. 联合索引遇到范围查询会怎么样?
现在假设有:
sql
CREATE INDEX idx_a_b_c
ON test(a, b, c);
执行:
sql
SELECT *
FROM test
WHERE a = 1
AND b > 10
AND c = 3;
这里:
text
a = 1
是等值条件。
text
b > 10
是范围条件。
索引顺序:
text
a → b → c
数据库可以先定位:
text
a = 1
再定位:
text
b > 10
对应的范围。
但是在这个范围内:
text
c = 3
通常无法继续像前面的等值条件那样,把 B+Tree 查找范围进一步缩小成一个连续区间。
可以理解成:
text
a = 1
↓
定位范围
b > 10
↓
继续缩小范围
c = 3
↓
通常作为后续过滤条件
这就是经常听到的:
联合索引遇到范围条件后,后面的列通常不能继续用于缩小索引查找范围。
但这里还有一个重要细节。
10. 范围后面的字段是不是完全没用了?
不是。
例如:
sql
WHERE a = 1
AND b > 10
AND c = 3;
虽然 c 通常不能继续缩小索引定位范围,但它仍然可能用于:
text
索引条件下推(Index Condition Pushdown)
也就是:
text
先扫描符合 a、b 范围的索引记录
↓
在索引层判断 c = 3
↓
符合条件的再继续读取完整行
所以不能简单说:
text
范围查询后面的索引列全部失效
更加准确的是:
后续列通常不能继续用于范围定位,但仍可能参与索引过滤,甚至用于覆盖索引。
这一点也是面试中比较容易被问到的细节。
11. 为什么联合索引字段顺序很重要?
假设业务经常查询:
sql
SELECT *
FROM user
WHERE name = '张三'
AND age = 20;
同时也经常查询:
sql
SELECT *
FROM user
WHERE name = '张三';
那么:
sql
CREATE INDEX idx_name_age
ON user(name, age);
通常比较合适。
因为它同时可以服务:
text
name
name + age
两类查询。
但如果创建:
sql
CREATE INDEX idx_age_name
ON user(age, name);
虽然:
text
age + name
一起等值查询也可以很好地利用索引,
但是:
sql
WHERE name = '张三'
就不再具备最左前缀。
因此:
联合索引字段顺序应该根据实际查询条件来设计,而不是随便排列。
12. 是不是区分度最高的字段一定放最左边?
也不是。
很多文章会说:
联合索引应该把区分度最高的字段放在最左边。
这只能算一种参考,不能当成绝对规则。
例如业务经常查询:
sql
WHERE status = 1
AND create_time > '2026-01-01';
以及:
sql
WHERE status = 1;
那么:
sql
(status, create_time)
可能比:
sql
(create_time, status)
更适合这些查询。
因为我们需要综合考虑:
text
常用查询条件
等值条件和范围条件
ORDER BY
GROUP BY
覆盖索引
字段选择性
所以联合索引设计并不是:
text
谁区分度高谁永远放最左边
而应该是:
根据实际 SQL 的过滤、排序和覆盖需求,选择合适的字段顺序。
13. 联合索引还能帮助 ORDER BY
假设有:
sql
CREATE INDEX idx_name_age
ON user(name, age);
执行:
sql
SELECT *
FROM user
WHERE name = '张三'
ORDER BY age;
因为索引内部:
text
name 相同的时候
↓
按照 age 排序
所以:
text
张三, 18
张三, 20
张三, 25
本身就是有序的。
数据库就有机会直接利用索引顺序:
text
定位 name = 张三
↓
按照 age 顺序扫描
而不需要额外做一次排序。
这也是联合索引除了加快 WHERE 查询以外,另一个非常重要的作用:
text
帮助 ORDER BY
当然,是否最终使用索引顺序,还要看查询条件、排序方向、执行计划等因素。
14. 什么是覆盖索引?
上一篇我们提到过:
text
二级索引
↓
找到主键
↓
回表
↓
读取完整行
例如:
sql
CREATE INDEX idx_name_age
ON user(name, age);
执行:
sql
SELECT *
FROM user
WHERE name = '张三';
如果需要读取:
text
id
name
age
city
而二级索引中没有完整的 city 数据,就可能需要回表。
但是如果执行:
sql
SELECT name, age
FROM user
WHERE name = '张三';
需要的字段:
text
name
age
都已经存在于:
text
idx_name_age
中。
那么数据库就可能直接从索引中返回结果:
text
二级索引
↓
找到 name、age
↓
直接返回
不需要再读取完整行。
这就是:
text
覆盖索引
简单理解:
查询需要的字段,索引本身已经全部包含了。
15. 联合索引和覆盖索引有什么关系?
联合索引:
text
(name, age)
描述的是:
text
索引由哪些字段组成
而覆盖索引描述的是:
text
某一次查询需要的字段
是否都能从索引中获取
所以:
text
联合索引
不等于:
text
覆盖索引
但是联合索引经常可以帮助实现覆盖索引。
例如:
sql
CREATE INDEX idx_name_age
ON user(name, age);
查询:
sql
SELECT name, age
FROM user
WHERE name = '张三';
就有机会:
text
使用联合索引
+
不需要回表
这就是它们之间的关系。
16. 联合索引是不是越多字段越好?
假设我们创建:
sql
CREATE INDEX idx_all
ON user(name, age, city, status, create_time);
看起来:
text
什么字段都有
是不是很完美?
当然不是。
索引字段越多:
text
索引项越大
可能导致:
text
一个数据页能容纳的索引项更少
索引占用更多磁盘
INSERT / UPDATE / DELETE 维护成本增加
而且:
text
(name, age, city, status, create_time)
也不是所有查询都能充分利用。
例如:
sql
WHERE city = '北京';
依然不具备最左前缀。
所以:
联合索引不是把所有字段都塞进去,而是根据常用查询设计一组有价值的字段。
17. 一个实际开发中的索引设计例子
假设订单表:
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status INT,
create_time DATETIME,
amount DECIMAL(10, 2)
);
常见查询:
sql
SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1
ORDER BY create_time DESC;
可以考虑:
sql
CREATE INDEX idx_user_status_time
ON orders(user_id, status, create_time);
为什么?
因为:
text
user_id = 10001
↓
先定位某个用户
status = 1
↓
继续缩小状态范围
create_time
↓
帮助按时间顺序读取
所以这个索引能够同时考虑:
text
WHERE 过滤
+
ORDER BY 排序
当然,实际项目中是否最优,还需要结合数据量、选择性和 EXPLAIN 验证。
但这已经是一种比较典型的联合索引设计思路。
18. 怎么判断联合索引有没有真正用上?
不能只靠自己猜。
MySQL 提供了:
sql
EXPLAIN
例如:
sql
EXPLAIN
SELECT *
FROM user
WHERE name = '张三'
AND age = 20;
重点可以先看:
text
key
type
rows
Extra
其中:
text
key
表示优化器实际选择的索引。
如果看到:
text
idx_name_age
说明查询使用了这个索引。
但还需要注意:
key显示使用了联合索引,不代表所有索引列都用于缩小查找范围。
例如:
sql
WHERE a = 1
AND b > 10
AND c = 3;
可能使用:
text
idx_a_b_c
但并不代表:
text
a、b、c
全部都参与了同样程度的索引定位。
所以后面学习 EXPLAIN 时,还需要结合:
text
key_len
rows
Extra
实际执行计划
进一步分析。
19. 最左匹配原则最容易记错的三个地方
第一:最左匹配不是 WHERE 书写顺序。
sql
WHERE age = 20
AND name = '张三'
只要索引是:
text
(name, age)
并且条件合适,优化器仍然可以利用这个联合索引。
第二:跳过最左列,不代表索引绝对不能被使用。
例如:
sql
WHERE age = 20
不符合:
text
(name, age)
的最左前缀,但 MySQL 仍可能选择索引扫描等执行方式。
第三:范围后面的字段,不是完全没有作用。
例如:
sql
WHERE a = 1
AND b > 10
AND c = 3
c 通常不能继续缩小索引范围,但仍可能参与索引条件下推或覆盖索引。