目录
一条查询语句在经过MySQL
查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划
,这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。Mysql为我们提供了EXPLAIN
语句来帮助我们查看某个查询语句的具体执行计划,从而可以有针对性的提升我们查询语句的性能。
一、初识Explain
如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个EXPLAIN
,就像这样:
- mysql> EXPLAIN SELECT 1;
- +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
- | 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
- +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
- 1 row in set, 1 warning (0.01 sec)
然后这输出的这个表格就是所谓的执行计划
,其实除了以SELECT
开头的查询语句,其余的DELETE
、INSERT
、REPLACE
以及UPDATE
语句前边都可以加上EXPLAIN
这个词儿,用来查看这些语句的执行计划,不过我们一般只会对查询语句的性能进行优化。我们把EXPLAIN
语句输出的各个列的作用先大致罗列一下:
列名 | 描述 |
id | 在一个大的查询语句中每个SELECT 关键字都对应一个唯一的id |
select_type | SELECT 关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际上使用的索引 |
key_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 一些额外的信息 |
下面我们将按照这个建表语句,新建s1,s2表,除id列外其余的列都插入随机值:
- CREATE TABLE single_table (
- id INT NOT NULL 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),
- KEY idx_key1 (key1),
- UNIQUE KEY idx_key2 (key2),
- KEY idx_key3 (key3),
- KEY idx_key_part(key_part1, key_part2, key_part3)
- ) Engine=InnoDB CHARSET=utf8;
建完表以后,我们后面开始对其执行计划的每个具体属性进行讲解,不过不会严格按照上面执行计划的展示顺序。
二、执行计划-table属性
不论我们的查询语句有多复杂,里边儿包含了多少个表,到最后也是需要对每个表进行单表访问的,所以设计MySQL
的大叔规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名。
我们看一下一个连接查询的执行计划:
- mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2;
- +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+
- | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 9688 | 100.00 | NULL |
- | 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 9954 | 100.00 | Using join buffer (Block Nested Loop) |
- +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+
- 2 rows in set, 1 warning (0.01 sec)
可以看到这个连接查询的执行计划中有两条记录,这两条记录的table
列分别是s1
和s2
,这两条记录用来分别说明对s1
表和s2
表的访问方法是什么。
三、执行计划-id属性
我们知道我们写的查询语句一般都以SELECT
关键字开头,这里id代表的就是这个select的id,比较简单的查询语句里只有一个SELECT
关键字,比如单表查询和连表查询。对于连接查询来说,一个SELECT
关键字后边的FROM
子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id值都是相同的,可以看第二节的示例。
针对查询中包含子查询和UNION的SQL语句,每个SELECT
关键字都会对应一个唯一的id
值,所以会有多个id:
- mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';
- +----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
- | 1 | PRIMARY | s1 | NULL | ALL | idx_key3 | NULL | NULL | NULL | 9688 | 100.00 | Using where |
- | 2 | SUBQUERY | s2 | NULL | index | idx_key1 | idx_key1 | 303 | NULL | 9954 | 100.00 | Using index |
- +----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
- 2 rows in set, 1 warning (0.02 sec)
但是这里大家需要特别注意,并不是所有包含子查询的都会是多个id。这是因为,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询(上一章的内容)。所以如果我们想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划就好了,比如说:
- mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key3 FROM s2 WHERE common_field = 'a');
- +----+-------------+-------+------------+------+---------------+----------+---------+-------------------+------+----------+------------------------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+----------+---------+-------------------+------+----------+------------------------------+
- | 1 | SIMPLE | s2 | NULL | ALL | idx_key3 | NULL | NULL | NULL | 9954 | 10.00 | Using where; Start temporary |
- | 1 | SIMPLE | s1 | NULL | ref | idx_key1 | idx_key1 | 303 | xiaohaizi.s2.key3 | 1 | 100.00 | End temporary |
- +----+-------------+-------+------------+------+---------------+----------+---------+-------------------+------+----------+------------------------------+
- 2 rows in set, 1 warning (0.00 sec)
可以看到,虽然我们的查询语句是一个子查询,但是执行计划中s1
和s2
表对应的记录的id
值全部是1
,这就表明了查询优化器将子查询转换为了连接查询。
对于包含UNION
子句的查询语句来说,每个SELECT
关键字对应一个id
值也是没错的,不过还是有点儿特别的东西,比方说下边这个查询:
- mysql> EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
- +----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
- | 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 9688 | 100.00 | NULL |
- | 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 9954 | 100.00 | NULL |
- | NULL | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
- +----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
- 3 rows in set, 1 warning (0.00 sec)
这里UNION会把多个查询的结果集合并起来生成内部的临时表,并对结果集中的记录进行去重,所以在内部创建了一个名为<union1, 2>
的临时表。当然UNION ALL就不会生成这个临时表。
四、执行计划-select_type属性
每一个SELECT
关键字代表的小查询都定义了一个称之为select_type
的属性,实际就是表示了小查询在整个大查询中扮演了一个什么角色。select_type的取值主要包含以下几种:
名称 | 描述 |
SIMPLE | 查询语句中不包含UNION 、UNION ALL 或者子查询的查询都是SIMPLE 类型,当然,连接查询也算是SIMPLE 类型。 |
PRIMARY | 对于包含UNION 、UNION ALL 或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type 值就是PRIMARY。 |
UNION | 对于包含UNION 或者UNION ALL 的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的小查询的select_type 值就是UNION。 |
UNION RESULT | MySQL 选择使用临时表来完成UNION 查询的去重工作,针对该临时表的查询的select_type 就是UNION RESULT ,例子上边有,就不赘述了。 |
SUBQUERY | 如果不相关子查询的查询语句不能够转为对应的semi-join 的形式,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,外层查询的select_type 就是PRIMARY ,子查询的select_type 就是SUBQUERY。 |
DEPENDENT SUBQUERY | 如果相关子查询的查询语句不能够转为对应的semi-join 的形式,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,外层查询的select_type 就是PRIMARY ,子查询的select_type 就是DEPENDENT SUBQUERY。 |
DEPENDENT UNION | 在包含UNION 或者UNION ALL 的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外(DEPENDENT SUBQUERY),其余的小查询的select_type 的值就是DEPENDENT UNION 。 |
DERIVED | 对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的select_type 就是DERIVED。 |
MATERIALIZED | 当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,该子查询对应的select_type 属性就是MATERIALIZED。 |