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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • 索引 数据库中的查询操作非常普遍,索引就是提升查找速度的一种手段 索引的类型 从数据结构角度分 1.B+索引:传统...
    一凡呀阅读 2,844评论 0 8
  • 面试题5:union all 和 union的区别 Union:对两个结果集进行并集操作,不包括重复行,同时进行默...
    行者和他的钢笔阅读 919评论 0 1
  • 一、MySQL优化 MySQL优化从哪些方面入手: (1)存储层(数据) 构建良好的数据结构。可以大大的提升我们S...
    宠辱不惊丶岁月静好阅读 2,400评论 1 8
  • 我终于离开了,舒坦! 我相信你做的决定,是你所能做的最佳选择。我们人,都是会被情绪左右,被接触的面所限制的,我也不...
    碎落的面包屑阅读 239评论 0 1
  • 雪后初晴 窗明几净 阳光如诗如歌 没人知道 有个人一直在等待 如此这般的晌午 愿时光倒流 飞奔至你楼下 恳请麻雀在...
    从此萧朗是路人阅读 278评论 0 2