Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL联合索引最左匹配原则

MySQL联合索引最左匹配原则是什么?一文详解索引失效场景

作者:Tang_tzb

还在为MySQL联合索引失效而困惑吗?本文用最直白的语言讲透最左前缀匹配原则,告诉你为什么“大哥不能死”、范围查询何时会中断索引,以及MySQL8的跳跃查询如何突破限制,学会设计联合索引,避免全表扫描,让你的查询性能飞起来

1. 引言

MySQL 索引的最左前缀匹配原则是指使用联合索引时,查询条件必须从最左列开始匹配。

联合索引包含多个列,查询时必须先有第一个列的条件,才能够匹配到第二个列、第三个列,以此类推。

这是因为 MySQL 中的联合索引使用的是 B+ 树,它的排序方式是从左到右依次排序。

例如 (a, b, c) 在索引中的排序就是先对 a 进行排序,a 相同,再对 b 进行排序,b 相同,再对 c 进行排序。

MySQL 通过这个联合索引进行查找的时候,就会先通过 a 进行查询作为匹配依据,然后再查询 b 和 c。如果跳过了 a,那么 b 和 c 就没有办法使用到这个索引进行快速查找。

也就是说“大哥不能死”。

2. 符合匹配的场景

WHERE a = 1;
WHERE a = 1 AND b = 2;
WHERE a = 1 AND b = 2 AND c = 3;

3. 不符合匹配的场景

WHERE b = 1;
WHERE c = 1;
WHERE b = 1 AND c = 2;

查询条件中没有 a,大哥死了,无法使用索引进行查询。在 MySQL 8 之前没有匹配到索引,只能走全表扫描。

4. 在 5.6 之后可以匹配的场景

WHERE a = 1 AND c = 2;

在 5.6 之后 MySQL 进行了优化,增加了索引下推机制。

在 5.6 之前,以上这种查询只能匹配到 a,因为跳过了中间的 b,所以 c 的查询无法使用索引;5.6 之后增加了索引下推,可以在查询到 a 以后,通过 c 过滤掉不符合的条件。

5. 查询优化机制

MySQL 的查询优化器可以优化查询条件的顺序,看如下代码:

WHERE b = 1 AND a = 2;
-- 等价于
WHERE a = 2 AND b = 1;

MySQL 的查询优化器可以智能调整查询顺序,使查询条件可以使用到联合索引,所以查询时可以不用在意字段顺序,只要符合最左匹配原则就可以。

6. 范围查询下的场景

6.1 范围查询无法匹配的情况

WHERE a > 1 AND b = 2;
WHERE a < 3 AND b = 2;

在上面这种情况下,索引只会使用到 a 就停止,后面的 b 无法使用到索引。

这是因为,遇到 >、< 的查询只匹配起点或终点,不包含边界的等值查询。a 查询过后,不同的 a 值之间的 b 值是无序的,导致 a 值后面的查询条件无法使用到索引。

6.2 范围查询可以匹配的情况

WHERE a >= 1 AND b = 2;
WHERE a <= 1 AND b = 2;

这个查询可以匹配到 a = 1 的值,而 a = 1 的情况下 b 的值是有序的,所以后续的值可以应用到索引。

WHERE a = 1 AND b = 2 AND c > 1;

这个查询可以使用到索引,因为 a 和 b 的值都是等值查询。

这种情况下 a 和 b 的值都是有序的,以这个为前提 c 也是有序的,c 的值也可以应用到索引。

除此之外,遇到像 BETWEEN、前缀 LIKE 'xx%' 这种范围查询,也不会停止查询,因为包含等值查询,可以接着往后扫描。

7. MySQL 8 之后出现的跳跃查询

MySQL 8.0.13 之后出现了跳跃查询 Skip Scan Range Access Method,在一些特殊的场景下能够越过最左匹配原则强制使用联合索引。

以下是文档截图,文档地址我放在文末。

以下的操作都是在 MySQL 8.0.13 版本下的操作。我们先建立一张测试表:

CREATE TABLE `idex_test` (
  `a` bigint NOT NULL AUTO_INCREMENT,
  `b` bigint DEFAULT NULL,
  `c` bigint DEFAULT NULL,
  PRIMARY KEY (`a`),
  KEY `idx_b_c` (`b`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

这张表很简单的三个字段,加上一个联合索引,最后再插入 200 条数据:

-- 先插入基础 10 条(b=1)
INSERT INTO idex_test (b, c) VALUES
  (1,1), (1,2), (1,3), (1,4), (1,5),
  (1,6), (1,7), (1,8), (1,9), (1,10);

-- 变成 20 条
INSERT INTO idex_test (b, c)
SELECT b, c + 10
FROM idex_test
WHERE b = 1;

-- 变成 40 条
INSERT INTO idex_test (b, c)
SELECT b, c + 20
FROM idex_test
WHERE b = 1;

-- 变成 80 条
INSERT INTO idex_test (b, c)
SELECT b, c + 40
FROM idex_test
WHERE b = 1;

-- 补足 100 条
INSERT INTO idex_test (b, c)
SELECT b, c + 80
FROM idex_test
WHERE b = 1
LIMIT 20;

-- 复制前 100 条,把 b 改成 2
INSERT INTO idex_test (b, c)
SELECT 2, c
FROM idex_test
WHERE b = 1
LIMIT 100;

以上我们插入了 200 条记录,其中 b 的值只会是 1 或者 2。

我们先试试正常命中索引看看是什么情况:

我们先查询 b、c 的值,查询条件 b = 2,type 是 ref,说明正常使用到了索引。

我们的联合索引是 (b, c),查询 b、c 是为了不回表可以直接覆盖索引。

下面我们来看看 MySQL 8 的新特性:

我们的查询并没有匹配最左前缀原则,但我们的 type 是 range,且 key 是我们的 idx_b_c,说明用到了索引内的范围扫描,extra 还出现了 Using index for skip scan,这就是 MySQL 8 的跳跃查询。查看过文档发现,其实它的底层机制仍然是最左前缀原则,且应用面还很窄。

具体原理如下:

  1. 当没有匹配最左前缀原则时,统计前缀的值,发现只有 1 和 2。
  2. 把 b 的值拼接到 WHERE 查询语句中:WHERE b = 1 AND c = 2 和 WHERE b = 2 AND c = 2。

就这样,你不符合最左前缀原则,我给你手动添加! 看起来不需要,实际上还是没逃出最左前缀原则。

且这个应用条件很受限,需要前缀值的基数稳定。如果 b 的值有上万个,这样的添加修改还不如全表扫描。

甚至不需要上万的数据,我对这个前缀值 b 修改成随机数就无法应用了:

UPDATE idex_test SET b = 2 + RAND()*1000000000;

现在没有 Using index for skip scan 了,变成了基于索引的全表扫描。

除了上面提到的这些,还有很多的局限性:

不能跨表查询,不能使用 GROUP、DISTINCT 等等。感兴趣可以自己看官方文档:range-access-skip-scan

8. 总结

创建联合索引的时候,字段的顺序很重要,注意以下几点:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文