MySQL 索引的最左前缀匹配原则是什么?

Sherwin.Wei Lv7

MySQL 索引的最左前缀匹配原则是什么?

回答重点

MySQL 索引的最左前缀匹配原则指的是在使用联合索引时,查询条件必须从索引的最左侧开始匹配。如果一个联合索引包含多个列,查询条件必须包含第一个列的条件,然后是第二个列,以此类推。

底层原理:因为联合索引在 B+ 树中的排列方式遵循“从左到右”的顺序,例如联合索引 (first_name, last_name, age) 会按照 (first_name, last_name, age) 的顺序在 B+ 树中进行排序。

MySQL 在查找时会优先使用 first_name 作为匹配依据,然后依次使用 last_nameage。因此,组合索引能够从左到右依次高效匹配,跳过最左侧字段会导致无法利用该索引。

按照 (first_name, last_name, age) 的顺序在 B+ 树中的排列方式(大致的示意图)如下

1
2
3
4
5
                      (Alice, Black, 35)
|
-------------------------------------------------------------
| | | |
(Alice, Brown, 25) (Alice, Smith, 30) (Bob, Smith, 20) (Bob, White, 19)

扩展知识

举例说明

假设当前表有一个联合索引(a,b,c)。

以下查询条件 符合 最左匹配原则:

1
2
3
where a=1;
where a=1, b=2;
where a=1, b=2, c=3;

以下查询条件 不符合 最左匹配原则:

1
2
3
where b=2;
where c=3;
where b=2, c=3;

再来看下这个查询:

1
where a=1, c=3;

这个查询可能可以用上联合索引(a,b,c),在 5.6版本之前,即使用上也只能利用 a=1 这个条件来过滤。但是在 MySQL 5.6 版本后有个优化,即索引下推,可以在引擎层面,查询得到 a=1 的数据之后,利用 c 过滤掉不符合的数据,再返回给 server 层。

注意范围查询

在理解范围查询不同点之前,需要理解联合索引的顺序

例如联合索引(a,b,c),在 B+树的中排序是:先按 a 的值排序,如果 a 的值相同,则按 b 的值排序,如果 b 的值相同,再按 c 的值排序。

类似下图的排序:
image.png

如果遇到范围查询(>、<),就会停止匹配

1
where a>1, b=2, c=3;

所谓的停止匹配指的是:a 可以用上联合索引,但是 b 和 c 却不行。因为 a 需要先经过范围查询,此时经过筛选得到 b 和 c 的数据是无序的。比如 a 为 1 和 a 为 2 数据之间 b 和 c 是无序的,因此无法利用索引查询。

如果遇到如 >=、<=、BETWEEN、前缀like(xx%)的范围查询,则不会停止匹配。因为这些查询包含一个等值判断,可以直接定位到某个数据,然后往后扫描即可。

例如:

1
where a>=1, b=2, c=3;

查询可以定位到 a=1 这条数据,然后往后遍历即可,而定位到的 a=1 数据内,b 和 c 是有序的,因此可以利用索引查询

而以下的查询则可以全部用上联合索引:

1
where a=1, b=2, c>3;

因为 a 和 b 是等值查询的情况下,c 是有序的,有序就可以用上索引。

补充说明:拿 where a>=1, b=2,再来理解一下。因为 a = 1 这个条件,再加上 b = 2,mysql 可以直接定位到 a=1 and b=2 这条数据,然后再往后遍历,因此说用上了 b(因为确实利用了 b 的条件快速定位到第一条记录)。但是要注意往后遍历即 扫描 a > 1 的情况是用不到 b 的。

MySQL 8 都不需要最左匹配就能用上联合索引了?

以下实验基于 MySQL8.0.26 版本

为了故事顺利的发展,我们先建个表。

1
2
3
4
5
6
7
CREATE TABLE `t1` (
`f1` int NOT NULL AUTO_INCREMENT,
`f2` int NOT NULL,
`f3` int NOT NULL,
PRIMARY KEY (`f1`),
KEY `idx_f2_f3` (`f2`,`f3`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

很简单的一张表,一个主键索引,一个联合索引idx_f2_f3

此时插入一波数据

1
2
3
4
5
6
7
INSERT INTO t1 (f2,f3) VALUES
(1,1), (1,2), (1,3), (1,4), (1,5),
(2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1(f2,f3) SELECT f2, f3 + 5 FROM t1;
INSERT INTO t1(f2,f3) SELECT f2, f3 + 10 FROM t1;
INSERT INTO t1(f2,f3) SELECT f2, f3 + 20 FROM t1;
INSERT INTO t1(f2,f3) SELECT f2, f3 + 40 FROM t1;

这个时候表里一共有160条数据,f2 的值基数很低,只有1、2这两个值,基数为2

数据准备完了,咱们可以开始操作一下了,此时执行:

image.png

先执行了一下ANALYZE TABLE t1; 更新了一下表的统计信息,防止 MySQL 误判。

简单执行了一个 f2 为条件的联合索引查询,且结果只要求返回 f2 和 f3,所以从这个联合索引可以直接得到值,不需要回表。

这个查询的 explain 结果相信大家也都能理解,用上了 idx_f2_f3,且 type 是ref,说明用上了这个索引查询。

现在我们换一个查询条件,让 f3 作为条件来查询,这个时候就不是最左匹配原则了,讲道理应该用不上索引查询。

image.png

看看这结果,好像不太对?type 是 range,这是基于索引的范围扫描,所以用上了索引!

但是我的查询条件 f3=1 明明是等值呀?怎么就用上了范围扫描呢?

还有 Extra 里面显示的Using index for skip scan又是啥?

Using index for skip scan

让我们直接进行一个官方文档的查询!

image.png

在优化里面的范围优化里确实有Skip Scan Range Access Method,它就是产生上述 Extra 的原因。

好了,可以忘了官网这个截图了,基于我展示的例子我来解释下。

根据我们所认知的最左匹配原则,执行

  • where f3 =1

是无法在idx_f2_f3上利用 type 是 ref 的查询方式来查询的。

因为只有带上索引左边的查询条件才能用上索引,例如:

  • where f2=1 and f3 =1

那如果无法利用索引进行查询,最终只有一个选择,全表扫描:要么全表扫描主键索引,要么全表扫描二级索引。

MySQL 就意识到这效率确实低,能不能找个优化点呢?

所以就在 8.0.13 版本引入了Skip Scan Range Access Method,它在一定条件下,利用了范围扫描来替代了全表扫描的发生。

具体原理如下:

  1. 先统计一下索引最左字段 f2 的基数即唯一值,这里一共有 1 和 2 两个值。
  2. 然后将 f2 的值拼入查询条件中构造 where f2=1 and f3=1where f2=2 and f3=1这样的条件进行查询。

这波是不是小秀?

你 SQL 不带索引最左的条件是吧,我自个儿给你拼上去!

所以一条查询就变成了多次查询,所以 type 就变成了 range 了。

因此最终还是没有逃出最左匹配原则,只是 MySQL 隐式的构造了查询条件,使得看起来好像不需要最左匹配原则而已。

这个优化的中文名叫:跳过扫描范围访问(我直译的,哈哈哈)

跳过扫描范围访问的局限性

不要高兴的太早,这个优化其实有很大的局限性!

它要求最左条件的基数很低,因为它需要列举最左条件的值来构造查询,假如最左条件有上万个,那还不如直接进行全表扫描了对吧。

其实也不需要上万个,我来改一下表里的数据来做一次实验。

image.png

我直接把 f2 的值随机化了,这样 f2 的基数就变大了,可以看到此时再执行查询语句就用不上了Using index for skip scan了。

变成了一个基于二级索引的全表扫描

除了上面这个关键点之外,还有很多局限性,我根据官网的内容做了一下翻译(和一点便于理解的小修改):

  • 需要联合索引
  • 查询不能跨表
  • 查询不能使用 GROUP BY 或者 DISTINCT
  • 查询只能用一个索引,即索引需要覆盖查询的值,不能回表
  • 查询条件必须是常量,包括 IN() 运算符。

还有两条我不翻译了,我觉得讲的是废话…有点兴趣的自己去看看吧,文末会放链接。

总而言之,能用上这个优化的场景很有限,就基数很低这一个限制我就觉得很大了。

所以这也不是什么万能药,就当一个知识点咱们知晓下,到时候遇到特殊场景能用上的时候装一下,或者在面试官问你最左匹配的时候,提一下,我知道那个 MySQL 8.0.13 版本啊,它做了个优化…..

Comments