目录
1、名词概念
2、索引分类
3、工作原理
3.1、从回表的特性来看索引维护和使用
4、索引的使用技巧
4.1、覆盖索引
4.2、最左前缀原则
4.3、联合索引
4.4、索引下推
5、优化的方向
5.1、平时代码书写注意
5.2、常见的容易引起索引失效
6、问题思考
7、小结
前言
每个索引在innodb里都是一颗B+树,一张表由多棵B+树组成,B+树很好的配合了磁盘的读写性能。
1、名词概念
-
主键索引-聚簇索引(clustered index) : 索引的顺序和表数据行的顺序保持一致
-
非主键索引-二级索引(secondary index) -非聚簇索引
-
唯一索引:与普通索引的区别主要是用来防止重复,同时也减少了维护的成本
-
覆盖索引:减少回表,提高效率, 优化手段
-
回表:先通过非主键索引得到主键id,然后再回表查询查询得到具体的行记录
-
索引下推:通过索引提前条件判断,减少回表次数
-
最左原则:使用联合索引的最左n个字符,索引内字段的顺序安排
2、索引分类
3、工作原理
下面通过一个实例来具体分析下索引的结构以及是如何工作的。
CREATE TABLE `USER` (
`id` int(11) NOT NULL,
`age` int(11),
`orderId` bigint NOT NULL,
PRIMARY KEY (`id`),
KEY `orderId` (`orderId`)
) ENGINE=InnoDB
插入数据:
insert into USER (id, age, orderId) values (1,18,101),(2,19,102),(3,20,103),(4,21,104),(5,22,105);
假设给这张表插入5行数据(id,age,orderId):(1,18,101),(2,19,102),(3,20,103),(4,21,104),(5,22,105)。假设该表的索引是一棵4叉树。
下面来看一下,主键索引和普通索引的区别:
select * from USER where id = 1;
主键索引的查询方式:我们只需要搜索id主键这棵B+树,根据id=2条件查到的就是整个id=2的整行记录数据;
select * from USER where orderId = 101;
普通索引查询,我们需要先查询orderId二级索引,通过orderId=101查询得到id=1,然后再通过id=1搜索主键索引得到行记录,得到全部的行记录数据。这里可以看到,我们进行来两次查询,这个过程叫做回表。回表增加了访问数据库io的次数,且容易造成随机io。所以很明显,主键索引只需要搜索一次就可以拿到数据,普通索引先搜索索引拿到主键,然后再到主键索引搜索一次才可以拿到这个值(回表)。我们在实际使用过程中,尽量使用主键索引减少查询次数,提高效率。
tips:如何减少回表经验:
-
尽量使用主键查询,防止回表;
-
可以适当使用覆盖索引,减少回表;
3.1、从回表的特性来看索引维护和使用
非主键索引,存储的都是索引的值和主键的值,且逐渐索引才存储了整行的记录数据,所以:
-
1、如果主键修改将会引起其他的索引失效和重建,及其消耗性能,应该避免;
-
2、如果主键比较大那就会比较占用内存,因为其他索引一定会存储这个值;
-
3、如果索引是无序的,那么在创建索引和插入数据的时候就会频繁的随机插入,容易引起页分裂和页合并,也是及其消耗性能;
-
4、我们需要 “尽量使用主键来查询”的原则,也可以使用覆盖索引,避免 “回表查询”搜索两棵树。
4、索引的使用技巧
(1)、覆盖索引:新建覆盖索引,如果查询条件是普通索引或者是联合索引的最左侧字段,查询结果是联合索引的字段或者是主键,此时不需要回表,直接返回结果,减少io磁盘的访问,提高效率;
(2)、最左前缀 :联合索引的最左N个字段,也可以是字符串索引的最左M个字符;
(3)、联合索引:联合索引的使用遵循最左前缀原则,所以尽量将频繁查询的字段靠左创建;
(4)、索引下推 :例如”select * from USER where name like “李%” and age = 12”,mysql5.6版本之前,会对所有匹配的数据进行回表操作,但是5.6之后的版本,会先过滤age!=12的数据,再进行回表,减少了访问磁盘的次数,提升检索效率;
tips:
4.1、覆盖索引
这里就用到了覆盖索引,如果执行的语句是:
select id from USER where orderId between 102 and 103;
这时候只需要查询id的值,而id的值已经在普通索引orderId上了,可以直接查询结果,而不需要回表。也就是说,普通索引orderId已经“覆盖了”我们的查询要求,所以我们成为覆盖索引。
由于覆盖索引可以减少树的搜索次数,避免了回表查询,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
需要注意的是:覆盖索引在引擎内部使用覆盖索引在索引orderId上其实读了3个记录,但是对于server层来说,它只拿到了2条记录,所以认为扫描数是2条。
当我们需要建立冗余索引来支持覆盖索引时就需要权衡考虑其维护代价和查询的效率了。
例如我们一般很少对姓名“name”字段来做索引,基本上都是对身份证id或者userId来建立索引,但是如果我们有通过userId查询“name”的需求的时候,数据量还比较大,又不想对"name"单独的建立索引,查询需求少且增加了维护成本,这时候使用联合索引(userId,name)就有意义了。在这个请求上不需要回表查整个记录,减少语句的执行时间。
4.2、最左前缀原则
当某个查询频率不高,但是存在的请求,考虑到空间及维护成本,我们不希望为它单独建立一个索引,但是又不希望它扫描全表,这时,我们就会想到最前缀索引,在B+树中,我们可以利用“最左前缀”,来定位记录。
最左前缀索引就是利用 联合索引的最左N个字段,也可以是字符串索引的最左个字符;
这里我们需要考虑一个问题:在建立联合索引的时候,如何安排索引内的字段顺序?
比如,对于身份证id和userId,我们既需要联合索引(id,userId),又需要id和userId各自的索引,这时候就要考虑,看那个字段更大,如果userId比id大,我们就需要建立(userid,id)和id的索引,这样既可以满足查询需求,又可以节省空间。
4.3、联合索引
联合索引的创建原则
4.4、索引下推
前面提到最左前缀索引,我们可以通过最左前缀来定位索引,那么那些不符合最左前缀的部分呢?
比如有一联合索引是(name,age)。现在需要查出姓李的年龄是12岁的男同学。
select * from USER where name like “张%” and age = 18;
这条语句在使用的时候只能用到”李"的索引,找到第一个满足条件姓李的同学,然后再匹配剩下的条件。
在mysql5.6之前,只能从找到的id将每条数据一个个开始回表,比对字段值查询,然后返回;
但是mysql5.6以后引入 索引下推优化【index condition pushdown 】简称icp,意思就是在索引遍历的结果中,先通过右侧索引包含的字段直接过滤掉不满足的条件记录,减少回表的次数。这里把条件判断提前了,而不是访问完磁盘查询到结果放到结果集前才去判断过滤。
在icp作用下的执行流程:
-
(1) server层将name like “李%”传入存储引擎:
-
(2) 引擎「快速定位」找到第一个 姓“张”的用户的记录行;5.6之前只能回表查询数据库的值,然后比对其他的条件;
-
(3)但是在5.6之后,引入了索引下推,在索引遍历的过程中,对索引包含的字段先做判断,直接过滤掉age!=18的值,减少回表的次数;
总之:联合索引因为有了icp优化,所以还是应该尽量将查询的字段放入联合索引中。
这个优化我觉得还是比较合理的,不然多个联合索引将造成右侧索引的浪费,没有物尽其用。
5、优化的方向
-
减少数据访问(减少磁盘访问) :使用主键索引和覆盖索引;
-
返回更少数据(减少网络传输或磁盘访问) :数据分页处理、只返回需要的字段;
-
减少交互次数(减少网络传输) :批量执行、使用存储过程(一般不建议使用);
-
减少服务器CPU开销(减少CPU及内存开销):使用绑定变量、合理使用排序 、减少比较操作、大量复杂运算在客户端处理;
5.1、平时代码书写注意
-
把聚合排序和计算放到java业务层,让sql更简单,更高效返回;
-
limit 的使用: 避免limit 100000,100 , 扫描行数过多。使用where id > 1000 limit 100;
-
避免select *,返回尽量少的字段,按需查询,有可能用上覆盖索引;
-
避免一次查询过多数据,数据量较大时一定要做分页;
-
如果明确只有一条返回在sql后面加上limit 1;
-
尽量使用count(*),不要使用count(字段)/(id)/(1) ;
-
数据区分度不大,不建议使用索引:where gender = 'M';性别只有男、女、未知三种;
-
尽量不好使用负向查询,例如:!=、not in、not exists;
5.2、常见的容易引起索引失效
-
隐式转换;
-
sql语句中对索引列进行函数和算术运算;
-
使用负向查询:不等于:<> not in ,!=,not exist;
-
like "%xxx";
-
前缀索引导致的覆盖索引失效;
失效情况举例:
(1). 隐式类型转 换导致索引失效.这一点应当引起重视.也是开发中经常会犯的错误.
如果表字段是int类型,传入的是string类型,索引不会失效;
如果表字段是char类型,传入的是int类型,会发生类型转换导致索引失效;
6、问题思考
问题 1、表的索引并非越全越好?
问题 2、为什么不要在离散度低,例如性别上建立索引?
问题 3、不要使用select *,写明具体查询字段,为什么?
-
可以利用覆盖索引,避免了回表;
-
减少数据量,从而降低了网络的传输压力;
问题4: 为什么不建议使用长的字符串做索引? 过长的索引如何建立索引?
索引是一棵单独落盘的B+树,浪费空间。如果过长可以采用以下方案:
-
完整索引:直接创建 完整索引,这样可能比较占空间;
-
前缀索引:创建前缀索引,节省空间,但是会 增加扫描次数,并且不能使用覆盖索引;因为系统无法确定该索引是截取的还是完整的;
-
倒序前缀索引 :先倒序存储,例如省份证只有后面的几位不同,再创建前缀索引,用于绕过字符串本身前缀区分读不够的问题;
-
hash索引:创建 hash字段索引,查询性能稳定,有额外的存储和消耗。
-
fullTest只有myisam支持,innodb不支持;
问题5: 索引为啥不存储主键的地址而是存储值呢,这样不浪费空间吗?
7、小结
索引类似于书的目录,需要好好利用,但也不是越多越好,需要注意查询和维护代价利弊的分析。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固,原内容2月有更新。
##参考资料,
《Innodb存储引擎》
《MySql实战详解》--丁奇