SQL使用了索引,为什么还是很慢

数据库的查询性能一直是性能优化的重灾区。近期看了丁奇老师的分享“为什么SQL语言使用了索引,但却还是慢查询?”,收获颇丰,以此做个记录

0 准备

创建一张测试表,设置主键索引(id)和普通索引(a) ,如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

插入两行测试数据

insert into t values(1,1,1),(2,2,2);

1 定义慢查询

我们来看一下官网对慢查询的定义

The slow query log consists of SQL statements that take more than long_query_time seconds to execute and require at least min_examined_row_limit rows to be examined.

mysql的慢查询与语句的执行时间有关,mysql会将语句的执行时间与系统设置的long_query_time这个参数做对比,如果执行时间time > long_query_time并且扫描的行数超过min_examined_row_limit, 则会被认为是慢查询,并将该语句到慢查询日志中。

查看mysql的long_time_time设置

mysql > show variables like 'long_query_time%';

## 如下所示,慢查询设置的时间为10秒
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+

查看慢查询日志的地址

mysql > show variables  like '%slow_query_log%';

## slow_query_log 表示慢查询日志是否开启
## slow_query_log_file 表示慢查询日志的地址
+---------------------+-------------------------------------------------+
| Variable_name       | Value                                           |
+---------------------+-------------------------------------------------+
| slow_query_log      | OFF                                             |
| slow_query_log_file | /usr/local/mysql/data/ali-186590d5fadf-slow.log |
+---------------------+-------------------------------------------------+

2 定义是否使用索引

对于一个语句,是否使用索引的意思是:这个语句在执行过程中是否使用到了索引。具体到表象中,是explain一个语句的时候,输出结果里key的值不是NULL

mysql > explain select * from t ;

## 如下key为null,表示未使用索引

+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | t     | ALL  | NULL          | NULL | NULL    | NULL |    2 | NULL  |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+

mysql> explain select * from t where id =2;

## 如下key为primary,表示使用了主键索引
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | t     | const | PRIMARY       | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

mysql> explain select a from t ;
## 如下key为a,表示使用了a这个普通索引
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | t     | index | NULL          | a    | 5       | NULL |    2 | Using index |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+

看上面的结果可以知道,2和3都使用了索引,但是他们的扫描行数(rows这一个字段)是不同的,2使用主键索引其扫描行数为1条,3使用了普通索引其扫描的行数是2条,由于总记录数是2条,所以3的方式几乎是全表扫描了。如果总记录数达到100万条,2的方式还是会很快,而3的方式其扫描的行数也将达到100万条,查询就会很慢。

但是这并不代表2的方式肯定不是慢查询,因为在极端情况下,如果mysql所在机器的cpu负载很高,2方式查询的执行时间超过了long_query_time,那么它就会被记录到慢查询日志中。

3. 慢查询与索引的关系

看了上述1,2就明白了,是否是慢查询与是否使用索引其实并无关系
慢查询: 慢查询与语句的执行时间相关,当语句的执行时间超过long_query_time,那么它就是慢查询
索引: 是否使用索引描述了语句的执行过程

4. 解析使用索引

在InnoDB中,数据都是存储在主键索引上的,如图所示。一行的所有信息都存储在主键索引上(所以InnoDB在查询过程中必然会扫描主键索引树),而普通索引上只记录了普通索引与对应主键索引的映射关系

image.png

那么在语句执行过程中,使用了主键索引,就一定不是全表扫描吗? 不一定,如下,虽然key为primary了,但是扫描行数还是全表扫描。优化器认为该语句需要使用主键索引定位到id>0的行,所以认为该执行使用了主键索引,

mysql> explain select * from t where id >0;
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | t     | range | PRIMARY       | PRIMARY | 4       | NULL |    2 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+

因此,我们需要理清几个概念:
全表扫描: 我们平时说的使用了全表扫描,指的并不是说语句不使用索引,而是该语句遍历了整个主键索引树 (select * from t where id > 0)
全索引扫描:指的是语句扫描了整个普通索引树(select a from t )
索引扫描: 指的是语句使用了索引的快速搜索功能,并且有效地减少了扫描行数(select * from t where id =2)

4.1 索引的过滤性

除了全索引扫描,还有哪些使用了索引但执行速度不够快的例子呢?

假设现在你维护了一张表记录了全国人的基本信息,需要查找所有年龄在10~15岁之间的姓名和基本信息。该语句为:

select * from t_people where age between 10 and 15

一般我们会认为这样的语句需要在age上建立索引,否则它就会变成全表扫描。但是最后你会发现,即使在age上建立了索引,执行的速度还是很慢,因为符合条件了记录数有上亿行。该语句的执行过程如下:

  1. 在age索引上找到age=10的第一个索引
  2. 根据索引对应的主键id,回表到主键索引上取整行数据
  3. 从age索引继续向下搜索,重复步骤2,直到碰到age > 15结束
image.png

由于上述的执行过程中符合条件的记录数超过上亿行,因此执行的时间会很长,它会被记录到慢查询中。而上述select a from t虽然也是全索引扫描,但是还是比这个全索引扫描的要快,因此它只扫描了2行。因此,语句执行速度的快慢与扫描的行数息息相关,所以我们在讨论索引的时候关心的是扫描行数,因此索引要有效,就要提高索引的过滤性,减少语句的扫描行数。上述的age索引过滤性就不够。

那么过滤性足够好了,扫描的行数就一定少呢?

如下有一个(name,age)联合索引,如果执行语句是 select * from t_people where name ='张三' and age = 8,那么该联合索引的过滤性非常不错,扫描的行数也会很少,执行的效率较高

image.png

但是查询的过滤性和索引的过滤性不一定是一样的,如果现在的需求是
select * from t_prople where name like '张%' and age =8
在mysql 5.5及之前的版本中,这个语句的执行流程如下:

  1. 在联合索引上查找第一个名字为张的联合索引
  2. 取出联合索引对应的主键id,回表到主键索引中获取整行速度并对比age是否为8
  3. 继续在联合索引中向下扫描,重复2,直到碰到第一个名字不为张结束

可以看到,上述的执行过程中,最耗费时间的就是2步骤的回表。假设全国第一个名字开头是张的有8000万,那么这个执行过程就需要回表8000万次。为什么联合索引需要回表取判断age是否为8? 这是因为在使用联合索引的时候,只能使用联合索引的最左前缀,这被称为最左前缀原则。

image.png

在mysql 5.6版本引入了一个优化方案,该优化方案是在联合索引中先判断age是否为8(将判断下推到索引搜索过程中),如果是才回表取出整行数据,假设名字开头是张的有8000万,其中age=8的有1000万行,那么该查询只需要回表1000万次,这会大大减少回表的次数。

但是该查询还是要扫描8000万行的记录,是否可以继续优化,减少扫描行数呢?

我们可以把名字的第一个字和年龄做一个联合索引,我们可以使用mysql 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列的前两个字节。并根据虚拟列生成了一个新的联合索引(name_first,age)。有了这个新的联合索引,我们就可以通过以下语句来优化执行语句
select * from t_prople where name_first = '张' and age =8
假设结果是1000万行,那么该语句只需要扫描1000万行,回表1000万次。这个优化的本质是我们创建了一个过滤性更好的索引。

我们使用索引,或者说索引优化的过程,实质上是减少扫描行数的过程

5. 问题

5.1 什么是SQL的执行时间 ?

sql语句的执行时间并不包含其获取锁的等待时间,而且慢查询日志插入的时间是在sql语句释放所有锁之后,因此日志顺序和执行顺序很可能是不同的

5.2 我在where中使用了普通索引字段,系统一定会使用全索引扫描吗?

不一定,系统可能会走全表扫描,这取决与系统的判断。判断来源于系统的预测,也就是说,如果要走 c 字段索引的话,系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多,它可能就不走索引而直接扫描全表了。

5.3 如果查询的字段上有索引,查询语句一定会使用索引扫描吗?

不一定,查询优化器在选择查询计划的时候,会对不同查询计划进行评估(通过一个cost function),选择它认为执行最快的查询计划。

5.4 优化器使用的索引不满意,可以自行指定sql使用的索引吗?

是可以的,使用use index来指定sql执行使用的索引,这是一种优化手段,具体可看: https://www.cnblogs.com/edwardlost/archive/2010/12/31/1923105.html

6. 本文摘录及参考自:

###1. The Slow Query Log - MySQL :: Developer Zone
###2. 为什么我加了索引,SQL执行还是这么慢*(一)? - 李佳霖i - 博客园
###3. https://www.zhihu.com/question/49738281?sort=created

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