通过 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)
上边输出的这些内容就是所谓的 执行计划
。
我们先来简单的看下这些列的含义:
列名 | 描述 |
---|---|
id |
SELECT 查询的序列号,包含一组数字,表示查询中执行SELECT 子句或操作表的顺序 |
select_type |
SELECT 关键字对应的查询的类型 (普通查询、联合查询、子查询等) |
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
子句或操作表的顺序
一般会出现如下几种情况。
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 |......
+----+-------------+-------+......
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。
指的是将 id
为 1 的查询和 id
为 2 的查询结果合并。
select_type列
这一列显示了对应行的查询类型是简单查询还是复杂查询。
UNION
,我们碰到的大部分查询都是简单查询。mysql> EXPLAIN SELECT * FROM t1, t2 where t1.id = t2.id;
+----+-------------+-------+......
| id | select_type | table |......
+----+-------------+-------+......
| 1 | SIMPLE | t1 |......
| 1 | SIMPLE | t2 |......
+----+-------------+-------+......
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 |......
+----+-------------+-------+......
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 |......
+----+-------------+------------+......
这里
表示的是一个派生的临时表。
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 表的别名,
partitions:
查询匹配记录的分区信息。对于非分区表,值为 NULL 。
type:
MySQL对某个表的执行查询时的访问方法,从最优到最差依次为:
NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
mysql> EXPLAIN select MAX(id) from t1;
+----+-------------+-------+...+------+...
| id | select_type | table |...| type |...
+----+-------------+-------+...+------+...
| 1 | SIMPLE | NULL |...| NULL |...
+----+-------------+-------+...+------+...
创建一个存储引擎为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 |...
+----+-------------+-------+...+--------+...
mysql> EXPLAIN select * from t1 where id = 2;
+----+-------------+-------+...+-------+...+---------+...
| id | select_type | table |...| type |...| key |...
+----+-------------+-------+...+-------+...+---------+...
| 1 | SIMPLE | t1 |...| const |...| PRIMARY |...
+----+-------------+-------+...+-------+...+---------+...
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
全表扫描。
mysql> EXPLAIN select * from t1 where name = 'Lucy';
+----+-------------+-------+...+------+...
| id | select_type | table |...| type |...
+----+-------------+-------+...+------+...
| 1 | SIMPLE | t1 |...| ref |...
+----+-------------+-------+...+------+...
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 |...
+----+-------------+-------+...+-------+...
Extra
列中看到 Using index
,说明查询用到了索引覆盖。之前也已经写过回表查询及索引覆盖方面的内容,感兴趣的同学可以看下:详解MySQL回表查询与索引覆盖mysql> EXPLAIN select name, age from t1;
+----+...+-------+...+-------------+
| id |...| type |...| Extra |
+----+...+-------+...+-------------+
| 1 |...| index |...| Using index |
+----+...+-------+...+-------------+
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 |...
+----+-------------+...+------------------+---------+...
上边这个执行计划显示可以使用 PRIMARY
和 idx_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列
实际使用到的索引长度(字节数),它是由这三个部分构成的:
我们来看两个例子。
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 到底将如何执行给定的查询语句,常用重要的有如下这些。
mysql> EXPLAIN select 1;
+----+-------------+...+----------------+
| id | select_type |...| Extra |
+----+-------------+...+----------------+
| 1 | SIMPLE |...| No tables used |
+----+-------------+...+----------------+
mysql> EXPLAIN select id, name from t1;
+----+...+-------+---------------+----------+...+-------------+
| id |...| type | possible_keys | key |...| Extra |
+----+...+-------+---------------+----------+...+-------------+
| 1 |...| index | NULL | idx_name |...| Using index |
+----+...+-------+---------------+----------+...+-------------+
mysql> EXPLAIN select * from t1 where name='Lucy' and gender=1;
+----+-------------+...+----------+...+-------------+
| id | select_type |...| key |...| Extra |
+----+-------------+...+----------+...+-------------+
| 1 | SIMPLE |...| idx_name |...| Using where |
+----+-------------+...+----------+...+-------------+
mysql> EXPLAIN select * from t1 where name>'Lucy' and age=17;
+----+...+----------+...+-----------------------+
| id |...| key |...| Extra |
+----+...+----------+...+-----------------------+
| 1 |...| idx_name |...| Using index condition |
+----+...+----------+...+-----------------------+
mysql> EXPLAIN SELECT DISTINCT age FROM t1;
+----+...+---------------+----------+...+------------------------------+
| id |...| possible_keys | key |...| Extra |
+----+...+---------------+----------+...+------------------------------+
| 1 |...| idx_name | idx_name |...| Using index; Using temporary |
+----+...+---------------+----------+...+------------------------------+
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 |
+----+-------------+...+------------------+
mysql> EXPLAIN SELECT MAX(age) FROM t1 WHERE name = 'HAHA';
+----+-------------+-------+...+-------------------------+
| id | select_type | table |...| Extra |
+----+-------------+-------+...+-------------------------+
| 1 | SIMPLE | NULL |...| No matching min/max row |
+----+-------------+-------+...+-------------------------+