MySQL执行计划Explain详解

通过 EXPLAIN 命令可以查看某条查询语句的具体执行计划。看懂 EXPLAIN 语句的各个输出项,将帮助我们了解MySQL优化器是如何工作的,从而可以有针对性的来提升我们查询语句的性能。

如果想看某个查询的执行计划,可以在查询语句前边加一个EXPLAIN,就像这样:

mysql> EXPLAIN select 1 \G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: NULL
   partitions: NULL
         type: NULL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: NULL
        Extra: No tables used
1 row in set, 1 warning (0.00 sec)

上边输出的这些内容就是所谓的 执行计划

我们先来简单的看下这些列的含义:

列名描述
idSELECT查询的序列号,包含一组数字,表示查询中执行SELECT子句或操作表的顺序
select_typeSELECT关键字对应的查询的类型 (普通查询、联合查询、子查询等)
table对应行访问的表名
partitions匹配的分区信息
type针对单表的访问方法
possible_keys可能用到的索引
key实际上使用的索引
key_len实际使用到的索引长度(字节数)
ref当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows预估的需要读取的记录条数
filtered某个表经过搜索条件过滤后剩余记录条数的百分比
Extra一些额外的信息

现在我们对这些列有了一个大体的概念,接下来我们具体的介绍这些列都是干嘛的。

为了演示方便,这里我们先来创建两个表 t1、t2。两个表结构一样,我就只贴一个建表语句了。

CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) DEFAULT NULL COMMENT '姓名',
  `age` tinyint(3) unsigned NOT NULL COMMENT '年龄',
  `gender` tinyint(3) unsigned NOT NULL COMMENT '性别:0女,1男,2中性, 3保密',
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`,`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

EXPLAIN 中的列

id列

这一列的编号用来表示查询中执行 SELECT 子句或操作表的顺序

一般会出现如下几种情况。

  • 只有一行,值为1,
mysql> EXPLAIN select 1;
+----+-------------+-------+......
| id | select_type | table |......
+----+-------------+-------+......
|  1 | SIMPLE      | NULL  |......
+----+-------------+-------+......
  • 有多行,且值相同,执行顺序由上至下
mysql> EXPLAIN SELECT * FROM t1, t2 where t1.id = t2.id;
+----+-------------+-------+......
| id | select_type | table |......
+----+-------------+-------+......
|  1 | SIMPLE      | t1    |......
|  1 | SIMPLE      | t2    |......
+----+-------------+-------+......
  • 有多行,值不同,值大的先执行
mysql> EXPLAIN select (select 1 from t2 limit 1) from t1;
+----+-------------+-------+......
| id | select_type | table |......
+----+-------------+-------+......
|  1 | PRIMARY     | t1    |......
|  2 | SUBQUERY    | t2    |......
+----+-------------+-------+......
  • 有多行,有NULL值
mysql> EXPLAIN select * from t1 union select * from t2;
+----+--------------+------------+......
| id | select_type  | table      |......
+----+--------------+------------+......
|  1 | PRIMARY      | t1         |......
|  2 | UNION        | t2         |......
| NULL | UNION RESULT | <union1,2> |......
+----+--------------+------------+......

上边这个执行计划需要解释一下。UNION 会把多个查询的结果合并起来并对结果集中的记录进行去重。UNION 结果会放在一个匿名临时表中,之后MySQL会将结果读取到临时表外。临时表并不在原SQL中出现,所以它的 id 列值为NULL。<union1, 2> 指的是将 id 为 1 的查询和 id 为 2 的查询结果合并。

select_type列

这一列显示了对应行的查询类型是简单查询还是复杂查询。

  • SIMPLE 简单查询: 查询不包含子查询和 UNION ,我们碰到的大部分查询都是简单查询。
mysql> EXPLAIN SELECT * FROM t1, t2 where t1.id = t2.id;
+----+-------------+-------+......
| id | select_type | table |......
+----+-------------+-------+......
|  1 | SIMPLE      | t1    |......
|  1 | SIMPLE      | t2    |......
+----+-------------+-------+......
  • PRIMARY: 如果查询有任何的子部分,则最外层标记为 PRIMARY
mysql> EXPLAIN select * from t1 union select * from t2;
+----+--------------+------------+......
| id | select_type  | table      |......
+----+--------------+------------+......
|  1 | PRIMARY      | t1         |......
|  2 | UNION        | t2         |......
| NULL | UNION RESULT | <union1,2> |......
+----+--------------+------------+......
  • UNION: 对于包含 UNION 或者 UNION ALL 的大查询来说,除了最左边的那个(或者说第一个)小查询以外,其余的小查询的 select_type 值就是UNION

  • UNION RESULT:UNION 结果临时表查询的select_type就是 UNION RESULT

  • SUBQUERY: 包含在 SELECT 列表中的子查询中的 SELECT(不在 FROM 子句中)标记为 SUBQUERY

mysql> EXPLAIN select (select 1 from t2 limit 1) from t1;
+----+-------------+-------+......
| id | select_type | table |......
+----+-------------+-------+......
|  1 | PRIMARY     | t1    |......
|  2 | SUBQUERY    | t2    |......
+----+-------------+-------+......
  • DERIVED:FROM 子句中的子查询 SELECT被标记为 DERIVED ,MySQL会递归执行并将结果存放在临时表中。
mysql> EXPLAIN select * from (select 1 from t2 limit 1) temp;
+----+-------------+------------+......
| id | select_type | table      |......
+----+-------------+------------+......
|  1 | PRIMARY     | <derived2> |......
|  2 | DERIVED     | t2         |......
+----+-------------+------------+......

这里 <derived2> 表示的是一个派生的临时表。

table列

这一列显示了对应行正在访问哪个表。一般为访问的表名,或者该表的别名,或者是临时表。

mysql> EXPLAIN select * from t1 union select * from t2 tt;
+----+--------------+------------+......
| id | select_type  | table      |......
+----+--------------+------------+......
|  1 | PRIMARY      | t1         |......
|  2 | UNION        | tt         |......
| NULL | UNION RESULT | <union1,2> |......
+----+--------------+------------+......

这里 t1 为表名,tt 为 t2 表的别名,<union1,2> 表示为临时表。

partitions:

查询匹配记录的分区信息。对于非分区表,值为 NULL 。

type:

MySQL对某个表的执行查询时的访问方法,从最优到最差依次为:

NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

  • NULL: 这种访问方式意味着MySQL能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或索引。
mysql> EXPLAIN select MAX(id) from t1;
+----+-------------+-------+...+------+...
| id | select_type | table |...| type |...
+----+-------------+-------+...+------+...
|  1 | SIMPLE      | NULL  |...| NULL |...
+----+-------------+-------+...+------+...
  • system: 当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM,那么对该表的访问方法就是 system 。

 创建一个存储引擎为MyISAM的表 t3,并插入一条数据

CREATE TABLE `t3` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

INSERT INTO `t3`(`name`) VALUES ('1');

 这时候来看下执行计划中 type 列的值

mysql> EXPLAIN select * from t3;
+----+-------------+-------+...+--------+...
| id | select_type | table |...| type   |...
+----+-------------+-------+...+--------+...
|  1 | SIMPLE      | t3    |...| system |...
+----+-------------+-------+...+--------+...
  • const: 当根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是 const 。
mysql> EXPLAIN select * from t1 where id = 2;
+----+-------------+-------+...+-------+...+---------+...
| id | select_type | table |...| type  |...| key     |...
+----+-------------+-------+...+-------+...+---------+...
|  1 | SIMPLE      | t1    |...| const |...| PRIMARY |...
+----+-------------+-------+...+-------+...+---------+...
  • eq_ref: 当根据主键或者唯一二级索列与某个参考值(一般不是常数)进行等值匹配时,访问方法就是 eq_ref 。
mysql> EXPLAIN SELECT * FROM t1, t2 where t1.id = t2.id;
+----+-------------+-------+...+--------+...
| id | select_type | table |...| type   |...
+----+-------------+-------+...+--------+...
|  1 | SIMPLE      | t1    |...| ALL    |...
|  1 | SIMPLE      | t2    |...| eq_ref |...
+----+-------------+-------+...+--------+...

 注:这里会对记录数较少的表进行 ALL 全表扫描。

  • ref: 这是一种索引访问,它可能找到多个符合条件的行。当查询使用普通二级索引或者唯一索引的非唯一性前缀(如唯一联合索引,只用第一个列进行查询)时会出现。
mysql> EXPLAIN select * from t1 where name = 'Lucy';
+----+-------------+-------+...+------+...
| id | select_type | table |...| type |...
+----+-------------+-------+...+------+...
|  1 | SIMPLE      | t1    |...| ref  |...
+----+-------------+-------+...+------+...
  • fulltext: 全文检索索引(这里不做介绍)。
  • ref_or_null:ref 类似,增加了null值判断。当索引列的值也可以是NULL值时,就可能会出现。
mysql> EXPLAIN select * from t1 where name = 'Lucy' or name is null;
+----+-------------+-------+...+-------------+...
| id | select_type | table |...| type        |...
+----+-------------+-------+...+-------------+...
|  1 | SIMPLE      | t1    |...| ref_or_null |...
+----+-------------+-------+...+-------------+...
  • index_merge(索引合并): MySQL5.0之前,一个表一次只能使用一个索引,无法同时使用多个索引分别进行条件扫描。但是从5.1开始,引入了 index merge 优化技术,对同一个表可以使用多个索引分别进行条件扫描。
    之前写过一个索引合并导致死锁的案例,有兴趣的同学可以去看下:MySQL 优化 index merge(索引合并)引起的死锁分析

  • unique_subquery、index_subquery : 都是子查询,前者返回唯一值,后者返回可能有重复。

  • range: 索引范围扫描,常用于 >、<、is null、between、in()、or、like 等。

mysql> EXPLAIN select * from t1 where name like 'Lu%';
+----+-------------+-------+...+-------+...
| id | select_type | table |...| type  |...
+----+-------------+-------+...+-------+...
|  1 | SIMPLE      | t1    |...| range |...
+----+-------------+-------+...+-------+...
  • index: 这个跟全表扫描一样,只是扫描的是索引,而不需要扫描数据。如果在 Extra 列中看到 Using index,说明查询用到了索引覆盖。之前也已经写过回表查询及索引覆盖方面的内容,感兴趣的同学可以看下:详解MySQL回表查询与索引覆盖
mysql> EXPLAIN select name, age from t1;
+----+...+-------+...+-------------+
| id |...| type  |...| Extra       |
+----+...+-------+...+-------------+
|  1 |...| index |...| Using index |
+----+...+-------+...+-------------+
  • ALL: 这个就是所说的全表扫描,通常意味着MySQL需要扫描整张表。当然如果出现 LIMIT 等限制条件,MySQL也会及时的停止扫描。
mysql> EXPLAIN select * from t1;
+----+-------------+...+------+...
| id | select_type |...| type |...
+----+-------------+...+------+...
|  1 | SIMPLE      |...| ALL  |...
+----+-------------+...+------+...

possible_keys列

这一列显示查询可以使用到的索引,具体用不用由后边的优化器说了算。

mysql> EXPLAIN select * from t1 where id = 3 and name ='Lucy';
+----+-------------+...+------------------+---------+...
| id | select_type |...| possible_keys    | key     |...
+----+-------------+...+------------------+---------+...
|  1 | SIMPLE      |...| PRIMARY,idx_name | PRIMARY |...
+----+-------------+...+------------------+---------+...

上边这个执行计划显示可以使用 PRIMARYidx_name 这两个索引,最终 key 那列表示最后用了 PRIMARY 这个主键索引。

key列

这一列显示查询实际使用的索引,select_type 为 index_merge(索引合并) 时,key 列可能有多个索引。如果key 中出现的索引,并不存在于 possible_keys 列中,可能是因为用到索引覆盖等原因。

mysql> EXPLAIN select id, name, age from t1;
+----+-------------+...+---------------+----------+...
| id | select_type |...| possible_keys | key      |...
+----+-------------+...+---------------+----------+...
|  1 | SIMPLE      |...| NULL          | idx_name |...
+----+-------------+...+---------------+----------+...

key_len列

实际使用到的索引长度(字节数),它是由这三个部分构成的:

  • 对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是VARCHAR(100),使用的字符集是utf8,那么该列实际占用的最大存储空间就是100 × 3 = 300个字节。
  • 如果索引列可以存储NULL值,则 key_len 加1个字节。
  • 如果索引列是变长类型,会有2个字节的空间来存储该变长列的实际长度。

我们来看两个例子。

mysql> EXPLAIN select * from t1 where name = 'Lucy';
+----+-------------+...+----------+---------+...
| id | select_type |...| key      | key_len |...
+----+-------------+...+----------+---------+...
|  1 | SIMPLE      |...| idx_name | 99      |...
+----+-------------+...+----------+---------+...

看执行计划,语句用到了 idx_name 索引,并且只用到了联合索引的 name 列。name 类型为 varchar(32) DEFAULT NULL,并且此处字符集为 utf8 。根据计算规则 索引长度 = 32 * 3(utf8下每个字符占3个字节) + 1(可以为NULL) + 2(变长) = 99.

查询条件中加上 age 列再来看下。

mysql> EXPLAIN select * from t1 where name = 'Lucy' and age = 17;
+----+-------------+...+---------------+----------+---------+...
| id | select_type |...| possible_keys | key      | key_len |...
+----+-------------+...+---------------+----------+---------+...
|  1 | SIMPLE      |...| idx_name      | idx_name | 100     |...
+----+-------------+...+---------------+----------+---------+...

这条语句比上边的多了一个 age 条件,正好是联合索引的第二列。age 类型为 tinyint(3) unsigned NOT NULL 。MySQL中 tinyint 占一个字节,固定长度,并且这里不允许为NULL,所以最终 key_len 长度为 99 + 1 = 100。

ref列

这一列显示当表使用索引列等值查询时,与索引列进行等值匹配的对象信息,也就是所用的列或常量。

mysql> EXPLAIN SELECT * FROM t1,t2 where t1.id=t2.id and t1.name='Lucy';
+----+-------------+-------+...+----------+...+-------------+...
| id | select_type | table |...| key      |...| ref         |...
+----+-------------+-------+...+----------+...+-------------+...
|  1 | SIMPLE      | t1    |...| idx_name |...| const       |...
|  1 | SIMPLE      | t2    |...| PRIMARY  |...| test2.t1.id |...
+----+-------------+-------+...+----------+...+-------------+...

rows列

这一列是 MySQL 为了找到所需的行,预计需要扫描的行数。受一些因素(存储引擎、表的统计信息、索引选用情况等)的影响,这个估算可能很不准确。并且,在 MySQL 的大多数版本中,它也反应不出 LIMIT 的情况。例如下边这个语句,不需要真的扫描 10 行数据。

mysql> EXPLAIN select * from t1 LIMIT 1;
+----+-------------+...+------+...+------+...
| id | select_type |...| type |...| rows |...
+----+-------------+...+------+...+------+...
|  1 | SIMPLE      |...| ALL  |...|   10 |...
+----+-------------+...+------+...+------+...

通过把所有 rows 列中的值相乘,可以粗略的估算出整个查询需要扫描的行数。如下边这个语句大约会扫描 40 行。

mysql> EXPLAIN select (select 1 from t2) from t1 where id < 5;
+----+-------------+-------+...+----------+...+------+...
| id | select_type | table |...| key      |...| rows |...
+----+-------------+-------+...+----------+...+------+...
|  1 | PRIMARY     | t1    |...| PRIMARY  |...|    4 |...
|  2 | SUBQUERY    | t2    |...| idx_name |...|   10 |...
+----+-------------+-------+...+----------+...+------+...

filtered列

这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数。(为了演示,我给表中多插入了一些数据)

mysql> EXPLAIN select * from t1 where id>500 and gender=1;
+----+-------------+...+---------+...+------+----------+...
| id | select_type |...| key     |...| rows | filtered |...
+----+-------------+...+---------+...+------+----------+...
|  1 | SIMPLE      |...| PRIMARY |...|  765 |    10.00 |...
+----+-------------+...+---------+...+------+----------+...

从执行计划中可以看出,这条语句使用了主键索引。rows 列表示满足 id>500 的记录大约有765条。filtered 这列表示,MySQL预测在这765条记录中,大约有10%的数据满足 gender=1 这个过滤条件,也就是765 * 10% ≈ 76(条数据)。

Extra列

Extra 列是用来说明一些额外信息的,我们可以通过这些额外信息来更准确的理解 MySQL 到底将如何执行给定的查询语句,常用重要的有如下这些。

  • No tables used: 当查询语句没有 FROM 子句时,将会出现。
mysql> EXPLAIN select 1;
+----+-------------+...+----------------+
| id | select_type |...| Extra          |
+----+-------------+...+----------------+
|  1 | SIMPLE      |...| No tables used |
+----+-------------+...+----------------+
  • Using index: 当查询用到索引覆盖时,将会出现。
mysql> EXPLAIN select id, name from t1;
+----+...+-------+---------------+----------+...+-------------+
| id |...| type  | possible_keys | key      |...| Extra       |
+----+...+-------+---------------+----------+...+-------------+
|  1 |...| index | NULL          | idx_name |...| Using index |
+----+...+-------+---------------+----------+...+-------------+
  • Using where: 当存储引擎返回的记录并不都是符合条件的,需要在 server 层进行其它条件筛选过滤时,将会出现。
mysql> EXPLAIN select * from t1 where name='Lucy' and gender=1;
+----+-------------+...+----------+...+-------------+
| id | select_type |...| key      |...| Extra       |
+----+-------------+...+----------+...+-------------+
|  1 | SIMPLE      |...| idx_name |...| Using where |
+----+-------------+...+----------+...+-------------+
  • Using index condition: 索引下推,不需要再在 server 层进行过滤。即,当一些查询条件中的列在索引中存在但却用不到索引时,可以直接在存储引擎层进行判断过滤,就不需要再将这些数据返回到 server 层再进行判断过滤了,这样可以省去很多不必要的回表查找数据的操作。如下这条语句,因为 name 列用的是范围查询,所以 age 列是用不到索引的。
mysql> EXPLAIN select * from t1 where name>'Lucy' and age=17;
+----+...+----------+...+-----------------------+
| id |...| key      |...| Extra                 |
+----+...+----------+...+-----------------------+
|  1 |...| idx_name |...| Using index condition |
+----+...+----------+...+-----------------------+
  • Using temporary: 表示使用了临时表存储中间结果。如去重、排序、分组之类情况下,会用到临时表。
mysql> EXPLAIN SELECT DISTINCT age FROM t1;
+----+...+---------------+----------+...+------------------------------+
| id |...| possible_keys | key      |...| Extra                        |
+----+...+---------------+----------+...+------------------------------+
|  1 |...| idx_name      | idx_name |...| Using index; Using temporary |
+----+...+---------------+----------+...+------------------------------+
  • Using filesort: 这个表示 MySQL 会使用一个外部的索引进行排序,而不是按照索引顺序进行读取。
mysql> EXPLAIN SELECT * FROM t1 order by age;
+----+...+------+...+----------------+
| id |...| key  |...| Extra          |
+----+...+------+...+----------------+
|  1 |...| NULL |...| Using filesort |
+----+...+------+...+----------------+
  • Using intersect(…)、Using union(…)和Using sort_union(…): 当对同一个表使用多个索引分别进行条件扫描,也就是用到索引合并时,将会出现。

  • Impossible WHERE: 查询条件永远为 FALSE 时,将会出现。

mysql> EXPLAIN SELECT * FROM t1 WHERE 1 = 2;
+----+-------------+...+------------------+
| id | select_type |...| Extra            |
+----+-------------+...+------------------+
|  1 | SIMPLE      |...| Impossible WHERE |
+----+-------------+...+------------------+
  • No matching min/max row: 当查询列表中有 MIN 或者 MAX 聚集函数,但是并没有符合 WHERE 子句中的搜索条件的记录时,将会出现。
mysql> EXPLAIN SELECT MAX(age) FROM t1 WHERE name = 'HAHA';
+----+-------------+-------+...+-------------------------+
| id | select_type | table |...| Extra                   |
+----+-------------+-------+...+-------------------------+
|  1 | SIMPLE      | NULL  |...| No matching min/max row |
+----+-------------+-------+...+-------------------------+
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓呆同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值