1. 引言
MySQL 索引的最左前缀匹配原则是指使用联合索引时,查询条件必须从最左列开始匹配。联合索引包含多个列,查询时必须先有第一个列的条件,才能够匹配到第二个列、第三个列,以此类推。
这是因为 MySQL 中的联合索引使用的是 B+ 树,它的排序方式是从左到右依次排序。例如(a, b, c)在索引中的排序就是先对a进行排序,a相同,再对b进行排序,b相同,再对c进行排序。
MySQL 通过这个联合索引进行查找的时候,就会先通过a进行查询作为匹配依据,然后再查询b和c。如果跳过了a,那么b和c就没有办法使用到这个索引进行快速查找。
也就是说“大哥不能死”。
2. 符合匹配的场景
WHEREa=1;WHEREa=1ANDb=2;WHEREa=1ANDb=2ANDc=3;3. 不符合匹配的场景
WHEREb=1;WHEREc=1;WHEREb=1ANDc=2;查询条件中没有a,大哥死了,无法使用索引进行查询。在 MySQL 8 之前没有匹配到索引,只能走全表扫描。
4. 在 5.6 之后可以匹配的场景
WHEREa=1ANDc=2;在 5.6 之后 MySQL 进行了优化,增加了索引下推机制。在 5.6 之前,以上这种查询只能匹配到a,因为跳过了中间的b,所以c的查询无法使用索引;5.6 之后增加了索引下推,可以在查询到a以后,通过c过滤掉不符合的条件。
5. 查询优化机制
MySQL 的查询优化器可以优化查询条件的顺序,看如下代码:
WHEREb=1ANDa=2;-- 等价于WHEREa=2ANDb=1;MySQL 的查询优化器可以智能调整查询顺序,使查询条件可以使用到联合索引,所以查询时可以不用在意字段顺序,只要符合最左匹配原则就可以。
6. 范围查询下的场景
6.1 范围查询无法匹配的情况
WHEREa>1ANDb=2;WHEREa<3ANDb=2;在上面这种情况下,索引只会使用到a就停止,后面的b无法使用到索引。这是因为,遇到>、<的查询只匹配起点或终点,不包含边界的等值查询。a查询过后,不同的a值之间的b值是无序的,导致a值后面的查询条件无法使用到索引。
6.2 范围查询可以匹配的情况
WHEREa>=1ANDb=2;WHEREa<=1ANDb=2;这个查询可以匹配到a = 1的值,而a = 1的情况下b的值是有序的,所以后续的值可以应用到索引。
WHEREa=1ANDb=2ANDc>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 版本下的操作。我们先建立一张测试表:
CREATETABLE`idex_test`(`a`bigintNOTNULLAUTO_INCREMENT,`b`bigintDEFAULTNULL,`c`bigintDEFAULTNULL,PRIMARYKEY(`a`),KEY`idx_b_c`(`b`,`c`))ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;这张表很简单的三个字段,加上一个联合索引,最后再插入 200 条数据:
-- 先插入基础 10 条(b=1)INSERTINTOidex_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 条INSERTINTOidex_test(b,c)SELECTb,c+10FROMidex_testWHEREb=1;-- 变成 40 条INSERTINTOidex_test(b,c)SELECTb,c+20FROMidex_testWHEREb=1;-- 变成 80 条INSERTINTOidex_test(b,c)SELECTb,c+40FROMidex_testWHEREb=1;-- 补足 100 条INSERTINTOidex_test(b,c)SELECTb,c+80FROMidex_testWHEREb=1LIMIT20;-- 复制前 100 条,把 b 改成 2INSERTINTOidex_test(b,c)SELECT2,cFROMidex_testWHEREb=1LIMIT100;以上我们插入了 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 和 2。
- 把
b的值拼接到WHERE查询语句中:WHERE b = 1 AND c = 2和WHERE b = 2 AND c = 2。
就这样,你不符合最左前缀原则,我给你手动添加!看起来不需要,实际上还是没逃出最左前缀原则。
且这个应用条件很受限,需要前缀值的基数稳定。如果b的值有上万个,这样的添加修改还不如全表扫描。
甚至不需要上万的数据,我对这个前缀值b修改成随机数就无法应用了:
UPDATEidex_testSETb=2+RAND()*1000000000;现在没有Using index for skip scan了,变成了基于索引的全表扫描。
除了上面提到的这些,还有很多的局限性:
不能跨表查询,不能使用GROUP、DISTINCT等等。感兴趣可以自己看官方文档:range-access-skip-scan
8. 总结
创建联合索引的时候,字段的顺序很重要,注意以下几点:
- 高频查询放左边,以便索引的命中。
- 范围查询放最右边,以避免索引失效。
- 区分度高的字段放左边,方便筛选提高效率。
- 如果查询常用排序,把排序字段加入索引,避免
filesort。
更多分享:MySQL 索引类型