MySQL

52-MySQL-性能分析-EXPLAIN

2022-10-25  本文已影响0人  紫荆秋雪_文

一、概述

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

1.1、EXPLAIN输出信息

1.2、官网介绍

MySQL-5.7

MySQL-8.0

二、基本语法

EXPLAIN SELECT select_options
或
DESCRIBE SELECT select_options
EXPLAIN SELECT 1;
EXPLAIN输出内容.png

三、数据准备

3.1、建表

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;
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、创建存储过程

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 ;
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、调用存储过程

CALL insert_s1(10001,10000);
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;

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 值也是没错的,但是还存在一些特别的东西

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 子句的查询语句

EXPLAIN
SELECT *
FROM s1
UNION ALL
SELECT *
FROM s2;
含有 UNION ALL 子句.png

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

4.2.7、小结

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 类型

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

4.3.2、PRIMARY

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

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

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

4.3.6、DEPENDENT SUBQUERY

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

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

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

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

-- 创建分区表,
-- 按照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 );
DESC SELECT * FROM user_partitions WHERE id>200;
image.png

4.5、type

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

4.5.1、system

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

 CREATE TABLE t(i int) Engine=MyISAM;
INSERT INTO t VALUES(1);
EXPLAIN SELECT * FROM t;
只有1条记录.png 有多条记录.png

4.5.2、const

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

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

4.5.3、eq_ref

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

EXPLAIN
SELECT *
FROM s1
         INNER JOIN s2 ON s1.id = s2.id;
eq_ref.png

4.5.3、ref

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

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这三种索引合并的方式来执行查询。

EXPLAIN
SELECT *
FROM s1
WHERE key1 = 'a'
   OR key3 = 'a';
index_merge.png

4.5.7、unique_subquery

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

EXPLAIN
SELECT *
FROM s1
WHERE key2 IN (SELECT id FROM s2 WHERE s1.key1 = s2.key1)
   OR key3 = 'a';
unique_subquery.png

4.5.8、index_subquery

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

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 访问方法

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

EXPLAIN
SELECT key_part2
FROM s1
WHERE key_part3 = 'a';
index.png

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

4.5.10、ALL

全表扫描

EXPLAIN SELECT * FROM s1;
ALL.png

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,则没有使用索引

 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

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

EXPLAIN SELECT * FROM s1 WHERE key2 = 10126;
image.png

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
EXPLAIN SELECT * FROM s1 WHERE key_part1 = 'a' AND key_part2 = 'b';
image.png
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常量

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

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

4.9、rows

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

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

4.10、 filtered

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

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)

EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1 WHERE s1.common_field = 'a';
image.png
上一篇 下一篇

猜你喜欢

热点阅读