【Mysql进阶-3】大量实例悟透EXPLAIN与慢查询

时间:2022-07-25
本文章向大家介绍【Mysql进阶-3】大量实例悟透EXPLAIN与慢查询,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

“你一定又写了烂SQL了!”,“你怎么这样凭空污人清白……慢查询,慢查询不能算烂……慢查询!……程序猿的事,能算烂么?” 本文从SQL执行效率方面略作研究,偏向基础性总结,但力求详实准确。如果有大佬误入此地,还请从容撤退,如果你真的愿意看,我也没什么意见。

本人水平有限,如有疏漏之处请轻喷。

1 EXPLAIN

EXPLAIN关键字能够分析呈现Mysql处理SQL语句的诸多要素,进而分析SQL语句的瓶颈所在。

在看下面内容之前,墙裂建议先看本文姊妹篇:图文并茂说透Mysql索引

EXPLAIN的使用非常简单,在SQL语句前加上EXPLAIN即可,例如:

EXPLAIN SELECT * FROM students;

执行后,会出现下图所示的结果:

执行EXPLAIN后出现上图所示结果表,读懂这个结果表才是关键,下面解释一下每个字段的含义(加粗行表示比较重要的项):

释义

id

列编号是 SELECT 的序列号,并且 id 的顺序是按 SELECT 出现的顺序增长的。id列越大执行优先级越高,id 相同则从上往下执行,id 为 NULL 最后执行。

select_type

SELECT关键字对应的查询类型

table

表名、表别名或临时表的标识

partitions

分区信息

type

表示关联类型或访问类型,即MySQL决定如何查找表中的行

possible_keys

可能用到的索引

key

实际使用的索引

key_len

实际使用的索引的长度

ref

使用索引列等值查询时,与索引列等值匹配的对象信息

rows

查询优化器估计要读取并检测的行数

Extra

额外信息,如Using index、Using where等

1、select_type表示查询类型,包括简单查询、复杂查询、子查询等:

类型

释义

SIMPLE

简单的SELECT查询,查询中不包含子查询或UNION

PRIMARY

查询中若包含任何复杂的子部分,最外层查询被标记为PRIMARY

SUBQUERY

在SELECT或WHERE中包含了子查询

DERIVED

在FROM中包含的子查询被标记为DERIVED,MySQL会递归执行这些子查询,把结果放在临时表里

UNION

若第二个SELECT出现在UNION之后,则被标记为UNION,若UNION包含在FROM子句的子查询中,外层SELECT将被标记为DERIVED

UNION RESULT

从UNION表获取结果的SELECT

2、type表示关联类型或访问类型,即MySQL决定如何查找表中的行:

类型

释义

system、const

const表示查询使用了主键索引(primary key)或唯一索引,system是表只有一行记录(等于系统表)时的type,是 const 类型的特例

eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref

ref

相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行

ref_or_null

对普通二级索引进行等值查询,该索引列也可以为NULL值时

index_merge

使用不同的索引查询并将结果合并

range

使用索引查询范围结果,通常出现在 in, between ,> ,<, >= 等操作中。

index

查询语句对一个索引树进行了全量扫描

ALL

全表扫描,MySQL会遍历所有行去查找结果,这种类型是效率最差的类型,必须进行索引优化

3、Extra表示额外信息:

类型

释义

Using index

查询语句只扫描一次索引树即获得了目标数据,效率很高,一般是通过索引列查询主键或查询与索引列建有联合索引的列

Using where Using index

无法直接通过索引查找来查询到符合条件的数据,一般是使用索引前导列进行范围查询或通过索引的非前导列查询

Using index condition

查询列的某一部分无法直接使用索引,一般是WHERE 条件列是索引前导列且是范围查询导致的

NULL

WHERE条件是索引前导列,但查询列至少有一个未与条件列在同一个索引树上,必须通过回表查询

Using where

WHERE条件列上无索引(既没有单独索引,也没有联合索引),而与查询列无关

Using filesort

MySQL需要创建一张内部临时表来处理查询,通常在许多执行包括DISTINCT、GROUP BY、ORDER BY等子句查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能会寻求建立内部临时表来执行查询

Using filesort

当SQL中使用ORDER BY关键字的时候,如果待排序的内容不能由所使用的索引直接完成排序的话,那么mysql有可能就要进行文件排序

Index merges

对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union),一般出现AND和OR查询

下面,我会构造测试表与数据,详细演示每一个案例,请做好战斗准备!感兴趣的同学,可以自己建表动手测试一下,毕竟纸上得来终觉浅,绝知此事要躬行。演示数据在文末。

1.1 id

id 列是 SELECT 的序列号,其顺序是按 SELECT 出现顺序增长的。id列越大执行优先级越高,id 相同则从上往下执行,idNULL 最后执行。

1.2 select_type

MySQL将 SELECT 查询分为简单查询 SIMPLE 和复杂查询 PRIMARY。复杂查询包括:简单子查询、派生表( FROM 语句中的子查询)、UNION 和 UNION ALL 查询。

1、SUBQUERY

SELECTWHERE中包含了子查询,例如:

EXPLAIN SELECT (SELECT 1 FROM student LIMIT 1) FROM student;

WHERE条件中含子查询,select_type也会出现PRIMARYSUBQUERY

EXPLAIN SELECT id FROM student WHERE id = (SELECT s_id id FROM class_student WHERE c_id=3)

2、DERIVED

FROM中包含的子查询被标记为DERIVED,最外层查询被标记为PRIMARY,如下面这个SQL:

EXPLAIN SELECT * FROM (SELECT * FROM student GROUP BY id) AS temp;

3、UNION和UNION ALL

UNIONUNION ALL是对两个SQL结果进行纵向合并,即列数不变,行数增 加,前者对合并结果去重,后者不去重。

因此,UNION 会将合并结果放在一个匿名临时表中进而做去重操作,临时表不在 SQL 中出现,临时表名为 <union1, 2>,因此它的 id 是 NULL,表明这个临时表是为了合并两个查询结果集而创建的。

EXPLAIN SELECT * FROM student WHERE id =1 UNION SELECT * FROM student

UNION ALL 无需为合并结果去重,仅是将多个查询结果集中的记录合并成一个,所以不会使用到临时表,故没有 id 为 NULL 记录:

EXPLAIN SELECT * FROM student WHERE id =1 UNION ALL SELECT * FROM student

另外,我们通过EXPLAIN可以看出,查询优化器往往会将SQL进行重写以达到优化效果,例如将子查询优化为连接查询:

EXPLAIN SELECT * FROM student WHERE id IN(SELECT s_id FROM class_student)

上面的子查询并没有出现预期的SUBQUERYselect_type

1.3 table

table 列表示 EXPLAIN 的单独行的唯一标识符。这个值可能是表名、表的别名或者一个未查询产生临时表的标识符,如派生表、子查询或集合。当 FROM 子句中有子查询时,如果优化器采用的物化方式,table 列是 <derivenN> 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。

当使用 UNION 查询时,UNION RESULTtable 列的值为 <UNION1,2>,1和2表示参与 UNIONSELECT 的行 id

1.4 type

type 表示关联类型或访问类型,即MySQL决定如何查找表中的行,从最好到最差依次排列:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

1、system、const

system是表只有一行记录(等于系统表)时的type,是 const 类型的特例,平时不会出现。const表示查询使用了主键索引(primary key)或唯一索引(unique),因为这两种索引具有唯一性,结果必然只匹配到一行数据,所以查询速度很快。

EXPLAIN SELECT * FROM student where id = 1

2、eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref。这可能是在 const 之外最好的联接类型了。

EXPLAIN SELECT * FROM class_student JOIN student ON class_student.s_id=student.id

3、ref

这意味着WHERE条件是索引前导列,且该索引是普通索引。如下SQL,name上建有普通索引:

EXPLAIN SELECT * FROM student WHERE name = '赵日天';

关联表查询,被驱动表使用普通索引,type也会是ref:

EXPLAIN SELECT s_id FROM student LEFT JOIN class_student ON student.id = class_student.s_id;

4、ref_or_null

对普通二级索引进行等值查询,该索引列也可以为NULL值时,例如:

EXPLAIN SELECT * FROM student WHERE name = '李胜利' OR name IS NULL

5、index_merge

顾名思义,使用不同的索引查询并将结果合并,例如:

EXPLAIN SELECT * FROM student WHERE student.name = '李胜利' OR id=3

6、range

使用索引查询范围结果,通常出现在 in, between ,> ,<, >= 等操作中。

EXPLAIN SELECT * FROM student WHERE id > 3

7、index

这种情况意味着查询语句对一个索引树进行了全量扫描,出现这种情况是因为:

  1. 查询列在同一个索引树上,但没有查询条件
  2. 查询列在同一个索引树上,但WHERE条件是索引的非前导列,导致不能直接在索引中定位

例如,name和age建有联合索引,且name是索引前导列。先看第一种情况:

EXPLAIN SELECT id,name FROM student

其次,使用索引的非前导列age作为条件进行查询:

EXPLAIN SELECT name FROM student WHERE age=17

8、ALL

全表扫描,MySQL会遍历所有行去查找结果,这种类型是效率最差的类型,很有必要进行索引优化。

出现这种情况,原因在于查询语句无法使用索引。假设name和age分别建有普通单列索引,create_time上无索引,下面两种情况会导致type为ALL:

  1. WHERE条件列上无索引,例如:EXPLAIN SELECT name FROM student WHERE create_time='2019'
  2. 查询语句无WHERE条件,且查询列无索引或不在同一个索引树上,例如EXPLAIN SELECT age,name FROM student

出现了ALL,SQL语句就很有优化的必要了,优化思路针对上面两种情形:要么对WHERE列加索引,要么保证查询列在同一个索引树上(比如建立联合索引)。

1.5 possible_keys

possible_keys表示SQL可能使用哪些索引来查找。

EXPLAIN 执行计划结果可能出现 possible_keys 列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,MySQL 会认为索引对此查询帮助不大,选择了全表查询。

如果 possible_keys 列为 NULL,则没有相关的索引。在这种情况下,可以通过检查 WHERE 子句去分析下,看看是否可以创造一个适当的索引来提高查询性能,然后用 EXPLAIN 查看效果。

另外注意:不是这一列的值越多越好,使用索引过多,查询优化器计算时查询成本高,所以如果可能的话,尽量删除那些不用的索引。

1.6 key

key 列表示SQL实际采用了哪个索引来优化对该表的访问。如果没有使用索引,则该列是 NULL。如果想强制 MySQL使用或忽视 possible_keys 列中的索引,在查询中使用 force index、ignore index。

1.7 key_len

key_len表示索引记录的最大长度。

key_len列计算规则如下:

  • char(n):n字节长度
  • varchar(n):2字节存储字符串长度,如果是utf-8,则长度 3n + 2,如果该列允许存储NULL,则再加1。
  • tinyint:1字节
  • smallint:2字节
  • int:4字节
  • bigint:8字节
  • date:3字节
  • timestamp:4字节
  • datetime:8字节

索引最大长度是768字节,当字符串过长时,MySQL 会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。

例如:student表name字段类型为varchar(25),允许为NULL,那么下面SQL索引长度将为3*25+2+1=78

EXPLAIN SELECT id,name FROM student WHERE name='叶良辰'

1.8 ref、rows、filtered

  • ref:显示了在 key 列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:student.id)。
  • rows:是查询优化器估计要读取并检测的行数,注意这个不是结果集里的行数。如果查询优化器使用全表扫描查询,rows 列代表预计的需要扫码的行数。如果查询优化器使用索引执行查询,rows 列代表预计扫描的索引记录行数。
  • filtered:对于单表来说意义不大,主要用于连接查询中。前文中也已提到 filtered 列,是一个百分比的值,对于连接查询来说,主要看驱动表的 filtered的值 ,通过 rows * filtered/100 计算可以估算出被驱动表还需要执行的查询次数。

1.9 Extra

Extra 列提供了一些额外信息。这一列在 MySQL中提供的信息有几十个。

首先先解释几个概念:

  • 索引覆盖:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度快。
  • 回表:使用聚集索引(聚集索引一般是主键或非空唯一索引)查询可以直接定位到记录,而普通索引通常需要扫描两遍索引树,即先通过普通索引定位到主键值,在通过聚集索引定位到行记录,这就是所谓的回表查询,它的性能比扫描一遍索引树低。
  • 索引前导列:所谓前导列,就是在创建复合索引语句的第一列或者连续的多列。例如,class_student表中,index_s_c_id这个索引建立在s_idc_id上,这个索引称之为复合索引,而s_idindex_s_c_id索引的前导列。

下面看一下Extra中的几个重要的值:

1、Using index

释义: 这种情况意味着查询语句只扫描一次索引树即获得了目标数据,效率很高。

条件:

  1. WHERE条件列上创建有索引,且是索引前导列
  2. 查询列要与条件列在同一棵索引树上,有3种情况:一是查询列即是条件列本身,二是查询列与条件列建立了联合索引,三是查询列是被聚集索引覆盖的列。

示例:

EXPLAIN SELECT id,name FROM student WHERE name='叶良辰'

例如上面的SQL中,id是主键,通过name的索引树可以获取id和name,不需要回表,符合索引覆盖。

当SQL变成下面的样子时,

EXPLAIN SELECT name,age FROM student WHERE name='叶良辰'

对应的索引是name和age建有单独索引:

查询结果可以看出未能达到索引覆盖,效率下降:

我们将name和age的索引改造为联合索引:

再次执行SQL,结果变成了下面这个样子:

以上探索给我们的启示是,对于频繁同时查询的多列,可以考虑建立联合索引来优化。

2、Using where Using index

释义: Mysql无法直接通过索引查找来查询到符合条件的数据。

条件:

  1. WHERE条件列不是索引前导列,查询列与条件列在同一个索引树上(查询列是主键或查询列与条件建有联合索引)
  2. WHERE条件列是索引前导列但使用范围查询时,且查询列与条件列在同一个索引树上

示例:

1)name和age有联合索引,以非前导列age为查询条件时

执行SQL:

EXPLAIN SELECT name FROM student WHERE age=17

2)调整索引,age为联合索引前导列,但使用age进行范围查询

执行SQL:

EXPLAIN SELECT id FROM student WHERE age>17

3、Using index condition

释义:这种情况意味着查询列的某一部分无法直接使用索引

条件:

  1. 至少有一个查询列与条件列不在同一个索引树上,WHERE 条件列是索引前导列且是范围查询
  2. 至少有一个查询列与条件列不在同一个索引树上,WHERE 条件列是索引前导列且是后置模糊查询

示例:

1)假如age是索引前导列,而create_time上无索引,以age为条件进行范围查询

EXPLAIN SELECT create_time FROM student WHERE age>17 AND age<20

2)假如name是索引前导列,而create_time上无索引,以name为条件进行后置模糊查询

EXPLAIN SELECT create_time FROM student WHERE name LIKE '叶%'

4、NULL

释义: 这种情况意味着WHERE条件是索引前导列,但查询列至少有一个未与条件列在同一个索引树上,必须通过回表查询。

示例: 例如name是索引前导列,而create_time上无索引

EXPLAIN SELECT create_time FROM student WHERE name ='叶良辰'

如果这种查询很频繁,可以通过将查询列与条件列建立联合索引来优化。

5、Using where

出现这种情况意味着, WHERE条件列上无索引(既没有单独索引,也没有联合索引),而与查询列无关,例如:

EXPLAIN SELECT id FROM student WHERE create_time='2020-07-28 20:22:40'

可见,这种情况对应的typeALL,也就是进行了全表扫描,效率堪忧。优化的方法很简单,给WHERE条件列添加索引即可。

6、Using temporary

这意味着MySQL需要创建一张内部临时表来处理查询。临时表的建立与维护是需要付出很大成本的,因此执行计划中出现 Using temporary 并不是个好现象,需要考虑使用索引来进行优化。

通常在许多执行包括DISTINCT、GROUP BY、ORDER BY等子句查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能会寻求建立内部临时表来执行查询,例如:EXPLAIN SELECT DISTINCT(create_time) FROM student

当我们对被索引覆盖的列进行去重查询时,结果会有很大不同:EXPLAIN SELECT DISTINCT(name) FROM student

7、Using filesort

当SQL中使用ORDER BY关键字的时候,如果待排序的内容不能由所使用的索引直接完成排序的话,那么mysql有可能就要进行文件排序。例如:EXPLAIN SELECT name FROM student ORDER BY create_time,create_time列未被索引覆盖,所以引发了Using filesort

不能说filesort一定会引发性能问题,但如果这种查询非常频繁,每次在Mysql中进行排序,还是有优化必要的。优化手段一是不使用ORDER,而是在应用程序中完成排序,二是对需要排序的列添加索引,直接利用索引的排序。

例如:EXPLAIN SELECT id FROM student ORDER BY name,因为name列上有索引,就不再出现filesort

8、Index merges

WHERE条件中有多个条件(或者join)涉及到多个字段,它们之间是 AND 或者 OR关系时,就有可能会使用到 index merge 技术。index merge 技术可以理解为:对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union)。

index merge 根据合并算法的不同分成了三种:intersect, union, sort_union

intersect是对多个索引条件扫描得到的结果进行交集运算,这显然是AND查询时才会出现的,例如:EXPLAIN SELECT user_id FROM test_order WHERE user_id=123 AND order_id=5

union则是对多个索引条件扫描得到的结果进行并集运算,也就是OR查询:SELECT * FROM t1 WHERE key1=1 OR key2=2,测试表中没出现该情形,可能是表中数据量太少,使用索引合并算法得不偿失,用一个数据量万级别的表测试,复现了场景。

Index merges看起来效率不错,但也反映出我们索引有不合理之处,将条件中涉及的列建立联合索引,可以很好地优化。

这篇关于Index merges的博文写得不错,值得看看:https://www.cnblogs.com/digdeep/p/4975977.html

其他

  • LooseScan:在 IN 子查询转为 semi-join 时,如果采用的是 LooseScan 执行策略,则会在Extra中提示。
  • FirstMatch(tbl_name):在 IN 子查询转为 semi-join 时,如果采用的是 FirstMatch 执行策略,则会在Extra中提示。
  • Using join buffer:强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。出现该值,应该注意,根据查询的具体情况可能需要添加索引来改进性能。

2 慢查询

想要优化SQL,找出效率低下的SQL是第一步,在这方面慢查询日志是有力的工具。

首先,通过一条语句查看当前数据库慢查询日志的情况:

SHOW VARIABLES LIKE '%slow_query_log%';
  • slow_query_log:慢查询开启状态,ON为开启,OFF为关闭
  • slow_query_log_file:慢查询日志存放的位置

查询到慢查询日志的状态后,可以使用命令进行修改(这种方式修改,Mysql服务器重启后会失效):

  • set global slow_query_log=on;:打开慢查询日志
  • set global long_query_time=1;:设置记录查询超过多长时间的SQL
  • set global slow_query_log_file='/tmp/slow_query.log';:设置mysql慢查询日志路径,此路径需要有写权限
  • set global log_queries_not_using_indexes=ON;:设置没有使用索引的SQL记录下来

如果想要设置永久生效,我们可以修改配置文件my.cnf(可以通过find命令查找,一般是/etc/my.cnf),找到[mysqld],写入:

# 设置慢查询开启状态
slow_query_log =1
# 慢查询日志存放的位置
slow_query_log_file=/application/mysql/data/localhost-slow.log
# 询超过多少秒才记录   默认10秒 修改为1秒
long_query_time = 1

为了查看效果,我们使用select sleep(2);语句人为制造一个慢查询,然后到慢查询日志中查看相关内容:

/opt/lampp/sbin/mysqld, Version: 3.5.16-log (Source distribution). started with:
Tcp port: 3306  Unix socket: /opt/lampp/var/mysql/mysql.sock
Time   Id   Command    Argument
# Time: 180107 14:53:11
# User@Host: root[root] @ localhost []  Id: 1
# Query_time: 2.000754 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1515307991;
select sleep(2);

3 演示所用数据

测试用表和数据奉上:

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
  `id` int(11) NOT NULL,
  `name` varchar(25) DEFAULT NULL,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `c_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES ('1', '赵日天', '2020-07-28 20:22:40', '2020-07-28 20:22:40');
INSERT INTO `student` VALUES ('2', '叶良辰', '2020-07-28 20:23:29', '2020-07-28 20:23:29');
INSERT INTO `student` VALUES ('3', '龙傲天', '2020-07-28 20:24:13', '2020-07-28 20:24:13');
INSERT INTO `student` VALUES ('4', '徐胜虎', '2020-07-28 20:24:24', '2020-07-28 20:24:24');

-- ----------------------------
-- Table structure for class
-- ----------------------------
DROP TABLE IF EXISTS `class`;
CREATE TABLE `class` (
  `id` int(11) NOT NULL,
  `name` varchar(25) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `s_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of class
-- ----------------------------
INSERT INTO `class` VALUES ('1', '三年1班');
INSERT INTO `class` VALUES ('2', '三年2班');
INSERT INTO `class` VALUES ('3', '三年3班');

-- ----------------------------
-- Table structure for class_student
-- ----------------------------
DROP TABLE IF EXISTS `class_student`;
CREATE TABLE `class_student` (
  `id` int(11) NOT NULL,
  `s_id` int(11) NOT NULL,
  `c_id` int(11) NOT NULL,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `index_c_id` (`c_id`) USING BTREE,
  KEY `index_s_id` (`s_id`) USING BTREE,
  KEY `index_s_c_id` (`s_id`,`c_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of class_student
-- ----------------------------
INSERT INTO `class_student` VALUES ('1', '1', '1', '2020-08-03 16:53:02');
INSERT INTO `class_student` VALUES ('2', '2', '2', '2020-08-03 16:53:02');
INSERT INTO `class_student` VALUES ('3', '3', '2', '2020-08-03 16:53:02');