一、概述
定位了慢查询的 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;
输出的上述信息就是所谓的
执行计划
。在这个执行计划的辅助下,可以知道应该改进自己的查询语句以使查询执行起来更高效。除了用在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;
4.2、id
查询语句一般都以
SELECT
关键字开头,比较简单的查询语句里只有一个SELECT
关键字
4.2.1-实例1:只有一个 SELECT
关键字
EXPLAIN
SELECT *
FROM s1
WHERE key1 = 'a';
4.2.2-实例2:稍微复杂一点的连接查询中也只有一个 SELECT 关键字
对于连接查询来说,一个
SELECT
关键字后边FROM
子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id
值都是相同的
EXPLAIN
SELECT *
FROM s1
INNER JOIN s2;
- 结论:上述连接查询中参与连接的
s1
和s2
表分别对应一条记录,但是这两条记录对应的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';
- 结论
从输出结果中可以看到,
s1表
在外层查询中,外层查询有一个独立的SELECT
关键字,所以第一条记录的id
值就是1
,s2表
在子查询中,子查询有一个独立的SELECT
关键字,所以第二条记录的id
值就是2
。需要特别注意,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询
。所以如果想知道查询优化器对某个包含子查询的语句是否进行了重写
,直接查看执行计划就好
4.2.4、实例4-优化器
对某个包含子查询
的语句是否进行了重写
EXPLAIN
SELECT *
FROM s1
WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.common_field = 'a');
- 小结
可以看到,虽然查询语句是一个子查询,但是执行计划中
s1
和s2
表对应的记录的id
值全部是1
,这就表明了查询优化器将 子查询 转换为了 连接查询
4.2.5、实例5-含有 UNION
子句的查询语句
对于包含
UNION
子句的查询语句来说,每个SELECT
关键字对应一个id
值也是没错的,但是还存在一些特别的东西
- SQL
EXPLAIN
SELECT *
FROM s1
UNION
SELECT *
FROM s2;
- 小结
从分析结果可知,有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
对比起来,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属性
,就知道了这个小查询在整个大查询中扮演了一个什么角色
。
4.3.1、SIMPLE
查询语句中不包含
UNION 或者 子查询
的查询都算是SIMPLE
类型
- SQL
EXPLAIN
SELECT *
FROM s1;
- 连接查询也算是
SIMPLE
类型
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
4.3.2、PRIMARY
对于包含
UNION、UNION ALL 或者 子查询
的大查询来说,它是由几个小查询组成的,其中最左边
的那个查询的select_type
值就是PRIMARY
- SQL
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
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';
- 分析:外层查询的
select_type
就是PRIMARY
,子查询的select_type
就是SUBQUERY
。需要注意的是,由于select_type
为SUBQUERY
的子查询会被物化
,所以只需要执行一遍
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';
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');
- 分析
这个查询比较复杂,大查询里包含了一个子查询,子查询里又是由
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;
- 分析
从执行计划中可以看出,id位 2 的记录就代表子查询的执行方式,它的
select_type
是DERIVED
,说明该子查询是以物化
的方式执行的。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;
- 查询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
当表中
只有一条记录
并且该表使用的存储引擎的统计数据是精确的,比如MyISAM
、Memory
,那么对该表的访问方法就是system
- 创建 MyISAM 表
CREATE TABLE t(i int) Engine=MyISAM;
- 插入数据
INSERT INTO t VALUES(1);
- EXPLAIN
EXPLAIN SELECT * FROM t;
4.5.2、const
当根据
主键
或者唯一二级索引列
与常数进行等值匹配时,对单表的访问方法就是const
- SQL
EXPLAIN SELECT * FROM s1 WHERE id = 10005;
4.5.3、eq_ref
在连接查询时,如果
被驱动表
是通过主键或者唯一二级索引列
等值匹配的方式进行访问的(如果该主键
或者唯一二级索引
是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表
的访问方法就是eq_ref
- SQL
EXPLAIN
SELECT *
FROM s1
INNER JOIN s2 ON s1.id = s2.id;
- 从执行计划的结果可知,MySQL将
s2作为驱动表
,s1作为被驱动表
,重点关注s1
的访问方法是eq_ref
,表明在访问s1表
的时候可以通过主键的等值匹配
来进行访问
4.5.3、ref
当通过普通的
二级索引列
与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref
- SQL
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
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;
4.5.6、index_merge
一般情况下对于某个表的查询只能使用到一个索引,但单表访问方法时在某些场景下可以使用
Intersection
、UNION
、Sort-Union
这三种索引合并的方式来执行查询。
- SQL
EXPLAIN
SELECT *
FROM s1
WHERE key1 = 'a'
OR key3 = 'a';
- 从执行计划的 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';
- 从执行计划的第二条记录的
type
值就是unique_subquery
,说明在执行子查询时会使用到id
列的索引
4.5.8、index_subquery
index_subquery
与unique_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';
4.5.10、index
当使用
索引覆盖
,但需要扫描全部
的索引记录时,该表的访问方法就是index
- SQL
EXPLAIN
SELECT key_part2
FROM s1
WHERE key_part3 = 'a';
- 上述查询中的搜索列表中只有
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
这个访问方法外,其余的访问方法都能用到索引,除了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';
4.7、key_len
实际使用到的索引长度(字节数)。帮我们检查
是否充分的利用上连索引
,值越大越好
,主要针对于联合索引
,有一定的参考意义
- 使用主键
主键INT占用 4 个字节
EXPLAIN SELECT * FROM s1 WHERE id = 10005;
- 使用
INT
类型,并且可以为NULL
key2是
INT
类型,并且可以为null (4 + 1)
EXPLAIN SELECT * FROM s1 WHERE key2 = 10126;
- 使用
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';
- 联合索引, 使用
varchar(100)
类型,并且可以为NULL
EXPLAIN SELECT * FROM s1 WHERE key_part1 = 'a' AND key_part2 = 'b';
- 小结: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
显示索引的哪一列被使用了,如果可能的话,是一个常数
当使用索引列等值匹配的条件去执行查询时,也就是在访问方法是const
、eq_ref
、ref
、ref_or_null
、unique_subquery
、index_subquery
其中之一时,ref
列展示的就是与索引列作等值匹配的结构是什么
4.8.1、const常量
- SQL
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
- 分析
ref列的值是
const
,表明在使用idx_key1
索引执行查询时,与key1
列作等值匹配的对象是一个常数
4.9、rows
预估的需要读取的记录条数
- SQL
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z';
4.10、 filtered
某个表经过搜索条件过滤后剩余记录调试的百分比。
如果使用的是索引执行的单表扫描
,那么计算时需要估计出满足除使用到对应所以得搜索条件外的其他搜索条件的记录有多少条
- SQL
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND common_field = 'a';
- 分析
增加了
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';