[轉載] 長達1.7萬子的關鍵字指南

文章轉自:

https://www.cnblogs.com/chanshuyi/p/quick-start-of-mysql-explain.html

當你的數據裏只有幾千幾萬,那麼 SQL 優化並不會發揮太大價值,但當你的數據裏去到了幾百上千萬,SQL 優化的價值就體現出來了!因此稍微有些經驗的同學都知道,怎麼讓 MySQL 查詢語句又快又好是一件很重要的事情。要讓 SQL 又快又好的前提是,我們知道它「病」在哪裏,而 explain 關鍵字就是 MySQL 提供給我們的一把武器!

在我們所執行的 SQL 前面加上 explain 關鍵字,MySQL 就不會真正去執行這條語句,而是模擬優化器執行 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 關鍵字可以告訴我們下面這麼多信息:

  1. 表的讀取順序如何
  2. 數據讀取操作有哪些操作類型
  3. 哪些索引可以使用
  4. 哪些索引被實際使用
  5. 表之間是如何引用
  6. 每張表有多少行被優化器查詢
    ......

今天,我們就來介紹 explain 關鍵字的各個指標的含義。繫好安全帶,準備發車了!

爲了方便講解,這裏新建了幾張表,並初始化了一些數據(建表語句見附錄)。這些表的關係如下:

  • 一共有老師、學生、課程三個實體,分別爲:teacher、student、course。
  • 三個實體間的關係分別爲:老師教學生的關係(teacher_student)、學生的課程分數(student_course)。

ID 字段

ID 字段的值及其排列順序,表明 MySQL 執行時從各表取數據的順序。一般情況下遵循下面兩個原則:

  • ID 相同的組,其執行優先級按照其順序由上到下。
  • ID 越大的組,其執行優先級越高。

對於下面這個例子:

EXPLAIN SELECT
	teacher.* 
FROM
	teacher,
	teacher_student 
WHERE
	teacher_student.student_name = 's001' 
	AND teacher.NAME = teacher_student.teacher_name

該例子的輸出爲:

img

上面的輸出一共有 2 條記錄,其 ID 都爲 1,這表示其歸爲一組。對於 ID 相同的組,MySQL 按照順序從上到下執行,即:先拿 teacher_student 表的數據,再拿 teacher 表的數據。

再來看下面這個例子:

EXPLAIN SELECT
	* 
FROM
	teacher 
WHERE
	NAME IN ( SELECT teacher_name FROM teacher_student WHERE student_name = 'S002' )

該例子的輸出爲:

img

上面的輸出一共有 3 條記錄,其中第 1、2 條的 ID 相同,第 3 條 ID 不同。那麼其執行順序就是 ID 值越大,其越早執行。ID 相同的,按順序執行。上面的例子,最早拿 teacher_student 表的數據,之後是一個子查詢組成的表,最後拿 teacher 表的數據。結合 SQL 分析,這也符合我們的常識。因爲我們必須先把子查詢的值算出來,因此需要先把 teacher_student 表裏的數據拿出來,之後纔可以拿去 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

img

可以看出其 3 個查詢都是簡單(SIMPLE)查詢。因爲 ID 相同,所以其查詢順序是按順序來的。首先從 teacher 表中取出數據,之後從 student 表取出數據,最後 teacher_student 表取數據。

PRIMARY

一般情況下,如果查詢中包含了任何複雜的子查詢,那麼最外層查詢會被標記爲主查詢。

-- PRIMARY 查詢哪些老師教授了選修數學課的學生
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' ) )

img

在上面的查詢中,首先是執行 ID 爲 3 的查詢,即去 student_course 表取出選修了數學課的學生名字,之後再去進行最外層的查詢。可以看到最外層查詢的 select_type 爲 PRIMARY。

SUBQUERY

在 select 或 where 中包含子查詢,那麼 select_type 會被標記爲 SUBQUERY。以上面的查詢爲例:

-- PRIMARY 查詢哪些老師教授了選修數學課的學生
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' ) )

img

在該查詢中,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 能夠在優化階段分解查詢語句,在執行階段用不着再訪問表或索引。

explain select max(id) from teacher

img

system

表只有一行記錄(等於系統表),這是const類型的特列。

出現的情況較少,這裏不深入介紹。

const

const 表示該表最多有一個匹配記錄。

通常情況下是 SQL 中出現了主鍵索引或唯一索引。

explain select * from teacher where name = 'T002'

img

上面例子中,teacher.name 字段爲唯一索引字段,所以通過該字段只能唯一找到一條記錄,因此其 type 類型爲 const。

eq_ref

eq_ref 表示主鍵索引或唯一索引的所有部分被連接使用 ,最多隻會返回一條符合條件的記錄。

與 const 類型非常相似,唯一的區別是 eq_ef 通常出現在聯表的情況下,而 const 通常出現在單表情況下。

EXPLAIN SELECT
	* 
FROM
	teacher,
	teacher_student 
WHERE
	teacher.NAME = teacher_student.teacher_name

img

從上面的執行結果可以看出,其首先全表掃描了 teacher_student 表,之後使用 teacher.name 唯一索引去將聯合 teacher 表的每一條記錄。

img

要注意的是,eq_ref 這種情況重點在於:讀取本表中和關聯表表中的每行組合成的一行。 如果並沒有關聯表中每行這個概念,那麼就不會出現 eq_ref 這種類型。例如我在上面的 SQL 中加上 age 爲 24 這個條件,即 SQL 爲:

EXPLAIN SELECT
	* 
FROM
	teacher,
	teacher_student 
WHERE
	teacher.NAME = teacher_student.teacher_name and teacher.age = 24

執行計劃變爲:

img

會看到 type 類型都變爲 ref 了,eq_ref 消失了。

ref

ref 表示使用了非唯一索引掃描,會返回匹配某個單獨值的所有行。

與 const 非常類似,只不過 ref 會匹配到多個記錄,而 const 則只會匹配到單個記錄。

explain select * from teacher where age = 24

age 爲普通索引,表中有 2 條記錄。

img

表中數據爲:

img

ref_or_null

類似ref,但是可以搜索值爲NULL的行。

explain select * from teacher where age = 24 or age is null

當我們增加 age is null 查詢條件後,其 type 字段就變成了 ref_or_null

img

index_merge

表示使用了索引合併的優化方法。

索引合併指的是:對多個索引分別進行條件掃描,然後將它們各自的結果進行合併。

EXPLAIN SELECT * from teacher where id = 1 or age = 24

執行計劃爲:

img

可以看到使用了 index_merge 的查詢類型。在 teacher 表中 id 和 age 都是索引,其將兩個字段的索引結果進行合併了。

range

range 表示檢索給定範圍的行,使用一個索引來選擇行,key 列顯示使用了哪個索引。

一般就是在你的 where 語句中出現 between、<>、in 等的範圍查詢。

EXPLAIN SELECT * FROM TEACHER where age between 10 and 20

執行計劃爲:

img

上面語句中,我們使用 between 進行範圍查詢,因此 type 類型爲 range。

index

index 表示只遍歷索引樹,且只從索引樹中獲取數據。

EXPLAIN SELECT id, age FROM TEACHER 

img

上面 SQL 中的 id、age 都是索引字段,可以直接從索引樹中讀取。因此其 type 字段爲 index,表示此次查詢數據可以直接從索引樹獲取到。但是如果查詢的字段不在索引樹中,那麼就是全表掃描了。例如:

EXPLAIN SELECT id, enter_time FROM TEACHER 

img

查詢 SQL 的 enter_time 字段不是索引,所以上面的查詢就變成了全表查詢(ALL)。

ALL

ALL 表示該查詢將遍歷全表以找到匹配行,這是最糟糕的一種查詢方式。

table 字段

表示數據來自哪張表

possible_keys 字段

顯示可能應用在這張表中的索引,一個或多個。

查詢涉及到的字段若存在索引,則該索引將被列出,但不一定被實際使用。

key 字段

實際使用到的索引,如果爲NULL,則沒有使用索引。

查詢中若使用了覆蓋索引(查詢的列剛好是索引),則該索引僅出現在key列表。

select * from teacher where name = 'T001'

img

上面這個查詢中,key 字段顯示使用了 udx_name 這個索引,也就是 name 這個字段作爲索引。

key_len 字段

這一列顯示了 mysql 在索引裏使用的字節數,通過這個值可以算出具體使用了索引中的哪些列。 舉例來說,film_actor的聯合索引 idx_film_actor_id 由 film_id 和 actor_id 兩個int列組成,並且每個int是4字節。通過結果中的key_len=4可推斷出查詢使用了第一個列:film_id列來執行索引查找。

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計算規則如下:

字符串

  • char(n):n字節長度
  • varchar(n):2字節存儲字符串長度,如果是utf-8,則長度 3n + 2

數值類型

  • tinyint:1字節
  • smallint:2字節
  • int:4字節
  • bigint:8字節 

時間類型

  • date:3字節
  • timestamp:4字節
  • datetime:8字節

其他

如果字段允許爲 NULL,需要1字節記錄是否爲 NULL

ref 字段

這一列顯示了在 key 列記錄的索引中,表查找值所用到的列或常量,常見的有:const(常量),func,NULL,字段名(例:film.id)。

rows 列

這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集裏的行數。

Extra 列

這一列展示的是額外信息。

distinct

一旦mysql找到了與行相聯合匹配的行,就不再搜索了。

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 服務器將在存儲引擎檢索行後再進行過濾。就是先讀取整行數據,再按 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需要創建一張臨時表來處理查詢。出現這種情況一般是要進行優化的,首先是想到用索引來優化。

1. actor.name沒有索引,此時創建了張臨時表來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有兩種排序實現方式:

  1. 利用有序索引獲取有序數據
  2. 文件排序

在explain中分析查詢的時候,利用有序索引獲取有序數據顯示 Using index ,文件排序顯示 Using filesort。至於什麼時候使用索引排序,什麼時候使用文件排序,這個問題太過於複雜,這裏不做深入介紹。

1. actor.name未創建索引,會瀏覽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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章