为什么用了索引,查询还是慢

一个sql语句使用了索引,为什么还是会进入到慢查询中?

案例剖析

为了实验,创建如下表:

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

首先判断SQL语句是不是慢查询语句,用的是语句的执行时间。他把语句执行时间跟long_query_time这个系统参数做比较,如果语句执行时间比他大,就会记录到慢查询日志里面,这个参数默认是10秒。当然生产生一般不会这么大。

我们看下explain select * from t;查看key的结果是null。
explain select * from t where id = 1;的key的结果是PRIMARY,使用了主键索引。
explain select a from t 的key结果是a,表示使用了a这个索引。
虽然后两个查询的key都不是null,但其实最后一个扫描了整个索引树。
假设数据库中数据较多,比如有100万条,使用主键索引还是可以很快,但是第三条语句就很慢了。更极端的情况,如果cpu压力非常大,那么第二条语句也可能非常慢,会进入long_query_time中。
所以:是否使用索引和是否进入慢查询之间并没有必然的联系。使用索引只是表示一个sql语句的执行过程,而是否进入慢查询是有它的执行时间决定的,而这个执行时间,可能会受到各种外部因素的影响,也就是说使用了索引sql查询依然很慢

全索引扫描的不足

InnoDB是索引组织表,所有的数据都存储在索引树上。从逻辑上来说,所有的InnoDB表上的查询,都至少使用了一个索引,所以比如执行select a from t where id > 0;explain上看输出结果显示的是primary,但从数据上看其实是做了全局扫描。但是优化器认为,这个语句的执行过程中,需要根据主键索引,定位到满足id>0的,也算用到了索引。
所以即使explain的结果里面的key不是null,也有可能是全扫描,因此InnoDB只有一种情况没有使用索引,就是从主键左边的叶子节点开始,往右扫描整个索引树。

索引的过滤性要好

我们知道全索引扫描会让查询变慢,接下来我们谈谈索引的过滤性:
假设有一张表,维护了中国14亿人口的基本信息,现在要查出10~15岁之间的姓名和基本信息,那么语句是不是会这么写:
select * from t_people where age between 10 and 15;
一看这个语句就是要在age上建立索引,否则进行全面扫描,但是建立索引后,这个语句还是会很慢,因为满足条件的结果行太多了。
建立索引之后,这个语句的执行流程是这样的:

  • 从索引上用树搜索,取到第一个age等于10的记录,得到它的主键id的值,根据id的值去主键索引整行的信息,作为结果集中的一部分返回。
  • 在索引age上向右扫描,取下一个id的值,到主键上获取信息,返回。
  • 重复上面的步骤,直到碰到第一个age>15的记录。
    然后我们看到虽然它使用了索引,但它扫描了一亿行,所以我们现在知道我们在讨论有没有使用索引的时候,其实就是在关心扫描的行数。
    对于一个大表,不止要有索引,缩印的过滤性要足够好
    刚才的例子,age的过滤性就不够好,所以在表结构设计时,要让所有的过滤性足够好,也就是区分度足够高。

回表的代价

再看一个例子:
select * from t_people where name = '张三' and age = 8;
t_people表上有一个索引是姓名和年龄的联合索引,过滤应该不错,可以在联合索引上快速找到第一个姓名是张三,并且年龄<10,而这样的people并不多,所以右扫描的行数很少,查询效率就很高。
如果现在的需求是查出所有名字的第一个字是张,并且年龄是8岁的people,你可能会这么写:
select * from t_people where name like '张%' and age = 8;
在MySQL5.5及之前的版本,执行流程是这样的:

  • 首先从联合索引上找到第一个名字字段是张开头的记录,取出id,然后到主键索引树上,根据id取出整行数据。
  • 判断年龄段是否为8,如果是就返回,否则丢弃
  • 在联合索引上向右遍历,并重复做回表和判断的逻辑,直到碰到联合索引树上的名称的第一个字不是张为止。
    我们把根据id到主键索引上查找整行数据这个动作叫做回表,在这个执行过程中,最耗费时间的就是回表。
    有没有什么优化的方法:
    在MySQL5.6版本,引入index condition pushdown的优化。优化流程:
  • 首先从索引树上,找到第一个名字字段是张开头的记录,判断这个索引记录里面,年龄的值是不是8,如果是就回表,取出整行记录返回。
  • 在联合索引树上,向右遍历,并判断年龄字段,根据需要做回表,知道碰到联合索引上名字的第一个字不是张的记录位置。
    这次和上次的差别,在遍历联合索引的过程中,将年龄等于8的条件下推到所有遍历的过程中,减少回表次数。假设全国名字第一字为张的people里面,有100万个是8岁的小朋友,那么查询过程中在联合索引里要遍历8000万次,回表只需要100万次。

虚拟列

上面的优化还是没有绕开最左前缀原则的限制,因此在联合索引还是扫描8000万行,有没有更优的优化呢?

我们可以考虑把名字的第一个字和age来做一个索引,这边使用5.7版本的虚拟列来实现。对应的修改表结构的语句:
alter table t_people add name_first varchar(2) generated always as (left(name, 1)), add index(name_first, age);
首先创建一个字段name_first的虚拟列,然后给name_first和age创建一个联合索引,并且让这个虚拟列的值总等于name字段的前两个字节。有了新的联合索引,我们这么写:select * from t_people where name_first = '张' and age = 8;
这样这个语句的执行过程,就只需要扫描联合索引的100万,回表100万次。我们这样创建了一个更紧凑的索引来加速查询。

总结:我们查询优化的过程,其实就是减少扫描行数的过程。

你可能感兴趣的:(为什么用了索引,查询还是慢)