请支持正版:MySQL实战45讲
在开始讲解之前,先看一个例子:
mysql>select * from T where k between 3 and 5;
这条语句会执行几次树的搜索?又会扫描多少行?
下面是这个表的初始化语句:
mysql>create table T(
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k)
)engine=InnoDB;
随后执行插入语句:
insert into T values(100,1,'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
那么开头的查询SQL语句执行流程是:
上一篇文章,04|深入浅出索引(上) 提到过这种回到主键索引树搜索的过程称之为回表。可以看出,这个查询过程中在k索引树上读了3条记录,回表了两次
因为普通索引树上只存主键,所以要查询数据不得不回表,那有没有办法避免呢?
如果执行的语句是select ID from T where k between 3 and 5
,这个时候只需要查询ID的值,而ID的值已经在k索引树上,因此可以直接提供查询结果,不需要回表。
也就是说,在这个查询里,索引k已经覆盖了
我们的查询需求,我们称之为覆盖索引
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段
值得注意的是:在引擎内部使用覆盖送印在索引k上其实读了三个记录 ,但是对于MySQL的Server层,它就是找引擎要了两条记录,因此MySQL会认为扫描行数是2
那么再回想上一篇中的一个问题,在一个市民信息表上,是否有必要将身份证号和名字建立联合索引?
假设市民表是如下定义:
mysql>create table 'tuser'(
'id' int(11) NOT NULL,
'id_card' varchar(32) DEFAULT NULL,
'name' varchar(32) DEFAULT NULL,
'age' int(11) DEFAULT NULL,
'ismale' tinyint(1) DEFAULT NULL,
primary key(id),
key 'id_card' ('id_card'),
key 'name_age' ('name','age')
)engine=InnoDB;
我们知道,身份证号是市民的唯一标识,也就是说,如果有根据身份证号查询市民信息的需求,我们只要在身份证号字段上建立索引就够了,而再建立一个(身份证号、姓名)的联合索引,是不是白白浪费空间?
如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间
当然,索引字段的维护总是有代价的,因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了
看到这里你一定有一个疑问,如果为每一种查询都设计一个索引,索引是不是太多了?如果我现在要按照市民的身份证号去查他的家庭地址呢?
虽然这个查询请求在业务中出现的概率不高,但是总不能让它走全表扫描吧。反过来说,单独为了一个不频繁的请求创建一个(身份证号,地址)的索引又感觉优点浪费,应该怎么做呢?
结论:B+数这种索引结构,可以利用索引的最左前缀
原则,来定位记录
假设有一个(姓名,年龄)的索引树的一个节点
”李四“,20 | ”王五“,10 | “张六”,30 | “张三”,10 | “张三”,10 | “张三”,20 |
---|---|---|---|---|---|
ID1 | ID2 | ID3 | ID4 | ID5 | ID6 |
可以看到,索引项是按照索引定义里面出现的字段顺序排序的
当你的逻辑需求是查到所有名字是“张三”的人时,可以快速定位到ID4,然后向后遍历得到所有需要的结果
如果你要查的是所有名字第一个是“张”的人,你的SQL语句的条件是where name like'张%‘
。这时,你也能够用上这个索引,查找到第一个符号条件的记录是ID3,然后向后遍历,直到不满足条件为止
可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索,这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符
基于以上对最左前缀索引的说明,我们来讨论一个问题,在建立联合索引的时候,如何安排索引内的字段顺序
结论是:利用最左前缀,当有了(a, b)这个联合索引后,一般就不需要在a上建立索引了,因此,第一原则是,如果通过调整顺序,可以少维护一个所以你,那么这个顺序往往就是需要优先考虑采用的
那么回到前面的那个问题,我们要为高频请求创建(身份证号,姓名)这个联合索引,并用这个联合索引顺序来实现按照身份证号查询地址
的需求
那么又有一个问题:如果既有联合查询,又有基于a,b各自的查询呢?
查询条件里只有b的语句,是无法使用(a, b)这个联合索引的,这个时候不得不维护另一个索引,也就是说你需要同时维护(b)和(a, b)这两个索引
这个时候我们需要考虑的原则就是空间了,比如上面这个市民表的情况,name字段是比age字段大的,那我就建议建立一个(name, age)的联合索引和(age)的单字段索引
说完最左前缀以后,你可能会想知道,那些不符合最左前缀的部分,会怎么样
还是以市民表的联合索引(name,age)为例,如果现在有一个需求,检索出表中名字第一个字是张,而且年龄是10岁的所有男孩
,那么SQL语句应该这么写
mysql>select * from tuser where name like '张%' and age=10 and ismale=1;
在MySQL5.6之前,只能从ID3开始一个个回表,到主键索引上找出数据行,再对比字段值
而MySQL5.6引入的索引下推优化,可以在索引遍历的过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表的次数
”李四“,20 | ”王五“,10 | “张六”,30 | “张三”,10 | “张三”,10 | “张三”,20 |
---|---|---|---|---|---|
ID1 | ID2 | ID3 | ID4 | ID5 | ID6 |
从数据中可以看出,我们要从ID3开始遍历
MySQL5.6之前是一个一个遍历,回表查看是否符合,具体来说就是,根据索引树上找到,”张六“,30
,找到ID3这个主键,然后从ID3开始,回表查找数据。这一步操作要一直到ID6为止
而有了索引下推技术,在索引树上找到一个索引"张三", 30
的时候,就会判断是否符合条件,显然age不等于10,所以并不会回表,同理,"张三",20
也不会回表