MySQL8.0有一种复合索引可以跳过扫描范围访问方法,什么意思呢?简单的说,对于复合索引(a,b,c),我们以往常说“where a=xx and b=xx”是可以用到索引,但“where b=xx and c=xx”是用不到索引的。但MySQL8.0中,后者也是可以用到索引了。
我们建一张测试表,其测试数据是104万,如下:
root@localhost|test>select version();+-----------+| version() |+-----------+| 8.0.19 |+-----------+1 row in set (0.00 sec)root@localhost|test>show create table t\G*************************** 1. row ***************************Table: tCreate Table: CREATE TABLE `t` (`id` int NOT NULL AUTO_INCREMENT,`dept` tinyint DEFAULT NULL comment '部门ID,1~10',`name` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL comment '姓名',`create_time` datetime DEFAULT NULL comment '注册时间',PRIMARY KEY (`id`),KEY `idx_muti` (`create_time`,`name`,`dept`)) ENGINE=InnoDB AUTO_INCREMENT=1441756 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin1 row in set (0.00 sec)root@localhost|test>select count(1) from t;+----------+| count(1) |+----------+| 1048576 |+----------+1 row in set (0.11 sec)
从上可看到这个表有一个复合索引idx_muti (create_time,name,dept),我们尝试直接查dept字段看是否能用到索引。
root@localhost|test>explain select count(1) from t where dept=5;+----+-------------+-------+------------+-------+---------------+----------+---------+------+---------+----------+--------------------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+-------+---------------+----------+---------+------+---------+----------+--------------------------+| 1 | SIMPLE | t | NULL | index | idx_muti | idx_muti | 131 | NULL | 1045864 | 10.00 | Using where; Using index |+----+-------------+-------+------------+-------+---------------+----------+---------+------+---------+----------+--------------------------+1 row in set, 1 warning (0.00 sec)root@localhost|test>select count(1) from t where dept=5;+----------+| count(1) |+----------+| 105271 |+----------+1 row in set (1.16 sec)
root@localhost|test>explain select count(1) from t ignore index(idx_muti) where dept=5;+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 1045864 | 10.00 | Using where |+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+1 row in set, 1 warning (0.00 sec)root@localhost|test>select count(1) from t ignore index(idx_muti) where dept=5;+----------+| count(1) |+----------+| 105271 |+----------+1 row in set (0.70 sec)
全表扫描的耗时居然比使用索引的耗时还要少,难道是因为innodb的缓存?
我们在my.cnf文件加入“innodb_buffer_pool_load_at_startup=0;”和“innodb_buffer_pool_dump_at_shutdown=0;”两个参数然后重启MySQL,再执行上述全表扫描的SQL,发现其执行时间都是在0.7秒左右,始终比使用索引还要快。
我们打开官档,官档的大体意思就是说,对于(a,b)的复合索引,如果“where b>xxx”的查询条件,那么先获取a字段的不同值,然后通过不同的值去构造出“where a=xx and b>xxx”,一直把a的不同值构造出来执行。
以下斜体部份来自官档:
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));INSERT INTO t1 VALUES(1,1), (1,2), (1,3), (1,4), (1,5),(2,1), (2,2), (2,3), (2,4), (2,5);INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;ANALYZE TABLE t1;EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
为了执行该查询,MySQL可以选择索引扫描以获取所有行(索引包括要选择的所有列),然后应用子句中的f2 > 40 条件WHERE以产生最终结果集。
范围扫描比全索引扫描更有效,但是在这种情况下不能使用,因为f1在第一个索引列上没有条件 。然而,如MySQL的8.0.13的,优化器可以执行多个范围扫描,一个用于的每个值f1,使用一种称为跳跃扫描方法是类似于松散索引扫描:
在第一个索引部分的不同值f1(索引前缀)之间跳过 。
f2 > 40对其余索引部分上的条件, 对每个不同的前缀值执行子范围扫描。
对于前面显示的数据集,算法的运行方式如下:
获取第一个关键部分的第一个不同值(f1 = 1)。
根据第一和第二关键部分(f1 = 1 AND f2 > 40)构造范围。
执行范围扫描。
获取第一个关键部分的下一个不同值(f1 = 2)。
根据第一和第二关键部分(f1 = 2 AND f2 > 40)构造范围。
执行范围扫描。
使用此策略可减少访问的行数,因为MySQL会跳过不符合每个构造范围的行。此跳过扫描访问方法适用于以下情况:
表T具有至少一个复合索引,其关键部分的形式为([A_1,...,A_ k,] B_1,...,B_ m,C [,D_1,...,D_ n])。关键部分A和D可能为空,但B和C必须为非空。
该查询仅引用一个表。
查询不使用GROUP BY或 DISTINCT。
该查询仅引用索引中的列。
A_1,...,A_上的谓词k必须是相等谓词,并且它们必须是常量。这包括 IN()操作员。
该查询必须是一个联合查询。即, AND的OR 条件:(cond1(key_part1) OR cond2(key_part1)) AND (cond1(key_part2) OR ...) AND ...
C上必须有范围条件。
D列上的条件是允许的。D上的条件必须与C上的范围条件结合使用。
从官档的说明来看,这种跳过扫描范围访问方法适用于复合索引(a,b)中,a的索引基数比较小的(就是a字段的值大部份相同,例如姓名、年龄、类型、状态之类),如果其索引基数比较大,则全表扫描可能比这种跳过扫描范围访问要更快些。
以下我们验证下复合索引(a,b)中,a的索引基数比较小的情况。
在实验前,我们先重启一次MySQL:
#删除原来的索引root@localhost|test>drop index idx_muti on t;Query OK, 0 rows affected (0.02 sec)Records: 0 Duplicates: 0 Warnings: 0#新建一个索引,以dept字段作为前缀字段。root@localhost|test>create index idx_muti on t(dept, create_time);Query OK, 0 rows affected (4.75 sec)Records: 0 Duplicates: 0 Warnings: 0root@localhost|test>explain select count(1) from t where create_time>='2020-01-01' and create_time<'2020-02-01';+----+-------------+-------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------------+| 1 | SIMPLE | t | NULL | range | idx_muti | idx_muti | 8 | NULL | 116183 | 100.00 | Using where; Using index for skip scan |+----+-------------+-------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------------+1 row in set, 1 warning (0.00 sec)root@localhost|test>select count(1) from t where create_time>='2020-01-01' and create_time<'2020-02-01';+----------+| count(1) |+----------+| 75932 |+----------+1 row in set (0.08 sec)#重启MySQLroot@localhost|test>system systemctl restart mysql#尝试使用全表扫描的方法root@localhost|test>explain select count(1) from t ignore index(idx_muti) where create_time>='2020-01-01' and create_time<'2020-02-01';ERROR 2006 (HY000): MySQL server has gone away #因为上面重启了mysql,因此此处链接丢失了需要重连No connection. Trying to reconnect...Connection id: 8Current database: test+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 1045864 | 11.11 | Using where |+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+1 row in set, 1 warning (0.03 sec)root@localhost|test>select count(1) from t ignore index(idx_muti) where create_time>='2020-01-01' and create_time<'2020-02-01';+----------+| count(1) |+----------+| 75932 |+----------+1 row in set (0.71 sec)
可见此时使用索引的效率更高。因此使用新特性时要注意其适用范围,否则可能会适得其反。
声明:本文首次发表在CSDN,详情请点击下方“阅读原文”
长按下图二维码,关注更多数据库实用技巧。





