导读:
首先创建一张 test 表,并插入一些数据:
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
`c` int(11) NOT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_abc` (`a`,`b`,`c`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
其中,test 表中有一个联合索引 idx_abc,其包含索引(a)、索引(ab)、索引(abc)。而我们都知道联合索引在使用时需要遵循最左匹配原则,也就是说,在执行 select * from test where a = 1 and b = 2 and c = 3 使用 索引(abc)、select * from test where a = 1 and b = 2 索引(ab),虽然使用到的索引列不同,但是归根结底都使用的是 idx_abc 索引。
但是在执行 select * from test where a = 1 and b > 2 and c = 3 时,使用到的索引列仅仅为(ab),但是查询条件中不是三个索引列都存在吗,为什么只用到了两个索引列呢?这个便是我们今天要讨论的问题,即联合索引中,为什么前一个索引列使用了范围查询后,下一个索引列将无法使用索引进行匹配?
一、最左匹配原则使用示例:
1、全值匹配查询时
例如:
select * from test where a = 1 and b = 2 and c = 3;
select * fom test where b = 2 and a = 1 and c = 3;
select * from test where c = 3 and b = 2 and a = 1;
这些查询都能用到索引 idx_abc,因为 MySQL 中有查询优化器,会自动优化查询顺序,所以查询条件中将索引列顺序调换不影响联合索引的使用。
2、匹配左边的列时
例如:
select * from test where a = '1';
select * from test where a = '1' and b = '2';
select * from test where a = '1' and b = '2' and c = '3'
这些查询都从最左边开始连续匹配,用到了索引。
但如果查询条件没有从最左边开始,则不会使用联合索引,查询会转为全表扫描:
select * from test where b = '2';
select * from test where c = '3';
select * from test where b = '1' and c = '3'
这些没有从最左边开始,最后查询没有用到索引,用的是全表扫描。
select * from test where a = 1 and c = 3
部分索引,只有 a 列用到了索引,c 列无法使用索引。
3、匹配列前缀(%)
对于字符型字段的匹配:
- 如果查询条件是前缀匹配(例如 a like 'As%'),可以使用索引,因为前缀是有序的。
- 如果查询条件是后缀或中缀匹配(例如 a like '%As' 或 a like '%As%'),则不能使用索引,需要全表扫描。
例:
- select * from test where a like 'As%'(前缀匹配,走索引查询)
- select * from test where a like '%As'(全表查询)
- select * from test where a like '%As%'(全表查询)
4、匹配范围值
例:
- select * from test where a > 1 and a < 3:一个列进行范围查询,前缀匹配,走索引查询。
- select * from test where a > 1 and a < 3 and b > 1:多个列同时进行范围查找时。只有对索引最左边的列进行范围查找才用到 B+ 树索引。因此,只有 a 列用到了索引,b 列无法使用索引,查询会基于 1 < a < 3 的范围查找记录后,继续逐条过滤。
5、精确匹配某一列并范围匹配另外一列
例:
- select * from test where a = 1 and b > 3
在这种查询中,如果左边的列是精确查找,右边的列可以进行范围查找,可以进行范围查找,联合索引会加速查询。
6、精确匹配某一列并范围匹配另外一列,再精确或范围匹配另外一列
例:
- select * from test where a = 1 and b > 3 and c = 2
在这种查询中,联合索引使用到的索引列仍然只有(a,b)。
解释:
MySQL 中的索引结构是 B+ 树,叶子节点中的数据是以索引列从小到大的顺序组织起来的,对于联合索引来说,因为索引列不止有一列,所以数据的排列排列先按 a 列进行从小到大的排序,再按 b 列进行排序,最后按 c 列进行排序,对于上述 test 表中的数据,索引列 idx_abc 的叶子节点数据排序可以简化为:
a | b | c |
1 | 2 | 4 |
1 | 2 | 8 |
1 | 6 | 2 |
1 | 6 | 3 |
2 | 2 | 7 |
2 | 7 | 4 |
3 | 2 | 4 |
3 | 2 | 6 |
3 | 3 | 2 |
3 | 3 | 8 |
可以看出,a 是有序的(构建一颗 B+ 树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建 B+ 树。),而 b,c 都是无序的。但是当 a 相同时,b 是有序的;当 b 相同时,c 又是有序的。
通过对联合索引的结构的了解,那么就可以很好的了解为什么最左匹配原则中如果遇到范围查询就会停止了。以 select * from test where a=5 and b > 0 and c =1 为例,当查询到 b 的值以后(这是一个范围值),c 是无序的,所以就不能根据联合索引来确定到底该取哪一行。
二、小结
根据上述使用示例,可以得出,最左匹配原则有以下特性:
- 最左优先:以最左边的字段为起点,任何连续的索引都能匹配上。
- 范围查询字段后停止:当遇到 >、<、BETWEEN、LIKE之后,下一个列就不会再使用索引进行匹配。
三、索引下推:
在搞清楚"联合索引中,前一个索引列使用了范围查询后,下一个索引列将无法使用索引进行快速定位"的问题后,我对上述示例6中的 SQL select * from test where a = 1 and b > 3 and c = 2; 进行了执行计划分析,如下:
这个 Using index condition 就是我们接下来要讨论的东西。
我们已知,上述 SQL 会使用到联合索引 idx_abc 中的 a、b 索引列,但是 c 索引列不会使用索引进行匹配。那么对于 where 条件中的 c = 2 该怎么处理呢?
在 MySQL 5.6 之前,我们通过非聚簇索引 + 聚簇索引定位到一条记录后,会将行数据返回给 Server 层,Server 层会根据 c = 2 对数据进行过滤,只要符合条件的记录才会被返回给客户端,此时的 Extra 中的信息应为 Using where。
但是从 MySQL 5.6 开始,引入了索引下推 (Index Condition Pushdown,ICP)来进行查询优化,最主要的区别是,对于索引列 c 的处理不会再让 Server 层去处理了,而是下推到存储引擎层,即在对联合索引进行数据匹配时,直接对使用不到索引的索引列条件进行判断,最后的结果就是只对符合条件的记录进行回表,这样的话就可以大大减少回表的次数,从而提升查询效率。
总结:
- 索引下推 (ICP) 是 MySQL 5.6 引入的一项关键的查询优化技术。
- 核心思想: 将部分可以由索引包含的列完成的 WHERE 条件过滤操作,从服务器层“下推”到存储引擎层执行。
- 主要应用场景:针对联合索引 (INDEX (col1, col2, ...)) 的查询,特别是当 WHERE 条件中:
- 查询使用了索引的第一列(通常是范围查询:>, >=, <, <=, BETWEEN, LIKE 'a%'),并且
- 后面还包含其他索引列作为等值(=)或范围条件 (col2 = X, col3 > Y, etc.)
- 可下推的条件:下推的条件需要是索引本身包含的列 (称为 Index Column)。在 idx_abc 例子中,c 是该索引的列,所以 c = 2 可以被下推。where a = 1 and b > 3 and c = 2 and d =7 中的 d = 7 因为 索引列不包含 b,所以其不能被下推。
- 执行计划标识:使用 EXPLAIN 命令查看查询执行计划。如果看到 Extra 列显示 Using index condition,则说明优化器对该查询使用了索引下推。
- 巨大价值:它允许存储引擎在回表读取完整数据行之前,就利用索引中存储的后续列的值过滤掉大量不满足所有条件的记录,显著减少不必要的回表操作次数,从而大幅降低磁盘 I/O 和 CPU 开销,提升查询性能。
举例:
四、参考:
- MYSQL最左匹配原则及其底层逻辑-CSDN博客
- https://www.zhihu.com/question/52536048/answer/1906024782132125707