一文读懂 MySQL Explain 执行计划

Posted 老周聊架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文读懂 MySQL Explain 执行计划相关的知识,希望对你有一定的参考价值。

一、前言

上周老周的一个好朋友让我出一篇教你读懂 SQL 执行计划,和我另一位读者反馈的面试题如何排查慢 SQL 的强相关,索性先出一篇一文读懂 mysql Explain 执行计划。Explain 执行计划你一定得会看,不然你简历上就别去写什么你会 SQL 调优,不然面试官会觉得,Explain 执行计划你都不会看,那你还 SQL 调啥优啊?SQL 调你吧???开个小玩笑,玩笑归玩笑,重要是真的重要!!!

二、Explain 执行计划是什么?

什么是执行计划?简而言之,就是 SQL 在数据库中执行时的表现情况,通常用于 SQL 性能分析、优化和加锁分析等场景,执行过程会在 MySQL 查询过程中由解析器,预处理器和查询优化器共同生成。在 MySQL 中使用 explain 关键字来查看。

2.1 执行计划有什么用?

它可以用来分析 SQL 语句和表结构的性能瓶颈

  • 关联查询的执行顺序
  • 查询操作的操作类型
  • 哪些索引可以被命中
  • 哪些索引实际被命中
  • 每张表有多少记录参与查询

2.2 MySQL 执行过程

如上图所示,MySQL 数据库由 Server 层和 Engine 层组成:

  • Server 层有 SQL 分析器、SQL优化器、SQL 执行器,用于负责 SQL 语句的具体执行过程;
  • Engine 层负责存储具体的数据,如最常使用的 MyISAM、InnoDB 存储引擎,还有用于在内存中存储临时结果集的 TempTable 引擎。

SQL 优化器会分析所有可能的执行计划,选择成本最低的执行,这种优化器称之为:CBO(Cost-based Optimizer,基于成本的优化器)。

而在 MySQL 中,一条 SQL 的计算成本计算如下所示:

Cost = Server Cost + Engine Cost
= CPU Cost + IO Cost

其中,CPU Cost 表示计算的开销,比如索引键值的比较、记录值的比较、结果集的排序等这些操作都在 Server 层完成;

IO Cost 表示引擎层 IO 的开销,MySQL 8.0 可以通过区分一张表的数据是否在内存中,分别计算读取内存 IO 开销以及读取磁盘 IO 的开销。

数据库 mysql 下的表 server_cost、engine_cost 则记录了对于各种成本的计算,如:


表 server_cost 记录了 Server 层优化器各种操作的成本,这里面包括了所有 CPU Cost,其具体含义如下:

  • disk_temptable_create_cost:创建磁盘临时表的成本,默认为 20
  • disk_temptable_row_cost:磁盘临时表中每条记录的成本,默认为 0.5
  • key_compare_cost:索引键值比较的成本,默认为 0.05,成本最小。
  • memory_temptable_create_cost:创建内存临时表的成本:默认为 1
  • memory_temptable_row_cost:内存临时表中每条记录的成本,默认为 0.1
  • row_evaluate_cost:记录间的比较成本,默认为 0.1

可以看到, MySQL 优化器认为如果一条 SQL 需要创建基于磁盘的临时表,则这时的成本是最大的,其成本是基于内存临时表的 20 倍。而索引键值的比较、记录之间的比较,其实开销是非常低的,但如果要比较的记录数非常多,则成本会变得非常大。

而表 engine_cost 记录了存储引擎层各种操作的成本,这里包含了所有的 IO Cost,具体含义如下:

  • io_block_read_cost:从磁盘读取一个页的成本,默认值为 1
  • memory_block_read_cost:从内存读取一个页的成本,默认值为 0.25

也就是说, MySQL 优化器认为从磁盘读取的开销是内存开销的 4 倍。

三、Explain 执行计划详解

我们先来准备以下 SQL 脚本:

CREATE TABLE `user` (
	`id` INT (11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR (20) DEFAULT NULL COMMENT "用户名",
	PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = "用户表";

CREATE TABLE `user_robot_relate` (
	`id` INT (11) NOT NULL AUTO_INCREMENT,
	`user_id` INT (11) NOT NULL COMMENT "用户id",
	`robot_id` INT (11) NOT NULL COMMENT "机器人id",
	PRIMARY KEY (`id`), 
	KEY `idx_user_id` (`user_id`), 
	KEY `idx_robot_id` (`robot_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = "用户与机器人表";

CREATE TABLE `robot` (
	`id` INT (11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR ( 20 ) DEFAULT NULL COMMENT "机器人名",
	PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = "机器人表";

INSERT INTO user VALUES (1, 'riemann');
INSERT INTO user VALUES (2, 'andy');

INSERT INTO user_robot_relate VALUES (1, 1, 1);
INSERT INTO user_robot_relate VALUES (2, 1, 2);
INSERT INTO user_robot_relate VALUES (3, 2, 3);

INSERT INTO robot VALUES (1, '小白鲸');
INSERT INTO robot VALUES (2, '扫地机');
INSERT INTO robot VALUES (3, '扫拖一体机');

我们创建三张表 user、user_robot_relate、robot,表之间的关系 user.id = user_robot_relate.user_id AND user_robot_relate.robot_id = robot.id。

先来看下我的 MySQL 版本,是 5.7.37 的。

接着我们看一下执行计划有哪些字段,先看个整体的,让大家有个大概的认识后,我们再逐一去详解分析。


explain 执行后输出的结果集包含 12 列,分别是 id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered 和 Extra,下面对这些字段进行解释。

  • id:Query Optimizer 所选定的执行计划中查询的序列号
  • select_type:显示本行是简单或复杂 select。如果查询有任何复杂的子查询,则最外层标记为PRIMARY、DERIVED.、UNION、UNION RESUIT 等。
  • table:显示这一步所访问的数据库中的表的名称
  • partitions:查询时匹配到的分区信息,对于非分区表值为 NULL,当查询的是分区表时,partitions 显示分区表命中的分区情况。
  • type:数据访问、读取操作类型(ALL、index、range、ref、eq_ref、const、system)等
  • possible_keys:该查询可以利用的索引,如果没有任何索引可以使用,就会显示成 null,这一
    项内容对于优化时候索引的调整非常重要。
  • key:MySQL Query Optimizer 从 possible_keys 中所选择使用的索引
  • key_len:被选中使用索引的索引键长度
  • ref:列出是通过常量(const),还是某个表的某个字段(如果是 join)来过滤(通过 key)
    的。
  • rows:MySQL Query Optimizer 通过系统收集到的统计信息估算出来的结果集记录条数
  • filtered:表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。
  • Extra:查询中每一步实现的额外细节信息,如 Using filesort、index 等。

3.1 id

看到三条记录的 id 都相同,可以理解成这三个表为一组,具有同样的优先级,执行顺序由上而下,具体顺序由优化器决定。

3.1.1 id 相同

mysql> EXPLAIN SELECT * FROM user u WHERE u.id = (SELECT ur.user_id FROM user_robot_relate ur WHERE ur.robot_id = (SELECT r.id FROM robot r WHERE r.name = '扫地机'));
+----+-------------+-------+------------+------+--------------------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys            | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+-------+------------+------+--------------------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | ur    | NULL       | ALL  | idx_user_id,idx_robot_id | NULL | NULL    | NULL |    3 |   100.00 | NULL                                               |
|  1 | SIMPLE      | u     | NULL       | ALL  | PRIMARY                  | NULL | NULL    | NULL |    2 |    50.00 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | r     | NULL       | ALL  | PRIMARY                  | NULL | NULL    | NULL |    3 |    50.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+--------------------------+------+---------+------+------+----------+----------------------------------------------------+

3.1.2 id 不同

如果我们的 SQL 中存在子查询,那么 id 的序号会递增,id 值越大优先级越高,越先被执行。当三个表依次嵌套,发现最里层的子查询 id 最大,最先执行。

mysql> EXPLAIN SELECT * FROM user u WHERE u.id = (SELECT ur.user_id FROM user_robot_relate ur WHERE ur.robot_id = (SELECT r.id FROM robot r WHERE r.name = '扫地机'));
+----+-------------+-------+------------+-------+---------------+--------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key          | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+--------------+---------+-------+------+----------+-------------+
|  1 | PRIMARY     | u     | NULL       | const | PRIMARY       | PRIMARY      | 4       | const |    1 |   100.00 | NULL        |
|  2 | SUBQUERY    | ur    | NULL       | ref   | idx_robot_id  | idx_robot_id | 4       | const |    1 |   100.00 | Using where |
|  3 | SUBQUERY    | r     | NULL       | ALL   | NULL          | NULL         | NULL    | NULL  |    3 |    33.33 | Using where |
+----+-------------+-------+------------+-------+---------------+--------------+---------+-------+------+----------+-------------+

3.1.3 以上两种同时存在

将上边的 SQL 稍微修改一下,增加一个子查询,发现 id 的以上两种同时存在。相同 id 划分为一组,这样就有三个组,同组的从上往下顺序执行,不同组 id 值越大,优先级越高,越先执行。

mysql> EXPLAIN SELECT * FROM user u WHERE u.id = (SELECT ur.user_id FROM user_robot_relate ur WHERE ur.robot_id = (SELECT r.id FROM robot r WHERE r.name = '扫地机')) AND u.id IN (SELECT u.id FROM user u WHERE u.name = 'riemann');
+----+-------------+-------+------------+-------+---------------+--------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key          | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+--------------+---------+-------+------+----------+-------------+
|  1 | PRIMARY     | u     | NULL       | const | PRIMARY       | PRIMARY      | 4       | const |    1 |   100.00 | NULL        |
|  1 | PRIMARY     | u     | NULL       | const | PRIMARY       | PRIMARY      | 4       | const |    1 |   100.00 | NULL        |
|  2 | SUBQUERY    | ur    | NULL       | ref   | idx_robot_id  | idx_robot_id | 4       | const |    1 |   100.00 | Using where |
|  3 | SUBQUERY    | r     | NULL       | ALL   | NULL          | NULL         | NULL    | NULL  |    3 |    33.33 | Using where |
+----+-------------+-------+------------+-------+---------------+--------------+---------+-------+------+----------+-------------+

3.2 select_type

select_type:表示 select 查询的类型,主要是用于区分各种复杂的查询,例如:普通查询、联合查询、子查询等。

  • SIMPLE:表示最简单的 select 查询语句,也就是在查询中不包含子查询或者 union 交并差集等操作。

  • PRIMARY:当查询语句中包含任何复杂的子部分,最外层查询则被标记为 PRIMARY。

  • SUBQUERY:当 select 或 where 列表中包含了子查询,该子查询被标记为 SUBQUERY。

  • DERIVED:表示包含在 from 子句中的子查询的 select,在我们的 from 列表中包含的子查询会被标记为 derived。

  • UNION:如果 union 后边又出现的 select 语句,则会被标记为 union;若 union 包含在 from 子句的子查询中,外层 select 将被标记为 derived。

  • UNION RESULT:代表从 union 的临时表中读取数据,而 table 列的 <union1,4> 表示用第一个和第四个 select 的结果进行 union 操作。

    mysql> EXPLAIN SELECT t.user_id, (SELECT u.id FROM user u) o FROM (SELECT ur.user_id, ur.robot_id FROM user_robot_relate ur WHERE ur.id = 2) t UNION (SELECT r.id, r.name FROM robot r);
    +----+--------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
    | id | select_type  | table      | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra           |
    +----+--------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
    |  1 | PRIMARY      | ur         | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL            |
    |  2 | SUBQUERY     | u          | NULL       | index | NULL          | PRIMARY | 4       | NULL  |    2 |   100.00 | Using index     |
    |  4 | UNION        | r          | NULL       | ALL   | NULL          | NULL    | NULL    | NULL  |    3 |   100.00 | NULL            |
    | NULL | UNION RESULT | <union1,4> | NULL       | ALL   | NULL          | NULL    | NULL    | NULL  | NULL |     NULL | Using temporary |
    +----+--------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
    

3.3 table

查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表,例如上边的 DERIVED、 <union1,4> 等。

3.4 partitions

查询时匹配到的分区信息,对于非分区表值为 NULL,当查询的是分区表时,partitions 显示分区表命中的分区情况。

3.5 type

type:查询使用了何种类型,它在 SQL优化中是一个非常重要的指标,以下性能从好到坏依次是:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

  • system:当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘 IO,速度非常快。

  • const:表示查询时命中 primary key 主键或者 unique 唯一索引,或者被连接的部分是一个常量(const)值。这类扫描效率极高,返回数据量少,速度非常快。

    mysql> EXPLAIN SELECT * FROM robot WHERE id = 1;
    +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
    | id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
    +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | robot | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
    +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
    
  • eq_ref:查询时命中主键 primary key 或者 unique key 索引, type 就是 eq_ref。

    mysql> EXPLAIN SELECT u.name FROM user u, user_robot_relate ur WHERE u.id = ur.id; 
    +----+-------------+-------+------------+--------+---------------+---------+---------+-----------+------+----------+-------------+
    | id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref       | rows | filtered | Extra       |
    +----+-------------+-------+------------+--------+---------------+---------+---------+-----------+------+----------+-------------+
    |  1 | SIMPLE      | u     | NULL       | ALL    | PRIMARY       | NULL    | NULL    | NULL      |    2 |   100.00 | NULL        |
    |  1 | SIMPLE      | ur    | NULL       | eq_ref | PRIMARY       | PRIMARY | 4       | test.u.id |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+--------+---------------+---------+---------+-----------+------+----------+-------------+
    
  • ref:区别于 eq_ref,ref 表示使用非唯一性索引,会找到很多个符合条件的行。

    mysql> EXPLAIN SELECT id FROM user_robot_relate WHERE user_id = 2; 
    +----+-------------+-------------------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
    | id | select_type | table             | partitions | type | possible_keys | key         | key_len | ref   | rows | filtered | Extra       |
    +----+-------------+-------------------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
    |  1 | SIMPLE      | user_robot_relate | NULL       | ref  | idx_user_id   | idx_user_id | 4       | const |    1 |   100.00 | Using index |
    +----+-------------+-------------------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
    
  • ref_or_null:这种连接类型类似于 ref,区别在于 MySQL 会额外搜索包含 NULL 值的行。

    -- 为了模拟这个场景,我又新增了一个 user_test 表。
    mysql> CREATE TABLE `user_test` (
        -> `id` INT (11) NOT NULL AUTO_INCREMENT,
        -> `name` VARCHAR (20) DEFAULT NULL COMMENT "用户名",
        -> PRIMARY KEY (`id`), 
        -> KEY `idx_name` (`name`)
        -> ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = "用户测试表";
    Query OK, 0 rows affected (0.01 sec)
    
    mysql> EXPLAIN SELECT id FROM user_test WHERE name = 'riemann' OR name IS NULL; 
    +----+-------------+-----------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
    | id | select_type | table     | partitions | type        | possible_keys | key      | key_len | ref   | rows | filtered | Extra                    |
    +----+-------------+-----------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
    |  1 | SIMPLE      | user_test | NULL       | ref_or_null | idx_name      | idx_name | 83      | const |    2 |   100.00 | Using where; Using index |
    +----+-------------+-----------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
    
  • index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。

    -- 下边示例中同时使用到主键 id 和字段 user_id 的索引。
    mysql> EXPLAIN SELECT * FROM user_robot_relate WHERE id > 1 AND user_id = 2; 
    +----+-------------+-------------------+------------+-------------+---------------------+---------------------+---------+------+------+----------+---------------------------------------------------+
    | id | select_type | table             | partitions | type        以上是关于一文读懂 MySQL Explain 执行计划的主要内容,如果未能解决你的问题,请参考以下文章

    一文读懂 MySQL Explain 执行计划

    MySQL进阶系列: 一文详解explain各字段含义

    带你看懂MySQL执行计划

    [z]Oracle性能优化-读懂执行计划

    mysql explain执行计划分析

    MySQL EXPLAIN 命令: 查看查询执行计划