为何我使用了索引,查询仍是慢?

常常有同窗问我,个人一个SQL语句使用了索引,为何仍是会进入到慢查询之中呢?今天咱们就从这个问题开始来聊一聊索引和慢查询。程序员

另外插入一个题外话,我的认为团队要合理的使用ORM,能够参考 ORM的权衡和抉择。合理利用的是ORM在面向对象和写操做方面的优点,避免联合查询上可能产生的坑(固然若是你的Linq查询能力很强另当别论),由于ORM屏蔽了太多的DB底层的知识内容,对程序员不是件好事,对性能有极致追求,可是ORM理解不透彻的团队更加要谨慎。面试

案例剖析

言归正传,为了实验,我建立了以下表:数据库

CREATE TABLE `T`(
`id` int(11) NOT NULL,
`a` int(11) DEFAUT NULL,
PRIMARY KEY(`id`),
KEY `a`(`a`)
) ENGINE=InnoDB;

该表有三个字段,其中用id是主键索引,a是普通索引。设计模式

首先SQL判断一个语句是否是慢查询语句,用的是语句的执行时间。他把语句执行时间跟long_query_time这个系统参数做比较,若是语句执行时间比它还大,就会把这个语句记录到慢查询日志里面,这个参数的默认值是10秒。固然在生产上,咱们不会设置这么大,通常会设置1秒,对于一些比较敏感的业务,可能会设置一个比1秒还小的值。性能

语句执行过程当中有没有用到表的索引,能够经过explain一个语句的输出结果来看KEY的值不是NULL。学习

咱们看下 explain select * from t;的KEY结果是NULL优化

file
  (图一)设计

explain select * from t where id=2;的KEY结果是PRIMARY,就是咱们常说的使用了主键索引日志

file
 (图二)code

explain select a from t;的KEY结果是a,表示使用了a这个索引。

file
 (图三)

虽而后两个查询的KEY都不是NULL,可是最后一个实际上扫描了整个索引树a。

假设这个表的数据量有100万行,图二的语句仍是能够执行很快,可是图三就确定很慢了。若是是更极端的状况,好比,这个数据库上CPU压力很是的高,那么可能第2个语句的执行时间也会超过long_query_time,会进入到慢查询日志里面。

因此咱们能够得出一个结论:是否使用索引和是否进入慢查询之间并无必然的联系。使用索引只是表示了一个SQL语句的执行过程,而是否进入到慢查询是由它的执行时间决定的,而这个执行时间,可能会受各类外部因素的影响。换句话来讲,使用了索引你的语句可能依然会很慢。

全索引扫描的不足

那若是咱们在更深层次的看这个问题,其实他还潜藏了一个问题须要澄清,就是什么叫作使用了索引。

咱们都知道,InnoDB是索引组织表,全部的数据都是存储在索引树上面的。好比上面的表t,这个表包含了两个索引,一个主键索引和一个普通索引。在InnoDB里,数据是放在主键索引里的。如图所示:

file
能够看到数据都放在主键索引上,若是从逻辑上说,全部的InnoDB表上的查询,都至少用了一个索引,因此如今我问你一个问题,若是你执行select from t where id>0,你以为这个语句有用上索引吗?

file
咱们看上面这个语句的explain的输出结果显示的是PRIMARY。其实从数据上你是知道的,这个语句必定是作了全面扫描。可是优化器认为,这个语句的执行过程当中,须要根据主键索引,定位到第1个知足ID>0的值,也算用到了索引。

因此即便explain的结果里写的KEY不是NULL,实际上也多是全表扫描的,所以InnoDB里面只有一种状况叫作没有使用索引,那就是从主键索引的最左边的叶节点开始,向右扫描整个索引树。

也就是说,没有使用索引并非一个准确的描述。

你能够用全表扫描来表示一个查询遍历了整个主键索引树;

也能够用全索引扫描,来讲明像select a from t;这样的查询,他扫描了整个普通索引树;

而select * from t where id=2这样的语句,才是咱们平时说的使用了索引。他表示的意思是,咱们使用了索引的快速搜索功能,而且有效的减小了扫描行数。

索引的过滤性要足够好

根据以上解剖,咱们知道全索引扫描会让查询变慢,接下来就要来谈谈索引的过滤性。

假设你如今维护了一个表,这个表记录了中国14亿人的基本信息,如今要查出全部年龄在10~15岁之间的姓名和基本信息,那么你的语句会这么写,select * from t_people where age between 10 and 15

你一看这个语句必定要在age字段上开始创建索引了,不然就是个全面扫描,可是你会发现,在你创建索引之后,这个语句仍是执行慢,由于知足这个条件的数据可能有超过1亿行。

咱们来看看创建索引之后,这个表的组织结构图:

file

这个语句的执行流程是这样的:
从索引上用树搜索,取到第1个age等于10的记录,获得它的主键id的值,根据id的值去主键索引取整行的信息,做为结果集的一部分返回;

在索引age上向右扫描,取下一个id的值,到主键索引上取整行信息,做为结果集的一部分返回;

重复上面的步骤,直到碰到第1个age大于15的记录;

你看这个语句,虽然他用了索引,可是他扫描超过了1亿行。因此你如今知道了,当咱们在讨论有没有使用索引的时候,其实咱们关心的是扫描行数。

对于一个大表,不止要有索引,索引的过滤性还要足够好。

像刚才这个例子的age,它的过滤性就不够好,在设计表结构的时候,咱们要让全部的过滤性足够好,也就是区分度足够高。

回表的代价

那么过滤性好了,是否是表示查询的扫描行数就必定少呢?

咱们再来看一个例子:

若是你的执行语句是 select * from t_people where name='张三' and age=8

t_people表上有一个索引是姓名和年龄的联合索引,那这个联合索引的过滤性应该不错,能够在联合索引上快速找到第1个姓名是张三,而且年龄是8的小朋友,固然这样的小朋友应该很少,所以向右扫描的行数不多,查询效率就很高。

可是查询的过滤性和索引的过滤性可不必定是同样的,若是如今你的需求是查出全部名字的第1个字是张,而且年龄是8岁的全部小朋友,你的语句会怎么写呢?

你的语句要怎么写?很显然你会这么写:select * from t_people where name like '张%' and age=8;

在MySQL5.5和以前的版本中,这个语句的执行流程是这样的:
file

首先从联合索引上找到第1个年龄字段是张开头的记录,取出主键id,而后到主键索引树上,根据id取出整行的值;

判断年龄字段是否等于8,若是是就做为结果集的一行返回,若是不是就丢弃。

在联合索引上向右遍历,并重复作回表和判断的逻辑,直到碰到联合索引树上名字的第1个字不是张的记录为止。

咱们把根据id到主键索引上查找整行数据这个动做,称为回表。你能够看到这个执行过程里面,最耗费时间的步骤就是回表,假设全国名字第1个字是张的人有8000万,那么这个过程就要回表8000万次,在定位第一行记录的时候,只能使用索引和联合索引的最左前缀,最称为最左前缀原则。

你能够看到这个执行过程,它的回表次数特别多,性能不够好,有没有优化的方法呢?

在MySQL5.6版本,引入了index condition pushdown的优化。咱们来看看这个优化的执行流程:

file
首先从联合索引树上,找到第1个年龄字段是张开头的记录,判断这个索引记录里面,年龄的值是否是8,若是是就回表,取出整行数据,做为结果集的一部分返回,若是不是就丢弃;

在联合索引树上,向右遍历,并判断年龄字段后,根据须要作回表,直到碰到联合索引树上名字的第1个字不是张的记录为止;

这个过程跟上面的差异,是在遍历联合索引的过程当中,将年龄等于8的条件下推到全部遍历的过程当中,减小了回表的次数,假设全国名字第1个字是张的人里面,有100万个是8岁的小朋友,那么这个查询过程当中在联合索引里要遍历8000万次,而回表只须要100万次。

虚拟列

能够看到这个优化的效果仍是很不错的,可是这个优化仍是没有绕开最左前缀原则的限制,所以在联合索引你仍是要扫描8000万行,那有没有更进一步的优化方法呢?

咱们能够考虑把名字的第一个字和age来作一个联合索引。这里可使用MySQL5.7引入的虚拟列来实现。对应的修改表结构的SQL语句:

alter table t_people add name_first varchar(2) generated (left(name,1)),add index(name_first,age);

咱们来看这个SQL语句的执行效果:

CREATE TABLE `t_people`(
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAUT NULL,
`name_first` varchar(2) GENERATED ALWAYS AS (left(`name`,1)) VIRTUAL,KEY `name_first`(`name_first`,'age')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

首先他在people上建立一个字段叫name_first的虚拟列,而后给name_first和age上建立一个联合索引,而且,让这个虚拟列的值老是等于name字段的前两个字节,虚拟列在插入数据的时候不能指定值,在更新的时候也不能主动修改,它的值会根据定义自动生成,在name字段修改的时候也会自动修改。

有了这个新的联合索引,咱们在找名字的第1个字是张,而且年龄为8的小朋友的时候,这个SQL语句就能够这么写:select * from t_people where name_first='张' and age=8。

这样这个语句的执行过程,就只须要扫描联合索引的100万行,并回表100万次,这个优化的本质是咱们建立了一个更紧凑的索引,来加速了查询的过程。

总结

本文给你介绍了索引的基本结构和一些查询优化的基本思路,你如今知道了,使用索引的语句也有多是慢查询,咱们的查询优化的过程,每每就是减小扫描行数的过程。

慢查询概括起来大概有这么几种状况:

  • 全表扫描
  • 全索引扫描
  • 索引过滤性很差
  • 频繁回表的开销

思考

假设业务要求的就是要统计年龄在10-15岁的14亿人的数量,不能增长过滤因子,那该怎么办?(select * from t_people where age between 10 and 15)

假设该统计必须是OLTP,实时展现统计数据,又该怎么解决?

欢迎关注公众号 【码农开花】一块儿学习成长 我会一直分享Java干货,也会分享免费的学习资料课程和面试宝典 回复:【计算机】【设计模式】【面试】有惊喜哦

相关文章
相关标签/搜索