前言
在之前的文章中说过索引的底层是B+树,现在让我们在回顾一下知识:
- B+树有很多层,最底层称为叶子节点,其余的称为内节点。所有的用户记录存储在叶子节点中,而记录项目录存储在内节点。
- InnoDB引擎会依据主键(没有指定时数据库自动加入虚主键)创建聚簇索引
- 根据业务的要求不同需要的索引列也不同,这时候可以创建
二级索引
,二级索引的用户记录为主键+索引列
。当需要查询的数据不是主键或者索引列时,使用二级索引会进行回表
操作,获取对应的主键后使用聚簇索引
进行二次查找得到需要的值 - 索引的每一个叶子节点都是对应一个页,而用户记录都存储页中,页中的记录(不管是用户记录还是目录记录)按照从小到大的顺序形成了一个单向链表,而页和页之间也按照从小到大的顺序形成了一个双向链表,这也是索引查询快速的原因
- 索引在进行查询操作时,从根节点出发,一层层的向下查询,因为索引通过索引列建立了页目录,所以查询过程是十分快的。
索引的代价
索引的B+树建立时是通过索引列排序的,所以在针对索引列查询时效率很高。但是其也有局限性:
- 空间上的代价:
每建立一个索引都会创建一个B+树,而B+树的每个节点都会是一个记录页,而记录页默认占有16k数据,当表的数据少的时候,其性能还不如不见索引呢! - 时间上的代价:
索引是按照索引列进行排序过的,查询效率极高。但是插入、删除、修改的效率就会变得极其低下,每当变更索引列数据,索引就要进行页分裂和记录位移,重新进行排序,这样的效率就会降低。
所以索引不能乱建,乱建的代价就是拖慢性能,增加维护成本。
这个时候了解索引的使用场景就变得尤为重要。
接下来的文章将按照联合索引
来完成,因为二级索引
(非主键的单列索引)的讨论没有意义,没有什么需要注意的情况,且建立多个二级索引
是浪费空间和性能的行为(业务要求除外)
索引的使用场景
首先,先创建一个表并且创建一个索引,为了方便理解,直接建立一个联合索引。
CREATE TABLE mysql_test(
id int NOT NULL auto_increment,
name VARCHAR(50) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country VARCHAR(100) NOT NULL,
PRIMARY KEY(id),
KEY idx_name_birthday_phone_number(name, birthday, phone_number)
)
输入show index from mysql_test;
可以看到索引已经建立:
索引建立的过程:
1、首先索引会根据name
的值得大小从左到右进行排序
2、name
值相同项使用birthday
进行排序
3、name
值和birthday
值都有相同项时,相同项根据phone_number
进行排序
4、排完序后会根据排序的顺序建立页目录,从下往上建立起一颗B+树
查找的过程:
1、首先MySQL会根据SQL语句判断是否使用索引,比如查询条件非索引列的就不会使用,又或者MySQL判断出使用索引的效率低于全表扫描也是不会使用索引的。
2、如果使用了上述描述的索引,那么MySQL会先根据name
值查找目标记录
3、根据name
值查找到多个纪录,则会在这多条记录中根据birthday
进行查找目标记录
4、经过name``birthday
二者查找后还是有多条记录,则根据phone_number
进行查找目标记录
5、此时全部查找完毕之后仍有多条记录,且没有其他查询条件的情况下,从左到右进行遍历链表到不符合记录。同时还要查看查询结果是否是索引列,如果是则直接给出结果;反之则遍历过程中都会进行回表
操作,注意并非遍历完才回表
,而是一条记录回表
一次。
6、如果有其他查询条件,则同样从左到右遍历目标记录,每条记录进行回表,然后在进行查询;
全值查询
如果查询条件中的列和索引列一致那么就称之为全值查询,SQL语句如下:
select * from mysql_test where name='张华' and birthday='1999-10-01' and phone_number='11111111111'
SQL在查询过程中就会使用到索引,注意查询字段的先后过程,可以想象一下查询过过程:
- 从根节点出发,一层层往下查询,先查询
name
的值为张华的选项 - 如果name为张华存在多个记录,就使用
birthday
开始查询 - 如果仍存在多条记录,使用
phone_number
查询
那么问题来了,如果将查询条件的位置交换一下,对于查询的效率是否会有影响,答案是并不会
,因为MySQL中有一个神奇的配置:查询优化器,查询优化器会根据SQL语句进行效率最高的方式查询。
匹配左边的列
在书写SQL语句时并非都要进行全值查询,这时候就有一个匹配左边的列
,即查询条件的顺序必须是索引的从左到右
的实现,当然,这只是针对联合索引
,多个二级索引
的情况不在此情形中。
比如以下SQL语句就符合匹配左边的列:
select * from mysql_test where name='hah';
插一句:explain
关键字可以查看SQL语句是否使用了索引,得到的结果中type
是ref
说明使用了索引。
同时呢,按照索引的建立顺序也是可以进行查询条件也是可以触发索引的:
但是如果不按照从左到右
的顺序来,直接查询索引列中的其他项的话,就会进行全表扫描:
注意,这里说的顺序并非是上边全值查询
的顺序,全值查询
是全部索引列的查询,所以顺序无所谓,但是这个是非全索引列的查询
,查询顺序必须是按照索引列建立的顺序进行查询,不能跳过
那么这是为什么呢?因为索引在进行排序的时候先后顺序就是name,birthday,phone_number,先按照name排列,name值相同的按照birthday排列,前二者都相同的按照phone_number排列;所以要让索引跳过name
直接比较birthday
,索引表示做不到。
如果有实操的同学会发现下列这个语句也是用到了索引:
select * from mysql_test where name='hah' and phone_number='17181911211';
这个时候就疑惑了,不是说不能跳过吗,怎么也使用了索引,其实只是name
列使用了索引定位,phone_number
列并未使用,这个就是使用了联合索引的部分索引。
所以,在查询有联合索引
的表要注意查询条件。
匹配列前缀
匹配列前缀就是查询条件A%
这样的SQL语句,索引的排序就是比较索引列的过程,索引前缀是可以快速定位的,但是后缀
不可以,因为索引的比较不是从后面开始比较的,但是可以想办法将索引列的值翻转过来,然后将后缀变成前缀查询即可。既然后缀
查询都不可以了,那%A%
这样形式的查询也是用不到索引。
匹配前缀值的使用方法在针对查询长字符串时十分有效,去7位前缀就可以排序成一百万的数据。
匹配范围值
查询范围作为查询条件的SQL语句就符合匹配范围值的特征,比如以下SQL语句:
select * from mysql_test where name >= 'ak' and name <= 'su';
本篇文章中创建这个联合索引是以name
为首要排序条件的,所以很容易就可以找到name>='ak'
的值,然后从左到右遍历过去知道name='su'
,然后将结果返回给客户端。
对于联合索引来说,范围查询是不友好的,因为一个联合索引能进行范围查找的就只有最左索引列,即首要排序列
。即使是在name
范围查询后在进行其他索引列的范围查找,也只是使用到了name
列的索引,比如以下SQL语句:
select * from mysql_test where name >= 'ak' and name <= 'su' and birthday >= '1999-10-01';
上述的SQL语句只是使用到了name的索引,并未使用到birthday,birthday还是要在name给出的范围值里面进行全表查询,因为name所得出来的数据是根据name值排列的,可能有相同也有不相同,这样birthday能不能使用到也是未知数,即birthday查询条件所进行的不是顺序IO,而是随机IO。
精准查询一列,范围查询另一列
在范围查询的时候说到之所以只能最左列进行范围查询的原因是最左的范围是有序的,但是name的有序对于其他列来说是无序的,无法进行顺序IO。
但是如果name
是精准查询的话,name
查询出来得数据是一样的,根据一样的数据按照birthday来排列的原则,可以使用birthday
进行范围查询,这样的查询是有序的,是顺序IO,不仅是用到了name的索引,还使用到了birthday的索引。
SQL语句如下:
select * from mysql_test where name='ak' and birthday >= '1999-10-01';
排序
索引还可以用于排序,如以下SQL语句:
select * from mysql_test order by name, birthday, phone_number;
过程和和索引排序的过程一样,不再描述。
但是有几点需要注意:
1、ASC和DESC不要混用,例如以下SQL语句:
select * from mysql_test order by name, birthday DESC limit 10;
2、不能使用非索引列进行排序,并且排序的顺序要和索引列创建顺序一致
3、不要使用复杂的表达式
分组
其实分组和排序类似,先把记录按照name值进行分组,所有name值相同的记录划分为一组。
将每个name值相同的分组里的记录再按照birthday的值进行分组,将birthday值相同的记录放到一个小分组里,所以看起来就像在一个大分组里又化分了好多小分组。
再将上一步中产生的小分组按照phone_number的值分成更小的分组,所以整体上看起来就像是先把记录分成一个大分组,然后把大分组分成若干个小分组,然后把若干个小分组再细分成更多的小小分组
回表的代价
不管使用二级索引
,还是联合索引
,在结果不为索引列时都要进行回表
操作,但是大家有没有想过回表
的代价是什么。
首先要先了解以下顺序IO和随机IO两个概念,顺序IO就是找到一个记录后,可以按照顺序遍历链表的方式找到后续符合条件的记录;而随机IO则是从根节点一层层往下寻找到对应的记录,也就是不能通过一条记录遍历链表找到后续记录的,这样会查询跟多的数据页,效率是比较慢的。
在二级索引
、联合索引
中并不是按照主键
来排序的,所以在进行回表
操作时进行的就是随机IO
,当查询的数据量比较大时效率确实低效的。
MySQL在执行SQL语句时查询优化器会根据数据量的大小(查询结果包含非索引列)会判断是否使用索引。
如何挑选索引
1、针对查询条件
SQL语句中无非就是搜索语句、排序、分组等等,所以当创建索引时可以根据查询、排序或者分组条件来创建索引
2、基数列大的表创建索引
什么是基数呢?就是列中不重复的值的个数,举个例子2、3、5、5、2、3
,假设这是某列的值,6条记录但是翻来覆去就只是2、3、5
这三个数,那么这列的基数
就是3
,一般来说,基数大的列较为分散,以这样的列作为索引列,那么能把效率的利用率提高到最高。
3、长字符串改前缀
在某些特殊业务中,不可避免的要将长字符串列作为索引列,这个时候可以采用前缀的格式。长字符串的索引有两个弊端:
- 长字符串所占据的空间较大,用户记录所占的数据页一般是16K,这样的话必然导致B+树的叶子结点增多,使得树的整体高度较高,查询的效率降低
- MySQL建立索引和后续的查找本质上都是进行列值的对比,长字符串进行对比时花费时间较长,查询效率会降低
使用前缀的方法虽然无法第一时间精确到要查询的字符串,但是可以查询到对应前缀之后进行回表
,使用聚簇索引查询条件。
在建立前缀索引时,鼓励使用前10
字符作为前缀,前10
作为前缀已经能够比较大多数的长字符串了。
需要注意的是,前缀索引无法进行排序,因为前缀索引中只有前缀,无法比较长字符串后面的字符。
4、索引列类型
索引列的类型不要太大,太大的索引列类型会有两个弊端:
- 过大的索引类型,占据空间也大,原因和3
中的原因一样
- 过大的索引类型在比较排序建立索引时会非常的慢
之前就说过索引虽然方便查询,但是也是需要额外空间的,所以能减少消耗就尽量减少。
这也是在开发过程中常常使用id
作为主键的原因,id
唯一并且其是int
类型的。
5、尽量使用自增主键
之前说过索引方便查询但是不方便增删改,在使用自增主键时插入一个新的记录时,索引只需要在最后一个数据页(或者创建一个新的数据页)增加即可。反之如果插入一个新的记录,这个记录的主键恰好是在中间位置,那么就需要页分裂和记录位移,这是十分影响效率的行为。
所以尽量使用自增主键,尽管插入的记录不会都是最末尾的,但是插入最末尾确实最常见的。
总结
- 索引的使用有全值查询、范围查询、精确匹配一列范围查询另一列、匹配左边的列、排序、分组等
- 较难理解的就是全值查询和匹配左边的列,二者的区别全值查询是索引列全部作为查询条件,所以不要求顺序性;而匹配左边的列是非全索引列查询,这个时候讲究顺序性,要求和索引建立的过程一样
- 联合索引的过程是按照索引建立的索引列写的顺序排序的,比如以下SQL语句:
alter table mysql_test add idx_name_birthday (name(10), birthday);
上面创建的idx_name_birthday
索引就是现根据name来排序,如果name值出现重复则按照birthday来排序。
SQL语句中name(10)
的意思是按照名字的前10
个字符来进行排序
- 索引不能够乱建,每创建一个索引都会建立一个B+树,是耗费空间资源的体现,并且表进行增删改时对索引的维护也是相当费力的,要进行页分裂再记录位移,也是相当影响性能的;所以当数据量不大的时候可以不建索引就不建
- 使用联合索引进行查询是
顺序IO
,而回表
操作是随机IO
,所以尽量减少回表
操作。减少方法:- 查找数据尽量不要使用
*
,要查找什么数据就是写字段名,这样如果索引列刚好是索要查的数据的列,那么就不会回表
- 能分页就分页,不要查询所有的数据,可以减少
回表
操作
1.应该建立怎样的索引:根据查询条件选择索引列,选择基数大的列作为索引列,选择数据类型小的作为索引列,可以将长字符串索引列优化成长字符串前缀索引列;对于创建表是为了简化聚簇索引,所以主键尽量使用自增主键
- 查找数据尽量不要使用