在工作中,我们会开启慢查询,去记录一些执行时间比较久的SQL语句,继而对这些SQL语句进行分析,此时,便用到了explain这个命令来查看这些SQL语句的执行计划——该SQL语句有没有使用索引,有没有做全表扫描,这都可以通过explain命令来查看。
所以,我们深入了解MySQL的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行SQL语句时哪种策略预计会被优化器采用。
使用explain
非常简单,我们只要在SQL语句的前面加上explain
即可,如代码explain select * from students
、explain insert students(name) values('张三')
等。
如下使用SQL查询学生和学生对应的班级,我们看下使用explain返回了哪些字段?
mysql> explain select * from students left join class on students.classid = class.id;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | students | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | SIMPLE | class | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
我们明显地可以看到explain返回了这些字段:id ,select_type ,table ,partitions ,type ,possible_keys ,key ,key_len ,ref ,rows ,filtered ,Extra
。
如下我们便对这些字段,做单独分析。
包含一组数字,表示查询中执行select子句或操作表的顺序
explain select * from class where id in (select classid from students);
+----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | students | NULL | index | fk_classId | fk_classId | 5 | NULL | 2 | 50.00 | Using index; LooseScan |
| 1 | SIMPLE | class | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 50.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
explain select * from class where id = (select distinct classid from students where classid =1);
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+-------------+
| 1 | PRIMARY | class | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 2 | SUBQUERY | students | NULL | ref | fk_classId | fk_classId | 5 | const | 2 | 100.00 | Using index |
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
mysql> explain select * from class where id = (select distinct classid from students left join address on students.id = address.student_id );
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+--------------------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+--------------------------------------------------------------+
| 1 | PRIMARY | class | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 2 | SUBQUERY | students | NULL | index | fk_classId | fk_classId | 5 | NULL | 2 | 100.00 | Using index; Using temporary |
| 2 | SUBQUERY | address | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Distinct; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+--------------------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)
查询中每个select子句的类型
值 | 含义 |
---|---|
SIMPLE | 简单SELECT,不使用UNION或子查询等 |
PRIMARY | 查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY |
UNION | UNION中的第二个或后面的SELECT语句 |
DEPENDENT UNION | UNION中的第二个或后面的SELECT语句,取决于外面的查询 |
UNION RESULT | UNION的结果 |
SUBQUERY | 子查询中的第一个SELECT |
DEPENDENT SUBQUERY | 子查询中的第一个SELECT,取决于外面的查询 |
DERIVED | 派生表的SELECT, FROM子句的子查询 |
UNCACHEABLE SUBQUERY | 一个子查询的结果不能被缓存,必须重新评估外链接的第一行 |
比如上述SQL语句就涉及到 PRIMARY 和SUBQUERY 的查询。
显示当前行的数据是属于哪张表的,但有时不是真实的表名字,我们看到的是derivedx,如下所示:
mysql> EXPLAIN select * from class, (select distinct classid from students left join address on students.id = address.student_id) tmp where class.id = tmp.classid;
+----+-------------+------------+------------+-------+---------------+-------------+---------+---------------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+---------------+-------------+---------+---------------+------+----------+----------------------------------------------------+
| 1 | PRIMARY | class | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 5 | test.class.id | 2 | 100.00 | Using index |
| 2 | DERIVED | students | NULL | index | fk_classId | fk_classId | 5 | NULL | 2 | 100.00 | Using index; Using temporary |
| 2 | DERIVED | address | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+------------+------------+-------+---------------+-------------+---------+---------------+------+----------+----------------------------------------------------+
4 rows in set, 1 warning (0.00 sec)
在第二步的时候就使用到了derived2这张表,但数据库并不存在这张表,因为我们在查询使用使用到了临时表,即如此SQL语句:(select distinct classid from students left join address on students.id = address.student_id) tmp
。
MySQL在表中找到所需行的方式,又称“访问类型”,常见类型:ALL,,index, range,ref,eq_ref, const, system,NULL
,从左到右,性能从低到优。
值 | 含义 |
---|---|
ALL | Full Table Scan, MySQL将遍历全表以找到匹配的行 |
index | Full Index Scan,index与ALL区别为index类型只遍历索引树 |
range | 只检索给定范围的行,使用一个索引来选择行 |
ref | 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 |
eq_ref | 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件 |
const | 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量 |
system | system是const类型的特例,当查询的表只有一行的情况下,使用system |
NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。 |
mysql> explain select * from students left join class on students.classid = class.id;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | students | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | SIMPLE | class | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
mysql> explain select * from students left join class on students.classid = class.id where students.id =1 and class.id =1;
+----+-------------+----------+------------+-------+--------------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+--------------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | students | NULL | const | PRIMARY,fk_classId | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 1 | SIMPLE | class | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+----------+------------+-------+--------------------+---------+---------+-------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)
指出MySQL能使用表中的哪个索引找到记录,若查询的字段上存在索引,则该索引将被列出,但不一定被查询使用。
该列完全独立于explain
输出所示表的次序,这意味着在possible_keys中的某些键,实际上不能按生成的表次序使用。
若该列是NULL,则没有相关的索引,可以通过检查where子句看是否引用某些列,或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用EXPLAIN检查查询。
explain select * from students left join class on students.classid = class.id;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | students | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | SIMPLE | class | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
key列显示MySQL实际决定使用的键(索引)
若没有选择索引,键是NULL,比如
mysql> explain select * from class left join students on students.classid = class.id;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | class | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | SIMPLE | students | NULL | ALL | fk_classId | NULL | NULL | NULL | 2 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)
虽然possible_keys 是fk_classId,但没有索引,若要使索引生效,可以这样写:
mysql> explain select * from class left join students on students.classid = class.id where students.classid =1;
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | class | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 1 | SIMPLE | students | NULL | ref | fk_classId | fk_classId | 5 | const | 2 | 100.00 | NULL |
+----+-------------+----------+------------+-------+---------------+------------+---------+-------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)
若要想强制MySQL使用索引,或者忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
表示索引中使用的字节数,可通过该列计算查询中使用的索引长度。
key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的。
不损失精确度的情况下,长度越短越好 。
列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。
mysql> explain select * from students where id =1;
+----+-------------+----------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | students | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+----------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
mysql> EXPLAIN select * from class, (select distinct classid from students ) tmp where class.id = tmp.classid;
+----+-------------+------------+------------+-------+---------------+-------------+---------+---------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+---------------+-------------+---------+---------------+------+----------+-------------+
| 1 | PRIMARY | class | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 5 | test.class.id | 2 | 100.00 | Using index |
| 2 | DERIVED | students | NULL | index | fk_classId | fk_classId | 5 | NULL | 2 | 100.00 | Using index |
+----+-------------+------------+------------+-------+---------------+-------------+---------+---------------+------+----------+-------------+
3 rows in set, 1 warning (0.04 sec)
过滤列表示按表条件过滤的表行的估计百分比。
最大值为 100,这意味着没有过滤行。 从 100 开始减小的值表示过滤量增加。
rows 显示检查的估计行数, rows × filters 显示与下表连接的行数。 例如,如果 rows 为 1000,filtered 为 50.00 (50%),则要与下表连接的行数为 1000 × 50% = 500。
该列包含MySQL解决查询的详细信息,有以下几种情况:
值 | 含义 |
---|---|
Using where | 列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤 |
Using temporary | 表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询 |
Using filesort | MySQL中无法利用索引完成的排序操作称为“文件排序” |
Using join buffer | 该值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。 |
Impossible where | 这个值强调了where语句会导致没有符合条件的行。 |
Select tables optimized away | 这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 |
using index | 指示查询优化器使用其中一个命名索引来查找表中的行 |
mysql> EXPLAIN select * from students, (select distinct student_id from address ) tmp where students.id = tmp.student_id;
+----+-------------+------------+------------+------+---------------+-------------+---------+------------------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+-------------+---------+------------------+------+----------+--------------------------+
| 1 | PRIMARY | students | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 4 | test.students.id | 2 | 100.00 | Using where; Using index |
| 2 | DERIVED | address | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using temporary |
+----+-------------+------------+------------+------+---------------+-------------+---------+------------------+------+----------+--------------------------+
3 rows in set, 1 warning (0.00 sec)
当前查询编使用到了 Using index,Using where, Using temporary
,也就是说,我们在使用SQL语句时,可能会使用到多个extra。
-- ----------------------------
-- Table structure for class
-- ----------------------------
CREATE TABLE `class` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`no` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of class
-- ----------------------------
INSERT INTO `class` VALUES (1, '六年级', 20060);
INSERT INTO `class` VALUES (2, '七年级', 20061);
-- ----------------------------
-- Table structure for students
-- ----------------------------
CREATE TABLE `students` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`classId` int(11) NULL DEFAULT NULL,
`no` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `fk_classId`(`classId`) USING BTREE,
CONSTRAINT `fk_classId` FOREIGN KEY (`classId`) REFERENCES `class` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of students
-- ----------------------------
INSERT INTO `students` VALUES (1, '张三', 1, '2000000221212');
INSERT INTO `students` VALUES (2, '李四', 1, '2000000021212');
CREATE TABLE `address` (
`student_id` int(10) NOT NULL,
`province` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`city` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`county` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`detail` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of address
-- ----------------------------
INSERT INTO `address` VALUES (1, '汉东省', '汉阳市', '经开区', '经开路18号', 1);