MySQL 联合索引范围查询:最左匹配原则的一个学习笔记
前情提要
最近看到一篇关于 MySQL 联合索引的文章,里面纠正了一个很常见的说法:
联合索引的最左匹配原则,会一直向右匹配,直到遇到范围查询(
>、<、between、like)就停止。
这句话在很多资料里都能看到,我以前也会把它当成一个比较简单的记忆结论。但仔细看完之后发现,它其实不够准确。
更准确的理解应该是:
联合索引在遇到某些范围查询时,后续字段可能无法继续参与构造索引扫描区间;但并不是所有范围查询都会让后续字段完全失效。像
>=、<=、BETWEEN、LIKE 'xxx%'这类场景,在 MySQL 中仍然可能继续利用联合索引中的后续字段。
这篇文章就按学习笔记的方式,把这个问题重新梳理一下。
先回顾一下 B+Tree 索引
InnoDB 中常见的索引底层结构是 B+Tree。
对于主键索引来说,叶子节点存放的是完整的行数据,所以主键索引也叫聚簇索引。
对于二级索引来说,叶子节点存放的是索引列的值以及对应的主键值。如果通过二级索引查到了主键,再回到主键索引中查完整行数据,这个过程就是回表。
比如:
1 | |
如果 product_no 上有二级索引,查询会先在 product_no 的索引树上找到对应记录,再拿到主键值,最后回表查询完整数据。
如果查询字段本身已经能从二级索引中拿到,就不需要回表,这就是覆盖索引。
1 | |
理解这些,是为了后面理解联合索引的扫描过程。
联合索引是怎么排序的?
假设有一个联合索引:
1 | |
它不是分别给 a、b、c 各建了一棵树,而是按照 (a, b, c) 这个组合顺序构建一棵 B+Tree。
排序规则可以简单理解为:
- 先按
a排序; a相同的情况下,再按b排序;a、b都相同的情况下,再按c排序。
所以,对于 (a, b, c) 这个联合索引:
1 | |
这些条件都符合最左匹配原则。
但是:
1 | |
这些条件无法从联合索引最左边开始匹配,因此不能有效利用这个联合索引做快速定位。
原因也很简单:在整棵联合索引树中,a 是全局有序的;但如果脱离了 a,b 和 c 并不是全局有序的,只是在前置字段相同的局部范围内有序。
范围查询为什么会影响后续字段?
问题的关键在于:一个查询条件能不能帮助 MySQL 缩小索引扫描区间。
比如联合索引是 (a, b),查询语句是:
1 | |
由于索引先按 a 排序,所以 a > 1 可以帮助 MySQL 找到一个扫描范围。
但是在 a > 1 的这个范围里,b 并不是全局有序的。因为这些记录可能来自很多不同的 a 值,每个 a 值下面的 b 才是局部有序。
因此,b = 2 很难继续用于缩小 B+Tree 的扫描边界。它更像是在扫描过程中再做过滤。
这就是常说的:遇到 >、< 这样的范围查询后,后面的字段无法继续参与索引定位。
但 >=、BETWEEN 为什么不一样?
再看一个类似的查询:
1 | |
它看起来也是范围查询,但和 a > 1 有一个细节差异:a >= 1 包含了 a = 1 这个边界值。
在 a = 1 的局部范围内,b 是有序的。
所以 MySQL 在构造扫描区间时,仍然有机会结合 b = 2 来定位边界,而不是完全忽略 b。
BETWEEN 在 MySQL 中也是类似的,因为 MySQL 的 BETWEEN value1 AND value2 包含左右边界,语义接近:
1 | |
所以类似下面的查询:
1 | |
在 MySQL 中,b 也可能继续参与联合索引的扫描区间构造。
这也是“范围查询一定让后续字段失效”这个说法不严谨的地方。
LIKE 'j%' 也不一定会停止匹配
再看一个字符串前缀匹配的例子。
假设有联合索引 (name, age):
1 | |
LIKE 'j%' 是前缀匹配,它可以利用索引,因为所有以 j 开头的 name 在索引中是相邻的。
它大致可以形成一个类似这样的扫描区间:
1 | |
也就是从 j 开头的位置开始扫,到 k 之前停止。
同时,在某些边界或局部相同值的范围内,后续字段 age 仍然可能参与索引条件判断。所以 LIKE '前缀%' 和 LIKE '%关键字%' 是完全不同的情况。
前者可能用到索引,后者因为前面有通配符,通常无法利用索引做有序定位。
怎么判断到底用了几个字段?
学习这个问题时,不要只看执行计划里的 key。
因为 key = idx_a_b 只能说明这条 SQL 使用了这个联合索引,但不能说明联合索引里的所有字段都参与了索引定位。
更应该关注的是 key_len。
key_len 可以帮助我们判断优化器使用了多少索引字段来构造扫描区间。
比如字段 a 和 b 都是 int not null:
- 如果
key_len = 4,通常说明只用到了一个int字段; - 如果
key_len = 8,通常说明两个int字段都参与了索引条件。
当然,实际计算时还要考虑字段是否允许 NULL、是否为变长字段、字符集等因素。
比如 varchar(30) 在 utf8mb4 下最多占用 30 * 4 = 120 字节,变长字段还需要额外字节记录长度,所以 key_len 会比直觉上更大。
这次学习后的结论
我觉得这篇文章最有价值的地方,不只是纠正了一个结论,而是提醒我们:
MySQL 索引优化不能只背口诀,要回到 B+Tree 的有序性和扫描区间去理解。
可以把结论整理成下面这样:
- 联合索引遵循最左匹配原则;
- 联合索引的字段顺序决定了 B+Tree 的排序规则;
- 后续字段能不能继续用上索引,取决于它能不能参与缩小扫描区间;
>、<这类范围查询后,后续字段通常不能继续参与索引定位;>=、<=、BETWEEN、LIKE 'xxx%'在 MySQL 中并不一定会让后续字段停止匹配;- 判断联合索引具体用了几个字段,不要只看
key,还要结合key_len; - 最好通过
EXPLAIN验证,而不是只背固定结论。
最后
以后再看到“联合索引遇到范围查询就停止匹配”这句话,我会更谨慎一点。
它可以作为一个粗略记忆,但不能当成完整结论。
真正理解这个问题,还是要回到两个点:
- 联合索引中的字段是如何排序的;
- 查询条件是如何形成索引扫描区间的。
这比单纯背一句八股文更可靠。