當你的數(shù)據(jù)里只有幾千幾萬,那么 SQL 優(yōu)化并不會發(fā)揮太大價值,但當你的數(shù)據(jù)里去到了幾百上千萬,SQL 優(yōu)化的價值就體現(xiàn)出來了!因此稍微有些經(jīng)驗的同學都知道,怎么讓 MySQL 查詢語句又快又好是一件很重要的事情。要讓 SQL 又快又好的前提是,我們知道它「病」在哪里,而 explain 關鍵字就是 MySQL 提供給我們的一把武器!
在我們所執(zhí)行的 SQL 前面加上 explain 關鍵字,MySQL 就不會真正去執(zhí)行這條語句,而是模擬優(yōu)化器執(zhí)行 SQL 查詢語句,最后會輸出一系列的指標告訴我們這條語句的性能如何,如下圖所示。
mysql> explain select * from student where id = 1 \G
******************************************************
id: 1
select_type: SIMPLE
table: subject
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
******************************************************
總的來說,explain 關鍵字可以告訴我們下面這么多信息:
- 表的讀取順序如何
- 數(shù)據(jù)讀取操作有哪些操作類型
- 哪些索引可以使用
- 哪些索引被實際使用
- 表之間是如何引用
- 每張表有多少行被優(yōu)化器查詢 ......
今天,我們就來介紹 explain 關鍵字的各個指標的含義。系好安全帶,準備發(fā)車了!
為了方便講解,這里新建了幾張表,并初始化了一些數(shù)據(jù)(建表語句見附錄)。這些表的關系如下:
- 一共有老師、學生、課程三個實體,分別為:teacher、student、course。
- 三個實體間的關系分別為:老師教學生的關系(teacher_student)、學生的課程分數(shù)(student_course)。
ID 字段
ID 字段的值及其排列順序,表明 MySQL 執(zhí)行時從各表取數(shù)據(jù)的順序。一般情況下遵循下面兩個原則:
- ID 相同的組,其執(zhí)行優(yōu)先級按照其順序由上到下。
- ID 越大的組,其執(zhí)行優(yōu)先級越高。
對于下面這個例子:
EXPLAIN SELECT
teacher.*
FROM
teacher,
teacher_student
WHERE
teacher_student.student_name = 's001'
AND teacher.NAME = teacher_student.teacher_name
該例子的輸出為:

上面的輸出一共有 2 條記錄,其 ID 都為 1,這表示其歸為一組。對于 ID 相同的組,MySQL 按照順序從上到下執(zhí)行,即:先拿 teacher_student 表的數(shù)據(jù),再拿 teacher 表的數(shù)據(jù)。
再來看下面這個例子:
EXPLAIN SELECT
*
FROM
teacher
WHERE
NAME IN ( SELECT teacher_name FROM teacher_student WHERE student_name = 'S002' )
該例子的輸出為:

上面的輸出一共有 3 條記錄,其中第 1、2 條的 ID 相同,第 3 條 ID 不同。那么其執(zhí)行順序就是 ID 值越大,其越早執(zhí)行。ID 相同的,按順序執(zhí)行。上面的例子,最早拿 teacher_student 表的數(shù)據(jù),之后是一個子查詢組成的表,最后拿 teacher 表的數(shù)據(jù)。結合 SQL 分析,這也符合我們的常識。因為我們必須先把子查詢的值算出來,因此需要先把 teacher_student 表里的數(shù)據(jù)拿出來,之后才可以拿去 teacher 表里查詢。
select_type 字段
select_type 字段表示該 SQL 是什么查詢類型,一共有以下 6 種:
- SIMPLE:簡單查詢,不包含子查詢或 union 查詢
- PRIMARY:主鍵查詢
- SUBQUERY:在 select 或 where 中包含子查詢
- DERIVED:from 中包含子查詢
- UNION:
- UNION RESULT
SIMPLE
簡單查詢,不包含子查詢或 union 查詢。
-- 查詢T001老師都教了哪些學生
EXPLAIN SELECT
student.*
FROM
teacher,
teacher_student,
student
WHERE
teacher.NAME = 'T001'
AND teacher.NAME = teacher_student.teacher_name
AND teacher_student.student_name = student.NAME

可以看出其 3 個查詢都是簡單(SIMPLE)查詢。因為 ID 相同,所以其查詢順序是按順序來的。首先從 teacher 表中取出數(shù)據(jù),之后從 student 表取出數(shù)據(jù),最后 teacher_student 表取數(shù)據(jù)。
PRIMARY
一般情況下,如果查詢中包含了任何復雜的子查詢,那么最外層查詢會被標記為主查詢。
-- PRIMARY 查詢哪些老師教授了選修數(shù)學課的學生
EXPLAIN SELECT
*
FROM
teacher
WHERE
NAME IN ( SELECT teacher_name FROM teacher_student WHERE student_name = ( SELECT student_name FROM student_course WHERE course_name = 'shuxue' ) )
在上面的查詢中,首先是執(zhí)行 ID 為 3 的查詢,即去 student_course 表取出選修了數(shù)學課的學生名字,之后再去進行最外層的查詢。可以看到最外層查詢的 select_type 為 PRIMARY。
SUBQUERY
在 select 或 where 中包含子查詢,那么 select_type 會被標記為 SUBQUERY。以上面的查詢?yōu)槔?/p>
-- PRIMARY 查詢哪些老師教授了選修數(shù)學課的學生
EXPLAIN SELECT
*
FROM
teacher
WHERE
NAME IN ( SELECT teacher_name FROM teacher_student WHERE student_name = ( SELECT student_name FROM student_course WHERE course_name = 'shuxue' ) )
在該查詢中,where 中包含了子查詢,因此在 explain 中有一個 ID 為 3 的查詢被標記為 SUBQUERY。
DERIVED
在 FROM 中包含子查詢,那么 select_type 會被標記為 SUBQUERY。
UNION
類似包含 union 關鍵字的會被標記成 UNION 類型,這種查詢方式比較少,這里不做深入講解。
UNION RESULT
類似包含 union 關鍵字的會被標記成 UNION RESULT 類型,這種查詢方式比較少,這里不做深入講解。
type 字段
type 字段表示訪問情況,通常用來衡量 SQL 的查詢效率。其值的查詢效率從最好到最差分別為:
- NULL
- system
- const
- eq_ref
- ref
- fulltext
- ref_or_null
- index_merge
- unique_subquery
- index_subquery
- range
- index
- ALL
NULL
NULL 表示 MySQL 能夠在優(yōu)化階段分解查詢語句,在執(zhí)行階段用不著再訪問表或索引。
explain select max(id) from teacher
system
表只有一行記錄(等于系統(tǒng)表),這是 const 類型的特列。
出現(xiàn)的情況較少,這里不深入介紹。
const
const 表示該表最多有一個匹配記錄。
通常情況下是 SQL 中出現(xiàn)了主鍵索引或唯一索引。
explain select * from teacher where name = 'T002'
上面例子中,teacher.name 字段為唯一索引字段,所以通過該字段只能唯一找到一條記錄,因此其 type 類型為 const。
eq_ref
eq_ref 表示主鍵索引或唯一索引的所有部分被連接使用 ,最多只會返回一條符合條件的記錄。
與 const 類型非常相似,唯一的區(qū)別是 eq_ef 通常出現(xiàn)在聯(lián)表的情況下,而 const 通常出現(xiàn)在單表情況下。
EXPLAIN SELECT
*
FROM
teacher,
teacher_student
WHERE
teacher.NAME = teacher_student.teacher_name
從上面的執(zhí)行結果可以看出,其首先全表掃描了 teacher_student 表,之后使用 teacher.name 唯一索引去將聯(lián)合 teacher 表的每一條記錄。
要注意的是,eq_ref 這種情況重點在于:讀取本表中和關聯(lián)表表中的每行組合成的一行。 如果并沒有關聯(lián)表中每行這個概念,那么就不會出現(xiàn) eq_ref 這種類型。例如我在上面的 SQL 中加上 age 為 24 這個條件,即 SQL 為:
EXPLAIN SELECT
*
FROM
teacher,
teacher_student
WHERE
teacher.NAME = teacher_student.teacher_name and teacher.age = 24
執(zhí)行計劃變?yōu)椋?/p>
會看到 type 類型都變?yōu)?ref 了,eq_ref 消失了。
ref
ref 表示使用了非唯一索引掃描,會返回匹配某個單獨值的所有行。
與 const 非常類似,只不過 ref 會匹配到多個記錄,而 const 則只會匹配到單個記錄。
explain select * from teacher where age = 24
age 為普通索引,表中有 2 條記錄。
表中數(shù)據(jù)為:
ref_or_null
類似 ref,但是可以搜索值為 NULL 的行。
explain select * from teacher where age = 24 or age is null
當我們增加 age is null? 查詢條件后,其 type 字段就變成了 ref_or_null。
index_merge
表示使用了索引合并的優(yōu)化方法。
索引合并指的是:對多個索引分別進行條件掃描,然后將它們各自的結果進行合并。
EXPLAIN SELECT * from teacher where id = 1 or age = 24
執(zhí)行計劃為:
可以看到使用了 index_merge 的查詢類型。在 teacher 表中 id 和 age 都是索引,其將兩個字段的索引結果進行合并了。
range
range 表示檢索給定范圍的行,使用一個索引來選擇行,key 列顯示使用了哪個索引。
一般就是在你的 where 語句中出現(xiàn) between、<>、in 等的范圍查詢。
EXPLAIN SELECT * FROM TEACHER where age between 10 and 20
執(zhí)行計劃為:
上面語句中,我們使用 between 進行范圍查詢,因此 type 類型為 range。
index
index 表示只遍歷索引樹,且只從索引樹中獲取數(shù)據(jù)。
EXPLAIN SELECT id, age FROM TEACHER
上面 SQL 中的 id、age 都是索引字段,可以直接從索引樹中讀取。因此其 type 字段為 index,表示此次查詢數(shù)據(jù)可以直接從索引樹獲取到。但是如果查詢的字段不在索引樹中,那么就是全表掃描了。例如:
EXPLAIN SELECT id, enter_time FROM TEACHER
查詢 SQL 的 enter_time 字段不是索引,所以上面的查詢就變成了全表查詢(ALL)。
ALL
ALL 表示該查詢將遍歷全表以找到匹配行,這是最糟糕的一種查詢方式。
table 字段
表示數(shù)據(jù)來自哪張表
possible_keys 字段
顯示可能應用在這張表中的索引,一個或多個。
查詢涉及到的字段若存在索引,則該索引將被列出,但不一定被實際使用。
key 字段
實際使用到的索引,如果為 NULL,則沒有使用索引。
查詢中若使用了覆蓋索引(查詢的列剛好是索引),則該索引僅出現(xiàn)在 key 列表。
select * from teacher where name = 'T001'
上面這個查詢中,key 字段顯示使用了 udx_name 這個索引,也就是 name 這個字段作為索引。
key_len 字段
這一列顯示了 mysql 在索引里使用的字節(jié)數(shù),通過這個值可以算出具體使用了索引中的哪些列。舉例來說,film_actor 的聯(lián)合索引 idx_film_actor_id 由 film_id 和 actor_id 兩個 int 列組成,并且每個 int 是 4 字節(jié)。通過結果中的 key_len=4 可推斷出查詢使用了第一個列:film_id 列來執(zhí)行索引查找。
mysql> explain select * from film_actor where film_id = 2;
+----+-------------+------------+------+-------------------+-------------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+-------------------+-------------------+---------+-------+------+-------------+
| 1 | SIMPLE | film_actor | ref | idx_film_actor_id | idx_film_actor_id | 4 | const | 1 | Using index |
+----+-------------+------------+------+-------------------+-------------------+---------+-------+------+-------------+
key_len 計算規(guī)則如下:
字符串
- char (n):n 字節(jié)長度
- varchar (n):2 字節(jié)存儲字符串長度,如果是 utf-8,則長度 3n + 2
數(shù)值類型
- tinyint:1 字節(jié)
- smallint:2 字節(jié)
- int:4 字節(jié)
- bigint:8 字節(jié)
時間類型
- date:3 字節(jié)
- timestamp:4 字節(jié)
- datetime:8 字節(jié)
其他
如果字段允許為 NULL,需要 1 字節(jié)記錄是否為 NULL
ref 字段
這一列顯示了在 key 列記錄的索引中,表查找值所用到的列或常量,常見的有:const(常量),func,NULL,字段名(例:film.id)。
rows 列
這一列是 mysql 估計要讀取并檢測的行數(shù),注意這個不是結果集里的行數(shù)。
Extra 列
這一列展示的是額外信息。
distinct
一旦 mysql 找到了與行相聯(lián)合匹配的行,就不再搜索了。
mysql> explain select distinct name from film left join film_actor on film.id = film_actor.film_id;
+----+-------------+------------+-------+-------------------+-------------------+---------+--------------+------+------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+-------------------+-------------------+---------+--------------+------+------------------------------+
| 1 | SIMPLE | film | index | idx_name | idx_name | 33 | NULL | 3 | Using index; Using temporary |
| 1 | SIMPLE | film_actor | ref | idx_film_actor_id | idx_film_actor_id | 4 | test.film.id | 1 | Using index; Distinct |
+----+-------------+------------+-------+-------------------+-------------------+---------+--------------+------+------------------------------+
Using index
這表示查找某個表的時候,所需要的信息直接從索引就可以拿到,而不需要再訪問行記錄。
mysql> explain select id from film order by id;
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | film | index | NULL | PRIMARY | 4 | NULL | 3 | Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
上面例子中,我只是選擇了 id 列,這個列本身是索引,其信息直接在索引樹中就可以拿到,因此不需要再訪問行記錄。
Using where
mysql 服務器將在存儲引擎檢索行后再進行過濾。就是先讀取整行數(shù)據(jù),再按 where 條件進行檢查,符合就留下,不符合就丟棄。
mysql> explain select * from film where id > 1;
+----+-------------+-------+-------+---------------+----------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+----------+---------+------+------+--------------------------+
| 1 | SIMPLE | film | index | PRIMARY | idx_name | 33 | NULL | 3 | Using where; Using index |
+----+-------------+-------+-------+---------------+----------+---------+------+------+--------------------------+
Using temporary
mysql 需要創(chuàng)建一張臨時表來處理查詢。出現(xiàn)這種情況一般是要進行優(yōu)化的,首先是想到用索引來優(yōu)化。
1. actor.name沒有索引,此時創(chuàng)建了張臨時表來distinct
mysql> explain select distinct name from actor;
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------+
| 1 | SIMPLE | actor | ALL | NULL | NULL | NULL | NULL | 2 | Using temporary |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------+
2. film.name建立了idx_name索引,此時查詢時extra是using index,沒有用臨時表
mysql> explain select distinct name from film;
+----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+
| 1 | SIMPLE | film | index | idx_name | idx_name | 33 | NULL | 3 | Using index |
+----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+
Using filesort
MySQL 中無法利用索引完成的排序操作稱為「文件排序」。
在 MySQL 中的 ORDER BY 有兩種排序?qū)崿F(xiàn)方式:
- 利用有序索引獲取有序數(shù)據(jù)
- 文件排序
在 explain 中分析查詢的時候,利用有序索引獲取有序數(shù)據(jù)顯示 Using index ,文件排序顯示 Using filesort。至于什么時候使用索引排序,什么時候使用文件排序,這個問題太過于復雜,這里不做深入介紹。
1. actor.name未創(chuàng)建索引,會瀏覽actor整個表,保存排序關鍵字name和對應的id,然后排序name并檢索行記錄
mysql> explain select * from actor order by name;
+----+-------------+-------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------+
| 1 | SIMPLE | actor | ALL | NULL | NULL | NULL | NULL | 2 | Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------+
2. film.name建立了idx_name索引,此時查詢時extra是using index
mysql> explain select * from film order by name;
+----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+
| 1 | SIMPLE | film | index | NULL | idx_name | 33 | NULL | 3 | Using index |
+----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+
建表語句
建表語句如下:
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80019
Source Host : localhost:3306
Source Schema : test
Target Server Type : MySQL
Target Server Version : 80019
File Encoding : 65001
Date: 22/06/2020 08:59:15
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for course
-- ----------------------------
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `udx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of course
-- ----------------------------
BEGIN;
INSERT INTO `course` VALUES (2, 'shuxue');
INSERT INTO `course` VALUES (3, 'yingyu');
INSERT INTO `course` VALUES (1, 'yuwen');
COMMIT;
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`age` int DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `udx_name` (`name`),
UNIQUE KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of student
-- ----------------------------
BEGIN;
INSERT INTO `student` VALUES (1, 'S001', 24);
INSERT INTO `student` VALUES (2, 'S002', 23);
INSERT INTO `student` VALUES (3, 'S003', 22);
COMMIT;
-- ----------------------------
-- Table structure for student_course
-- ----------------------------
DROP TABLE IF EXISTS `student_course`;
CREATE TABLE `student_course` (
`id` int NOT NULL AUTO_INCREMENT,
`student_name` varchar(20) DEFAULT NULL,
`course_name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_student_name` (`student_name`),
KEY `idx_course_name` (`course_name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of student_course
-- ----------------------------
BEGIN;
INSERT INTO `student_course` VALUES (1, 'S001', 'yuwen');
INSERT INTO `student_course` VALUES (2, 'S001', 'shuxue');
INSERT INTO `student_course` VALUES (3, 'S001', 'yingyu');
INSERT INTO `student_course` VALUES (4, 'S002', 'yuwen');
INSERT INTO `student_course` VALUES (5, 'S002', 'shuxue');
INSERT INTO `student_course` VALUES (6, 'S003', 'yuwen');
COMMIT;
-- ----------------------------
-- Table structure for teacher
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`enter_time` datetime DEFAULT NULL,
`age` int DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `udx_name` (`name`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of teacher
-- ----------------------------
BEGIN;
INSERT INTO `teacher` VALUES (1, 'T001', '2020-06-16 21:51:54', 12);
INSERT INTO `teacher` VALUES (2, 'T002', '2020-06-15 21:52:02', 12);
INSERT INTO `teacher` VALUES (3, 'T003', '2020-06-14 21:52:08', 24);
INSERT INTO `teacher` VALUES (4, 'T004', '2020-06-14 21:52:08', 24);
COMMIT;
-- ----------------------------
-- Table structure for teacher_student
-- ----------------------------
DROP TABLE IF EXISTS `teacher_student`;
CREATE TABLE `teacher_student` (
`id` int NOT NULL AUTO_INCREMENT,
`teacher_name` varchar(20) DEFAULT NULL,
`student_name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_teacher_name` (`teacher_name`),
KEY `idx_student_name` (`student_name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of teacher_student
-- ----------------------------
BEGIN;
INSERT INTO `teacher_student` VALUES (1, 'T001', 'S001');
INSERT INTO `teacher_student` VALUES (2, 'T001', 'S002');
INSERT INTO `teacher_student` VALUES (3, 'T001', 'S003');
INSERT INTO `teacher_student` VALUES (4, 'T002', 'S001');
INSERT INTO `teacher_student` VALUES (5, 'T002', 'S002');
INSERT INTO `teacher_student` VALUES (6, 'T003', 'S001');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
參考資料
- 一張圖徹底搞懂 MySQL 的 explain - 個人文章 - SegmentFault 思否
- MySQL explain 詳解 - butterfly100 - 博客園?