MySQL 索引的最左前缀原则详解

大纲

最左前缀原则的介绍

最左前缀原则指的是:使用组合索引(联合索引)时,查询条件需要从索引的最左列开始匹配,并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效 (这一列后面的字段的索引会失效)。比如,针对 A、B、C 三个字段建立了一个组合索引,那么在写一个 SQL 时就一定要提供 A 字段的查询条件,这样才能让组合索引生效。这是由于在建立 A、B、C 三个字段的组合索引时,MySQL 底层的 B+ 树是按照 A、B、C 三个字段从左往右去比较大小进行排序的,也就是说 B+ 树的索引结构依赖于从左到右逐层递进地有序搜索路径。如果查询条件中缺少了最左边的列,B+ 树将无法确定初始的搜索路径,从而无法利用索引进行高效查询。

最左前缀原则的使用

使用案例一

组合索引 + where 子句的使用

使用案例二

组合索引 + where 子句的使用

假设 MySQL 8.0 数据库中有一张表 t_user,表里面有 idphonenameagestatus 这些字段,而且 phonenameage 这三个字段创建了组合索引 idx_phone_name_age

1
2
# 创建组合索引
mysql> create index idx_phone_name_age on t_user(phone, name, age);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 索引不会失效,因为遵循最左前缀原则
mysql> explain select * from t_user where phone = '123456' and name = 'Amy' and age = 20;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 221 | const,const,const | 1 | 100.00 | NULL |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------------------+------+----------+-------+

# 索引不会失效,最左前缀原则和过滤条件的顺序无关
mysql> explain select * from t_user where name = 'Amy' and age = 20 and phone = '123456';
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 221 | const,const,const | 1 | 100.00 | NULL |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------------------+------+----------+-------+

# 索引部分失效,因为跳跃了 name 这里一列
mysql> explain select * from t_user where phone = '123456' and age = 20;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 25.00 | Using index condition |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------+

# 索引全部失效,因为跳跃了 phone 这里一列,将不遵循最左前缀原则
mysql> explain select * from t_user where name = 'Amy' and age = 20;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | t_user | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 25.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+

使用案例三

组合索引 + where 子句 + order by 子句的使用

假设 MySQL 8.0 数据库中有一张表 t_user,表里面有 idphonenameagestatus 这些字段,而且 phonenameage 这三个字段创建了组合索引 idx_phone_name_age

  • 创建组合索引
1
mysql> create index idx_phone_name_age on t_user(phone, name, age);
  • 无过滤条件(如 where 条件,不包括 limit 条件),索引不会生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 索引全部失效,因为没有过滤条件
mysql> explain select * from t_user order by phone, name, age;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | t_user | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+

# 索引全部失效,因为没有过滤条件,不包括 `limit` 条件
mysql> explain select * from t_user order by phone, name, age limit 5;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | t_user | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+----------------+

# 索引不会失效,因为有过滤条件
mysql> explain select * from t_user where phone = '123456' order by phone, name, age;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 100.00 | NULL |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
  • 查询字段与过滤字段对索引的影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 索引部分生效
mysql> explain select * from t_user where phone = '123456' and age > 20 order by phone, name, age;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 33.33 | Using index condition |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------+

# 索引完全生效,因为触发了覆盖索引
mysql> explain select phone, age from t_user where phone = '123456' and age > 20 order by phone, name, age;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+--------------------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 33.33 | Using where; Using index |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+--------------------------+
  • 排序字段的顺序和索引的顺序不一致对索引的影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 排序字段的顺序和索引的顺序一致时,不会触发 `filesort`
mysql> explain select phone, age from t_user where phone = '123456' order by phone, name, age;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 100.00 | Using index |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------------+

# 排序字段的顺序和索引的顺序不一致时,会触发 `filesort`
mysql> explain select phone, age from t_user where phone = '123456' order by phone, age, name;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 100.00 | Using index; Using filesort |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------------+
  • 多个索引字段排序时,排序方向(升序、降序)不一致对索引的影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 多个索引字段排序时,排序方向(升序、降序)一致,不会触发 `filesort`
mysql> explain select phone, age from t_user where phone = '123456' order by phone desc, name desc, age desc;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+----------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+----------------------------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 100.00 | Backward index scan; Using index |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+----------------------------------+

# 多个索引字段排序时,排序方向(升序、降序)不一致,会触发 `filesort`
mysql> explain select phone, age from t_user where phone = '123456' order by phone desc, name asc, age desc;
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------------+
| 1 | SIMPLE | t_user | NULL | ref | idx_phone_name_age | idx_phone_name_age | 63 | const | 4 | 100.00 | Using index; Using filesort |
+----+-------------+--------+------------+------+--------------------+--------------------+---------+-------+------+----------+-----------------------------+

最左前缀原则的原理

最左前缀原则指的是在使用组合索引时,查询必须从索引的最左边开始匹配。换句话说,如果针对 A、B、C 三个字段建立了一个组合索引,那么在查询中需要至少包含字段 A,才能有效地利用索引。具体原因可以从 B+ 树的结构来分析。

B+ 树的结构

B+ 树是一种平衡树,每个节点包含多个键,并且所有的叶子节点处在同一层。B+ 树有以下几个特性:

  • 叶子节点之间形成一个有序的链表。
  • 所有的值(Value)都只在叶子节点上出现。
  • 非叶子节点只存储键(Key),用于指导搜索方向。

提示

更多关于 MySQL 索引底层的 B+ 树数据结构的介绍,请看 这里

底层原理分析

  • (1) B+ 树的键排序:B+ 树的非叶子节点包含键,这些键按照字典顺序排序。在组合索引 (A、B、C) 中,键的排序是先按照列 A 排序,在列 A 相同的情况下,再按照列 B 排序,最后按照列 C 排序。因此,B+ 树的节点实际上形成了一种层次排序结构。
  • (2) 搜索路径的确定:在 B+ 树中进行搜索时,从根节点开始,根据节点中的键判断搜索路径。例如,根节点包含列 A 的不同值范围,如果查询条件中不包含列 A,B+ 树无法判断该走哪条路径,从而无法使用索引。
  • (3) 最左前缀匹配:如果查询条件中包含了列 A,那么 B+ 树可以使用列 A 的值来定位到相应的子节点。如果查询条件进一步包含列 B 和列 C,B+ 树可以继续使用这些列的值来精确定位。但是,如果查询条件中缺少列 A,B+ 树将无法进行初步的路径选择,导致无法使用索引。

案例分析说明

假设有一个表 my_table,在 A、B、C 三个字段上建立了一个组合索引,数据如下:

ABC
123
132
211
222

B+ 树索引的结构如下:

1
2
3
4
5
6
7
       A
/ \
1 2
/ \ / \
B B B B
/ \ / \/ \ / \
2 3 1 2 1 2
  • 查询 SELECT * FROM my_table WHERE A = 1 AND B = 2 可以有效利用索引,因为从根节点 A 出发,找到值为 1 的分支,再在 B 节点找到值为 2 的分支。
  • 查询 SELECT * FROM my_table WHERE B = 2 则无法利用索引,因为无法确定从根节点 A 的哪个分支开始搜索。

总结

最左前缀原则之所以存在,正是因为 B+ 树的索引结构依赖于从左到右逐层递进地有序搜索路径。如果查询条件中缺少了最左边的列,B+ 树将无法确定初始的搜索路径,从而无法利用索引进行高效查询。

最佳实践

最佳实践一

组合索引的创建应该遵从以下规则:

  • (1) 在创建组合索引的时候,过滤性(唯一性)最好的字段在索引字段顺序中,位置越靠左边越好
  • (2) 组合索引出现范围查询时,应该尽量把这个字段放在索引顺序的最右边
  • (3) 在创建组合索引的时候,应该尽量包含 where 子句中的字段
  • (4) 应该尽量避免造成索引失效的情况

最佳实践二

索引的使用应该注意以下几点内容:

  • (1) 如果排序字段不在索引列上,MySQL 就会触发 filesort,导致查询性能降低;有两种排序算法:单路排序和双路排序
  • (2) 无过滤不索引,即如果没有过滤条件(如 where 条件,不包括 limit 条件),索引不会生效
  • (3) 在 SQL 语句中,如果索引字段的顺序写错了,会触发 filesort
  • (4) 使用 order by 查询时,如果索引字段的位置非最左,会触发 filesort
  • (5) 使用 order by 查询时,如果索引字段的排序方向(升序、降序)不一致,会触发 filesort
  • (6) 熟练使用 explain,必要时使用 optimizer_trace

最佳实践三

利用最左前缀原则优化 Order By 语句,最关键的几点如下:

  • (1) 在 SQL 语句中,一定要有过滤条件(如 where 条件),否则执行 order by 时索引会失效
  • (2) 首先要对 SQL 进行分析,检查必要的查询字段、过滤字段、排序字段是否按顺序创建好了素引
  • (3) 如果查询字段没有创建索引,可能会产生回表操作,导致触发 filesort,降低查询性能
  • (4) 排序字段的顺序和索引的顺序不一致时,会触发 filesort,降低查询性能
  • (5) 多个索引字段排序时,如果排序方向(升序、降序)不一致,会触发 filesort,降低查询性能
  • (6) 使用 explain 观察查询类型和索引利用情况
  • (7) 尽可能减少不必要的 filesort