52-MySQL-性能分析-EXPLAIN

一、概述

定位了慢查询的 SQL 之后,就可以使用 EXPLAIN 或 DESCRIBE(DESC) 工具做针对性的分析查询语句
MySQL有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的 Query 提供它认为最优的 执行计划(系统认为最优的数据检索方式,不见得是DBA任务的最优解)
这个执行计划展示了具体执行查询的方式,如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MySQL提供了 EXPLAIN 语句来帮助查看某个查询语句的具体执行计划。可以通过 EXPLAIN 语句的各个输出项来有针对性的提升查询语句的性能

1.1、EXPLAIN输出信息

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少航被优化器查询

1.2、官网介绍

MySQL-5.7

MySQL-8.0

  • MySQL 5.6.3以前只能 EXPLAIN SELECT ;MYSQL 5.6.3以后就可以 EXPLAIN SELECT,UPDATE, DELETE
  • 在5.7以前的版本中,想要显示 partitions 需要使用 explain partitions 命令;想要显示filtered 需要使用 explain extended 命令。在5.7版本后,默认explain直接显示partitions和 filtered中的信息

二、基本语法

  • EXPLAIN 或 DESCRIBE语句的语法形式如下
EXPLAIN SELECT select_options
或
DESCRIBE SELECT select_options
  • 查看 EXPLAIN的字段含义
EXPLAIN SELECT 1;
EXPLAIN输出内容.png
  • 输出的上述信息就是所谓的 执行计划 。在这个执行计划的辅助下,可以知道应该改进自己的查询语句以使查询执行起来更高效。除了用在SELECT的查询语句,也可以加在UPDATE、INSERT、REPLACE、DELETE语句上

  • EXPLAIN 语句输出的各个列的作用如下
    EXPLAIN 语句输出的各个列的作用.png

三、数据准备

3.1、建表

  • 创建表 s1
CREATE TABLE s1
(
    id           INT AUTO_INCREMENT,
    key1         VARCHAR(100),
    key2         INT,
    key3         VARCHAR(100),
    key_part1    VARCHAR(100),
    key_part2    VARCHAR(100),
    key_part3    VARCHAR(100),
    common_field VARCHAR(100),
    PRIMARY KEY (id),
    INDEX idx_key1 (key1),
    UNIQUE INDEX idx_key2 (key2),
    INDEX idx_key3 (key3),
    INDEX idx_key_part (key_part1, key_part2, key_part3)
) ENGINE=INNODB CHARSET=utf8;
  • 创建表 s2
CREATE TABLE s2
(
    id           INT AUTO_INCREMENT,
    key1         VARCHAR(100),
    key2         INT,
    key3         VARCHAR(100),
    key_part1    VARCHAR(100),
    key_part2    VARCHAR(100),
    key_part3    VARCHAR(100),
    common_field VARCHAR(100),
    PRIMARY KEY (id),
    INDEX idx_key1 (key1),
    UNIQUE INDEX idx_key2 (key2),
    INDEX idx_key3 (key3),
    INDEX idx_key_part (key_part1, key_part2, key_part3)
) ENGINE = INNODB
  CHARSET = utf8;

3.2、设置参数 log_bin_trust_function_creators

  • 创建函数,假如报错,需开启如下命令:允许创建函数设置
# 不加global只是当前窗口有效。
set global log_bin_trust_function_creators=1; 

3.3、创建函数

DELIMITER & CREATE FUNCTION rand_string1(n INT)
    RETURNS VARCHAR(255) #该函数会返回一个字符串
BEGIN
    DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
    DECLARE return_str VARCHAR(255) DEFAULT '';
    DECLARE i INT DEFAULT 0;
    WHILE i < n
        DO
            SET return_str = CONCAT(return_str, SUBSTRING(chars_str, FLOOR(1 + RAND() * 52), 1));
            SET i = i + 1;
        END WHILE;
    RETURN return_str;
END & DELIMITER ;

3.4、创建存储过程

  • 创建往s1表中插入数据的存储过程
DELIMITER &
CREATE PROCEDURE insert_s1(IN min_num INT(10), IN max_num INT(10))
BEGIN
    DECLARE i INT DEFAULT 0;
    SET autocommit = 0;
    REPEAT
        SET i = i + 1;
        INSERT INTO s1
        VALUES ((min_num + i), rand_string1(6), (min_num + 30 * i + 5), rand_string1(6), rand_string1(10),
                rand_string1(5),
                rand_string1(10), rand_string1(10));
    UNTIL i = max_num END REPEAT;
    COMMIT;
END &
DELIMITER ;
  • 创建往s2表中插入数据的存储过程
DELIMITER $ CREATE PROCEDURE insert_s2(IN min_num INT(10), IN max_num INT(10))
BEGIN
    DECLARE i INT DEFAULT 0; SET autocommit = 0;
    REPEAT
        SET i = i + 1;
        INSERT INTO s2
        VALUES ((min_num + i), rand_string1(6), (min_num + 30 * i + 5), rand_string1(6), rand_string1(10),
                rand_string1(5), rand_string1(10), rand_string1(10));
    UNTIL i = max_num END REPEAT;
    COMMIT;
END $
DELIMITER ;

3.5、调用存储过程

  • s1表数据的添加:加入1万条记录
CALL insert_s1(10001,10000);
  • s2表数据的添加:加入1万条记录
CALL insert_s2(10001,10000);

四、EXPLAIN各列作用

4.1、table

不论 SQL 语句有多复杂,里边儿 包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的,所以MySQL规定 EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的 table 列代表着该表的表名(有时不是真实的表名字,可能是简称)

4.1.1、实例1-单表查询

EXPLAIN
SELECT *
FROM s1;
image.png

4.2、id

查询语句一般都以 SELECT 关键字开头,比较简单的查询语句里只有一个 SELECT 关键字

4.2.1-实例1:只有一个 SELECT 关键字

EXPLAIN
SELECT *
FROM s1
WHERE key1 = 'a';
1个id.png

4.2.2-实例2:稍微复杂一点的连接查询中也只有一个 SELECT 关键字

对于连接查询来说,一个SELECT关键字后边FROM子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的 id 值都是相同的

EXPLAIN
SELECT *
FROM s1
INNER JOIN s2;
  • 结论:上述连接查询中参与连接的 s1s2 表分别对应一条记录,但是这两条记录对应的 id 值都是 1。在连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的,出现在前边的表是驱动表,出现在后边的表是被驱动表。所以从上边的 EXPLAIN 输出中可以看出,查询优化器准备让 s2表作为驱动表,让s1表作为被驱动表来执行查询

4.2.3、实例3-包含子查询

对于包含子查询的查询语句来说,就可能涉及多个SELECT关键字,所以在包含子查询的查询语句中的执行计划中,每个SELECT关键字都会对应一个唯一的 id 值

EXPLAIN
SELECT *
FROM s1
WHERE key1 IN (SELECT key1 FROM s2)
   OR key3 = 'a';

image.png
  • 结论

从输出结果中可以看到,s1表在外层查询中,外层查询有一个独立的 SELECT 关键字,所以第一条记录的id值就是 1s2表在子查询中,子查询有一个 独立的SELECT关键字,所以第二条记录的 id值就是 2。需要特别注意,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。所以如果想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划就好

4.2.4、实例4-优化器对某个包含子查询的语句是否进行了重写

EXPLAIN
SELECT *
FROM s1
WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.common_field = 'a');
优化器重写.png
  • 小结

可以看到,虽然查询语句是一个子查询,但是执行计划中s1s2表对应的记录的id值全部是1,这就表明了查询优化器将 子查询 转换为了 连接查询

4.2.5、实例5-含有 UNION 子句的查询语句

对于包含 UNION 子句的查询语句来说,每个 SELECT 关键字对应一个 id 值也是没错的,但是还存在一些特别的东西

  • SQL
EXPLAIN
SELECT *
FROM s1
UNION
SELECT *
FROM s2;
含UNION子句.png
  • 小结

从分析结果可知,有3条记录。 UNION 它会把多个查询的结果集合并起来并对结果集中的记录 进行去重,怎么去重?MySQL 使用的是内部的 临时表。正如上边的查询计划中所示, UNION 子句是为了把 id 为 1 的查询和 id 为 2 的查询的结果集合并起来并去重,所以在内部创建了一个名为 <union1,2> 的临时表(就是执行计划第三条记录的 table 列的名称)

4.2.6、实例6-含有 UNION ALL 子句的查询语句

  • SQL
EXPLAIN
SELECT *
FROM s1
UNION ALL
SELECT *
FROM s2;
含有 UNION ALL 子句.png
  • 小结

UNION 对比起来,UNION ALL就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表

4.2.7、小结

  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • 关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好

4.3、select_type

一条大的查询语句里边可以包含若干个 SELECT 关键字,每个 SELECT 关键字代表着一个小的查询语句,而每个 SELECT 关键字的 FROM 子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个 SELECT 关键字中的表来说,它们的id值是相同的。
MySQL为每一个 SELECT 关键字代表的小查询都定义了一个称为 select_type 的属性,意思是只要知道了某个小查询的 select_type属性,就知道了这个小查询在整个大查询中扮演了一个什么角色

select_type.png

4.3.1、SIMPLE

查询语句中不包含 UNION 或者 子查询的查询都算是 SIMPLE 类型

  • SQL
EXPLAIN
SELECT *
FROM s1;
SIMPLE 类型.png
  • 连接查询也算是 SIMPLE 类型
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
连接查询也算是 `SIMPLE` 类型.png

4.3.2、PRIMARY

对于包含 UNION、UNION ALL 或者 子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type 值就是 PRIMARY

  • SQL
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
PRIMARY-UNION.png

4.3.3、UNION

对于包含 UNION 或者 UNION ALL的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的小查询的select_type值就是UNION

4.3.4、UNION RESULT

MySQL 选择使用临时表来完成 UNION 查询的去重工作,针对该临时表的查询的 select_type值就是UNION RESULT

4.3.5、SUBQUERY

如果包含子查询的查询语句不能够转为对应的 semi-join 的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的 第一个 SELECT 关键字代表的那个查询的 select_type值就是SUBQUERY

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE key1 IN (SELECT key1 FROM s2)
   OR key3 = 'a';
SUBQUERY.png
  • 分析:外层查询的 select_type就是PRIMARY,子查询的select_type就是SUBQUERY。需要注意的是,由于select_typeSUBQUERY的子查询会被物化,所以只需要执行一遍

4.3.6、DEPENDENT SUBQUERY

如果包含子查询的查询语句不能够转为对应的 semi-join 的形式,并且该子查询是相关子查询,则该子查询的 第一个 SELECT 关键字代表的那个查询的 select_type值就是DEPENDENT SUBQUERY

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2)
   OR key3 = 'a';
DEPENDENT SUBQUERY.png

4.3.7、DEPENDENT UNION

在包含 UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话,那么除了最左边的那个小查询之外,其余的小查询的 select_type值就是DEPENDENT UNION

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b');
DEPENDENT UNION.png
  • 分析

这个查询比较复杂,大查询里包含了一个子查询,子查询里又是由 UNION 连起来的两个小查询。从执行计划中可以看出来,SELECT key1 FROM s2 WHERE key1 = 'a'这个小查询由于是子查询中第一个查询,所以它的 select_type值就是DEPENDENT SUBQUERY,而 SELECT key1 FROM s1 WHERE key1 = 'b'这个查询的select_type值就是DEPENDENT UNION

4.3.8、DERIVED

对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的select_type就是DERIVED

  • SQL
EXPLAIN
SELECT *
FROM (SELECT key1, COUNT(*) AS c FROM s1 GROUP BY key1) AS derived_s1
WHERE c > 1;
DERIVED.png
  • 分析

从执行计划中可以看出,id位 2 的记录就代表子查询的执行方式,它的select_typeDERIVED,说明该子查询是以物化的方式执行的。id为1的记录代表外层查询,table列显示的是<derived2>,表示该查询时针对将派生表物化之后的表进行查询的

4.3.9、MATERIALIZED

当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,该子查询对应的select_type属性就是MATERIALIZED

4.3.10、UNCACHEABLE SUBQUERY

4.3.11、UNCACHEABLE UNION

4.4、partitions

代表分区表中的命中情况,非分区表,该项为NULL。一般情况下的查询语句的执行计划的 partitions 的值都是 NULL

  • SQL
-- 创建分区表,
-- 按照id分区,id<100 p0分区,其他p1分区
CREATE TABLE user_partitions
(
    id   INT AUTO_INCREMENT,
    name VARCHAR(12),
    PRIMARY KEY (id)
) PARTITION BY RANGE (id)( PARTITION p0 VALUES LESS THAN (100), PARTITION p1 VALUES LESS THAN MAXVALUE );
  • SQL
DESC SELECT * FROM user_partitions WHERE id>200;
image.png
  • 查询id大于200(200>100,p1分区)的记录,查看执行计划,partitions是p1,符合我们的分区规则

4.5、type

执行计划的一条记录就代表着 MySQL 对某个表的 执行查询时的访问方法,又称访问类型,其中的 type 列就表明了这个访问方法是什么,是较为重要的一个指标。

  • 完整的访问方法如下: system , const , eq_ref , ref , fulltext , ref_or_null , index_merge , unique_subquery , index_subquery , range , index , ALL

4.5.1、system

当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAMMemory,那么对该表的访问方法就是system

  • 创建 MyISAM 表
 CREATE TABLE t(i int) Engine=MyISAM;
  • 插入数据
INSERT INTO t VALUES(1);
  • EXPLAIN
EXPLAIN SELECT * FROM t;
只有1条记录.png
有多条记录.png

4.5.2、const

当根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是 const

  • SQL
EXPLAIN SELECT * FROM s1 WHERE id = 10005;
const.png

4.5.3、eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref

  • SQL
EXPLAIN
SELECT *
FROM s1
         INNER JOIN s2 ON s1.id = s2.id;
eq_ref.png
  • 从执行计划的结果可知,MySQL将s2作为驱动表s1作为被驱动表,重点关注s1的访问方法是eq_ref,表明在访问s1表的时候可以通过主键的等值匹配来进行访问

4.5.3、ref

当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref

  • SQL
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
ref.png

4.5.4、fulltext:全文索引

4.5.5、ref_or_null

当对普通二级索引进行等值匹配查询,该索引列的值也可以为 NULL 值时,那么对该表的访问方法就可能是 ref_or_null

EXPLAIN
SELECT *
FROM s1
WHERE key1 = 'a'
   OR key1 IS NULL;
ref_or_null.png

4.5.6、index_merge

一般情况下对于某个表的查询只能使用到一个索引,但单表访问方法时在某些场景下可以使用IntersectionUNIONSort-Union这三种索引合并的方式来执行查询。

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE key1 = 'a'
   OR key3 = 'a';
index_merge.png
  • 从执行计划的 type 列的值是 index_merge 就可以看出,MySQL 打算使用索引合并的方式来执行对 s1 表的查询

4.5.7、unique_subquery

类似于两表连接中被驱动表的 eq_ref访问方法,unique_subquery是针对在一些包含 IN 子查询的查询语句中,如果查询优化器决定将 IN 子查询转换为 EXISTS 子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的 type 列的值就是 unique_subquery

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE key2 IN (SELECT id FROM s2 WHERE s1.key1 = s2.key1)
   OR key3 = 'a';
unique_subquery.png
  • 从执行计划的第二条记录的 type 值就是unique_subquery,说明在执行子查询时会使用到id列的索引

4.5.8、index_subquery

index_subqueryunique_subquery类似,只不过访问子查询中的表时使用的是普通的索引

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE common_field IN (SELECT key3 FROM s2 WHERE s1.key1 = s2.key1)
   OR key3 = 'a';

4.5.9、range

如果使用索引获取某些 范围区间 的记录,那么就可能使用到 range 访问方法

  • SQL
EXPLAIN
SELECT *
FROM s1
WHERE key1 IN ('a', 'b', 'c');

或

EXPLAIN
SELECT *
FROM s1
WHERE key1 > 'a'
  AND key1 < 'b';
range.png

4.5.10、index

当使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是 index

  • SQL
EXPLAIN
SELECT key_part2
FROM s1
WHERE key_part3 = 'a';
index.png
  • 上述查询中的搜索列表中只有key_part2一个列,而且搜索条件也只有key_part3一个列,这两个列又恰好包含在idx_key_part这个索引中,可是搜索条件key_part3不能直接使用该索引进行ref或者range方式的访问,只能扫描整个idx_key_part索引的记录,所以查询计划的type列的值就是index

对于使用InnoDB存储引擎的表来说,二级索引的记录只包含索引列主键列的值,而 聚簇索引中包含用户定义的全部列以及一些隐藏列,所以扫描 二级索引的代价比直接扫描全表,也就是扫描聚簇索引的代价更低一些

4.5.10、ALL

全表扫描

  • SQL
EXPLAIN SELECT * FROM s1;
ALL.png
  • 一般来说,这些访问方法中除了ALL这个访问方法外,其余的访问方法都能用到索引,除了index_merge访问方法外,其余的访问方法都最多只能用到一个索引

4.5.11、小结

结果值从最好到最坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。(阿里巴巴开发手册要求)

4.6、possible_keys和key

possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上若存在索引,则该索引将列出,但不一定被查询使用。
key列表示实际使用到的索引有哪些,如果为NULL,则没有使用索引

  • SQL
 EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key3 = 'a';
image.png

4.7、key_len

实际使用到的索引长度(字节数)。帮我们检查是否充分的利用上连索引值越大越好,主要针对于联合索引,有一定的参考意义

  • 使用主键

主键INT占用 4 个字节

EXPLAIN SELECT * FROM s1 WHERE id = 10005;
image.png
  • 使用 INT 类型,并且可以为NULL

key2是INT类型,并且可以为null (4 + 1)

EXPLAIN SELECT * FROM s1 WHERE key2 = 10126;
image.png
  • 使用 varchar(100) 类型,并且可以为NULL

s1表字符集为CHARSET=utf8mb4,所以 varchar(100) 占用 400 个字节,可以为NULL占用1字节,由于varchar是变长数据类型,所以占用 2 字节。 varchar(100)类型的key1占用key_len = 403(400+1+2)

EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
image.png
  • 联合索引, 使用 varchar(100) 类型,并且可以为NULL
EXPLAIN SELECT * FROM s1 WHERE key_part1 = 'a' AND key_part2 = 'b';
image.png
  • 小结:key_len的长度计算公式
varchar(10)变长字段且允许NULL = 10 * ( character set: utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)

varchar(10)变长字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)

char(10)固定字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)

char(10)固定字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)

4.8、ref

显示索引的哪一列被使用了,如果可能的话,是一个常数
当使用索引列等值匹配的条件去执行查询时,也就是在访问方法是consteq_refrefref_or_nullunique_subqueryindex_subquery其中之一时,ref列展示的就是与索引列作等值匹配的结构是什么

4.8.1、const常量

  • SQL
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
ref=const.png
  • 分析

ref列的值是const,表明在使用idx_key1索引执行查询时,与key1列作等值匹配的对象是一个常数

4.9、rows

预估的需要读取的记录条数

  • SQL
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z';
image.png

4.10、 filtered

某个表经过搜索条件过滤后剩余记录调试的百分比。
如果使用的是索引执行的单表扫描,那么计算时需要估计出满足除使用到对应所以得搜索条件外的其他搜索条件的记录有多少条

  • SQL
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND common_field = 'a';
添加 common_field = a条件.png
  • 分析

增加了common_field = 'a'搜索条件后,filtered为 10

4.10.1、连接查询

对于单表查询来说,这个 filtered 列的值没有什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的 filtered 的值,它决定了被驱动表要执行的次数(即:rows * filtered)

  • SQL
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1 WHERE s1.common_field = 'a';
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容