问题
最近在看《MySQL实战45讲》中遇到一个比较困惑的问题。问题是:
有如下表
CREATE TABLE `geek` (
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
`c` int(11) NOT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`a`,`b`),
KEY `c` (`c`),
KEY `ca` (`c`,`a`),
KEY `cb` (`c`,`b`)
) ENGINE=InnoDB;
有两条SQL如下:
select * from geek where c=N order by a limit 1;
select * from geek where c=N order by b limit 1;
问题:哪个索引是多余的?
作者认为索引c与联合索引(c,a,b)效果相同。
看到这块就很不理解,为什么走索引c的时候会用到主键联合索引a,b?我们知道二级索引叶子节点存储的是主键id的值,在查询的时候,会先根据二级索引找到主键id的值,再进行回表去主键id的聚簇索引树中拿到具体的行数据。举个例子,k为普通索引,Id为主键索引。对于SQL select * from T where k = 5
,执行过程是先去k索引树找到k=5对于的id值:500;再到ID索引树找到id =500对应的行数据,并返回;接着再去k索引树取下条数据k=6,不符合则返回。
(图片来源:《MySQL实战45讲》)
索引扩展
MySQL官网中介绍了索引扩展的原理:
InnoDB
automatically extends each secondary index by appending the primary key columns to it.InnoDB通过向每个二级索引添加主键列来自动扩展它。
扫描二维码关注公众号,回复: 12750031 查看本文章
举个例子, t1表有主键索引(i1,i2) 和辅助索引 k_d,但是InnoDB在内部扩展了这个索引k_d,会把主键值追加到索引列后,扩展后的索引变为 (d, i1, i2)。MySQL优化器通过使用扩展的辅助索引来进行更有效率地 连接、排序、ref、range查询。
optimizer_switch系统变量的use_index_extensions标志允许控制优化器在确定如何使用InnoDB表的辅助索引时是否考虑主键列。默认情况下,启用“使用索引”扩展。如果禁用使用系统扩展,可以通过以下指令:
SET optimizer_switch = 'use_index_extensions=off';
下面通过实验来验证这个结论,注意,本次实验基于数据库版本5.7。
(1)准备工作,建表和插入数据
CREATE TABLE t1 (
i1 INT NOT NULL DEFAULT 0,
i2 INT NOT NULL DEFAULT 0,
d DATE DEFAULT NULL,
PRIMARY KEY (i1, i2),
INDEX k_d (d)
) ENGINE = InnoDB;
插入数据
INSERT INTO t1 VALUES
(1, 1, '1998-01-01'), (1, 2, '1999-01-01'),
(1, 3, '2000-01-01'), (1, 4, '2001-01-01'),
(1, 5, '2002-01-01'), (2, 1, '1998-01-01'),
(2, 2, '1999-01-01'), (2, 3, '2000-01-01'),
(2, 4, '2001-01-01'), (2, 5, '2002-01-01'),
(3, 1, '1998-01-01'), (3, 2, '1999-01-01'),
(3, 3, '2000-01-01'), (3, 4, '2001-01-01'),
(3, 5, '2002-01-01'), (4, 1, '1998-01-01'),
(4, 2, '1999-01-01'), (4, 3, '2000-01-01'),
(4, 4, '2001-01-01'), (4, 5, '2002-01-01'),
(5, 1, '1998-01-01'), (5, 2, '1999-01-01'),
(5, 3, '2000-01-01'), (5, 4, '2001-01-01'),
(5, 5, '2002-01-01');
(2)关闭索引扩展
查看优化器配置开关:show variables like '%optimizer_switch%';
可以看到MySQL默认是打开索引扩展功能的 use_index_extensions=on:
index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
执行关闭索引扩展的命令:SET optimizer_switch = 'use_index_extensions=off';
执行explain指令查看:EXPLAIN SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01'
分析结果:
- 使用了主键索引PRIMARY,key_len 长度为4,Extra 使用了Using where,说明使用主键i1查询到符合条件的结果集,然后再使用where 条件 d = ‘2000-01-01’ 进行过滤;
(3)开启索引扩展
执行开启索引扩展命令:SET optimizer_switch = 'use_index_extensions=on';
执行explain指令查看:EXPLAIN SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01'
分析结果:
- 实际使用的索引key是k_d,但是key_len不是4而是8,ref也是const 和const,同时Extra是Using index表面使用了覆盖索引。
- 虽然我们以为优化器选择的索引key是k_d,但是实际使用的是索引扩展(d,i),这样就能解释为什么key_len=8=4+4, ref=const和const,Extra的覆盖索引是正好使用了(d,i)的覆盖索引
以上分析结果说明MySQL在执行的时候会使用扩展索引(d -> (d,i))来提高执行效率。
注:5.7版本的MySQL文档的一个小漏洞
在上述第2步(2)关闭索引扩展的实验中,我们分析结果表面关闭索引扩展的情况下,explain分析得出的使用的key是主键key - PRIMARY,key_len = 4, extra = Using where.
但是官方5.7的文档结果却说使用的k_d单列索引,extra却走得是Using index覆盖索引,跟我分析的结果完全不一样。
经过多次实验发现MySQL 5.6版本下实验结果与文档中分析结果一致。因此,我猜测MySQL 5.7文档 https://dev.mysql.com/doc/refman/5.7/en/index-extensions.html 在关闭索引扩展的分析结果是直接抄了5.6的文档,没有做任何改动,上述的分析结果是错误的。
实际在MySQL底层实现时5.7可能已经有了变化,在count(*)时不会使用二级索引 + Using index的查询,而是直接选择主键PRIMARY的查询。
参考:https://dev.mysql.com/doc/refman/5.7/en/index-extensions.html
https://coderbee.net/index.php/db/20190106/1708