MySQL 性能調優專題二(Explain執行計劃使用詳解)

什麼是Explain執行計劃

  • EXPLAIN 分析你的查詢語句或是結構的性能瓶頸html

  • 使用EXPLAIN 關鍵字能夠模擬優化器執行 SQL 語句,從而知道 MySQL 是如何處理你的 SQL 語句的。mysql

  • 在平常工做中,咱們有時會碰到執行較慢的 SQL,此時咱們可使用 EXPLAIN 關聯字來執行 SQL,能夠查看到 SQL 語句有沒有用到索引,是否是全表掃描等等,這些均可以經過 EXPLIN 命令來查看,咱們能夠經過查看到的信息進行進一步的優化。sql

  • 在SELECT語句以前增長 EXPLAIN 關鍵字,MySQL 會在查詢上設置一個標識,執行查詢時,會返回執行計劃的信息,而不是執行這條 SQLbash

數據準備

  • 學生表

id = 主鍵、 name = 學生姓名、 update_time = 更新時間函數

DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入數據
INSERT INTO `student` (`id`, `name`, `update_time`) VALUES (1,'a','2017-12-22 15:27:18'), (2,'b','2017-12-22 15:27:18'), (3,'c','2017-12-22 15:27:18');
複製代碼
  • 課程表

id = 主鍵、 name = 課程名稱(普通索引)性能

DROP TABLE IF EXISTS `course`;
CREATE TABLE `course` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入數據
INSERT INTO `course` (`id`, `name`) VALUES (1,'Java'), (2,'Python'), (3,'JS');
複製代碼
  • 學生和課程關係表

id = 主鍵、 student_id = 學生表主鍵、 course_id = 課程表主鍵、 remark = 備註測試

其中 student_id 和 course_id 爲複合索引優化

DROP TABLE IF EXISTS `student_course`;
CREATE TABLE `student_course` (
  `id` int(11) NOT NULL,
  `student_id` int(11) NOT NULL,
  `course_id` int(11) NOT NULL,
  `remark` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_student_course_id` (`student_id`,`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 插入數據
INSERT INTO `student_course` (`id`, `student_id`, `course_id`) VALUES (1,1,1),(2,1,2),(3,2,1); 
複製代碼

EXPLAIN 輸出結果集

到如今爲止,須要準備的表已經完成了,咱們先簡單使用下 EXPLAIN 看下都有哪些東西。spa

explain select * from actor; 
複製代碼

執行上面這條語句,會獲得這麼一個結果集,稍後咱們詳細介紹每一個字段的含義。翻譯

在使用EXPLAIN時,在查詢中的每一個表都會輸出一行記錄(一條SQL中有幾個 SElECT 就會有幾條記錄)。

EXPLAIN 有兩個變種:

  • explain extended: 會在 explain 的基礎上額外提供一些查詢優化的信息。在其後經過 show warnings; 命令能夠獲得優化後的查詢語句,能夠獲得優化後的查詢語句,從而看出優化器優化了什麼。(這個能夠優化一些簡單的SQL, 稍微複雜一點的仍是本身動手吧 ヾ(=・ω・=)o )
explain extended select * from student WHERE id = 1; 
SHOW WARNINGS;
複製代碼

  • explain partitions: 相比 explain 多了個 partitions 字段,若是查詢是基於分區表的話,會顯示查詢將訪問的分區。

EXPLAIN 中的列

接下來咱們將展現 EXPLAIN 中每一個列的信息。

id 列

id 列的編號是 SELECT 的序列號,有幾個 SELECT 就有幾個 id,而且 id 的順序是按 SELECT 出現的順序增加的。 id 列越大執行優先級越高,id 相同則從上往下執行,id 爲 NULL 最後執行

  • 簡單子查詢
EXPLAIN SELECT (SELECT 1 FROM student LIMIT 1) FROM course;
複製代碼

  • FROM 子句中的子查詢
EXPLAIN SELECT id FROM (SELECT id FROM student) AS stu;
複製代碼

注: 我使用的 5.7 的版本,在執行後,只是一條簡單的查詢,返回結果裏並無用到臨時表。

我在使用 5.6 版本測試的時候,執行該語句,獲得的結果集中,出現了臨時表。

在5.7以前的版本,會建立一個臨時表,而外部的 SELECT 查詢時用到了這個臨時表,例以下圖:

id 爲 2 的 select_type 爲 DERIVER,id 爲 1 的 在查詢時 table 爲 說明用到了臨時表。

查看了官方文檔,官方文檔這樣寫道:使用了合併或實現來優化派生表和視圖引用 (原諒我個英語渣渣,直接谷歌翻譯了( ̄. ̄),英文好的大佬能夠直接看官網原文)。

優化器可使用兩種策略(也適用於視圖引用)處理派生表引用:

  • 將派生表合併到外部查詢塊中

  • 將派生表實現爲內部臨時表

例1:
SELECT * FROM (SELECT * FROM t1) AS derived_t1;

經過合併派生表 derived_t1,該查詢的執行相似於:

SELECT * FROM t1;

例2:

SELECT *
  FROM t1 JOIN (SELECT t2.f1 FROM t2) AS derived_t2 ON t1.f2=derived_t2.f1
  WHERE t1.f1 > 0;
  
經過合併派生表 derived_t2,該查詢的執行相似於:

SELECT t1.*, t2.f1
  FROM t1 JOIN t2 ON t1.f2=t2.f1
  WHERE t1.f1 > 0;
複製代碼

簡而言之就是:優化器以相同的方式處理派生表和視圖引用:它儘量避免沒必要要的實現,這樣能夠將條件從外部查詢推送到派生表,並生成更高效的執行計劃。

  • union查詢
EXPLAIN SELECT 1 UNION ALL SELECT 1;
複製代碼

注: 和 FROM 子句中的子查詢 同樣,5.7以後的版本進行了優化。

select_type列

select_type 表示對應行是簡單仍是複雜的查詢,若是是複雜的查詢,又是上述三種複雜查詢中的哪種。

  • simple

簡單查詢

EXPLAIN SELECT * FROM student WHERE id = 2;
複製代碼

  • primary

複雜查詢中最外層的 select

  • subquery

包含在 select 中的子查詢(不在 from 子句中)

  • derived

MySQL會將結果存放在一個臨時表中,也稱爲派生表(derived的英文含義)

用這個例子來了解 primary、subquery 類型

EXPLAIN SELECT (SELECT 1 FROM student WHERE id = 1) FROM (SELECT * FROM course WHERE id = 1) c;
複製代碼

  • union

在 union 中的第二個和隨後的 select

  • union result

從 union 臨時表檢索結果的 select

table列

這一列表示 EXPLAIN 的一行正在訪問哪一個表。

type列(重要)

這一列表示關聯類型或訪問類型,即 MySQL 決定如何查找表中的行,查找數據行記錄的大概範圍。

依次從最優到最差分別爲:system > const > eq_ref > ref > range > index > ALL,通常來講,得保證查詢達到 range 級別,最好達到 ref

  • NULL

MySQL 可以在優化階段分解查詢語句,在執行階段用不着再訪問表或索引。

例如:在索引列中選取最小值,能夠單獨查找索引來完成,不須要在執行時訪問表。

EXPLAIN SELECT MIN(id) FROM student;
複製代碼

  • const、system

MySQL 能對查詢的某部分進行優化並將其轉化成一個常量(能夠看show warnings 的結果)。

  • 用於 primary keyunique key 的全部列與常數比較時,因此表最多有一個匹配行,讀取1次,速度比較快。

  • system 是 const 的特例,表裏只有一條元組匹配時爲 system

EXPLAIN extended SELECT * FROM (SELECT * FROM course WHERE id = 1) tmp;
複製代碼

show warnings; 
複製代碼

  • eq_ref

primary keyunique key 索引的全部部分被鏈接使用 ,最多隻會返回一條符合條件的記錄。這多是在 const 以外最好的聯接類型了,簡單的 select 查詢不會出現這種 type。

EXPLAIN SELECT * FROM student_course LEFT JOIN course ON student_course.course_id = course.id;
複製代碼

  • ref

相比 eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前綴,索引要和某個值相比較,可能會找到多個符合條件的行。

  • 簡單 SELECT 查詢,name 是普通索引(非惟一索引)
EXPLAIN SELECT * FROM course WHERE name = "Java";
複製代碼

  • 關聯表查詢,idx_student_course_id 是 student_id 和 course_id 的聯合索引,這裏使用到了 student_course 的左邊前綴 student_id 部分。
EXPLAIN SELECT student_id FROM student LEFT JOIN student_course ON student.id = student_course.student_id;
複製代碼

  • range

範圍掃描一般出如今 IN()、BETWEEN、>、<、>= 等操做中。使用一個索引來檢索給定範圍的行。

EXPLAIN SELECT * FROM student WHERE id > 1;
複製代碼

  • index

掃描全表索引,這一般比ALL快一些。(index是從索引中讀取的,而all是從硬盤中讀取)

EXPLAIN SELECT * FROM course;
複製代碼

  • ALL

即全表掃描,意味着 MySQL 須要從頭至尾去查找所須要的行。一般狀況下這須要增長索引來進行優化了。

EXPLAIN SELECT * FROM student;
複製代碼

possible_keys列

這一列顯示查詢可能使用哪些索引來查找。

  • 使用 EXPLAIN 時可能出現 possible_keys 有列,而 key 顯示 NULL 的狀況,這種狀況是由於表中數據很少,MySQL 認爲索引對此查詢幫助不大,選擇了全表查詢。

  • 若是該列是NULL,則沒有相關的索引。在這種狀況下,能夠經過檢查 WHERE 子句看是否能夠創造一個適當的索引來提升查詢性能,而後用 EXPLAIN 查看效果。

key列

這一列顯示 MySQL 實際採用哪一個索引來優化對該表的訪問。

  • 若是沒有使用索引,則該列是 NULL。

  • 若是想強制 MySQL 使用或忽視 possible_keys 列中的索引,在查詢中使用 force index、ignore index。

key_len列

這一列顯示了 MySQL 在索引裏使用的字節數,經過這個值能夠算出具體使用了索引中的哪些列。

key_len 計算規則以下:

  • 字符串

    類型 長度
    char(n) n字節長度
    varchar(n) 2字節存儲字符串長度,若是是utf-8,則長度 3n + 2
  • 數值類型

    類型 長度
    tinyint 1字節
    tinyint 2字節
    int 4字節
    bigint 8字節
  • 時間類型 

    類型 長度
    date 3字節
    timestamp 4字節
    timestamp 8字節
  • 若是字段容許爲 NULL,須要1字節記錄是否爲 NULL

索引最大長度是768字節,當字符串過長時,MySQL 會作一個相似左前綴索引的處理,將前半部分的字符提取出來作索引。

ref列

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

rows列

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

Extra列(重要)

這一列展現的是額外信息。常見的重要值以下:

  • Using index

查詢的列被索引覆蓋,而且 WHERE 篩選條件是索引的前導列,是性能高的表現。通常是使用了覆蓋索引(索引包含了全部查詢的字段)。對於 InnoDB 來講,若是是輔助索引性能會有很多提升。

EXPLAIN SELECT student_id FROM student_course WHERE student_id = 1;
複製代碼

  • Using where

查詢的列未被索引覆蓋, WHERE 篩選條件非索引的前導列

EXPLAIN SELECT * FROM student WHERE NAME = 'Python';
複製代碼

  • Using where Using index

查詢的列被索引覆蓋,而且 WHERE 篩選條件是索引列之一可是不是索引的前導列,意味着沒法直接經過索引查找來查詢到符合條件的數據

EXPLAIN SELECT course_id FROM student_course WHERE course_id = 1;
複製代碼

  • NULL

查詢的列未被索引覆蓋,而且 WHERE 篩選條件是索引的前導列,意味着用到了索引,可是部分字段未被索引覆蓋,必須經過 「回表」 來實現,不是純粹地用到了索引,也不是徹底沒用到索引

EXPLAIN SELECT * FROM student_course WHERE student_id = 1;
複製代碼

  • Using index condition

與 Using where 相似,查詢的列不徹底被索引覆蓋, WHERE 條件中是一個前導列的範圍

EXPLAIN SELECT * FROM student_course WHERE student_id > 1;
複製代碼

  • Using temporary

MySQL 須要建立一張臨時表來處理查詢。出現這種狀況通常是要進行優化的,首先是想到用索引來優化。

  • student.name沒有索引,此時建立了張臨時表來 DISTINCT
EXPLAIN SELECT DISTINCT name FROM student; 
複製代碼

  • course.name創建了idx_name索引,此時查詢時 extra 是 Using index,沒有用臨時表 。
EXPLAIN SELECT DISTINCT name FROM course;
複製代碼

  • Using filesort

MySQL 會對結果使用一個外部索引排序,而不是按索引次序從表裏讀取行。此時 MySQL 會根據聯接類型瀏覽全部符合條件的記錄,並保存排序關鍵字和行指針,而後排序關鍵字並按順序檢索行信息。這種狀況下也是要考慮使用索引來優化的。

  • student.name 未建立索引,會瀏覽 student 整個表,保存排序關鍵字 name 和對應的 id,而後排序 name 並檢索行記錄
EXPLAIN SELECT * FROM student ORDER BY name;
複製代碼

  • course.name 創建了idx_name索引,此時查詢時 extra 是 Using index。
EXPLAIN SELECT * FROM course ORDER BY name;
複製代碼

  • filtered列

filtered 是一個半分比的值,rows * filtered / 100 能夠估算出將要和 EXPLAIN 中前一個表進行鏈接的行數(前一個表指 EXPLAIN 中的 id 值比當前表 id 值小的表)。

趁熱打鐵來波實踐

準備表和數據

DROP TABLE IF EXISTS `employees`;
CREATE TABLE `employees` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) NOT NULL DEFAULT '0' COMMENT '年齡',
  `position` varchar(20) NOT NULL DEFAULT '' COMMENT '職位',
  `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時間',
  PRIMARY KEY (`id`),
  KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='員工記錄表';

INSERT INTO employees(name,age,position,hire_time) VALUES('小明',22,'JAVA',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('李雷', 23,'Python',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('韓梅梅',23,'JS',NOW());
複製代碼

如上,nameageposition複合索引

全值匹配

EXPLAIN SELECT * FROM employees WHERE NAME= '小明';
複製代碼

EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age = 22;
複製代碼

EXPLAIN SELECT * FROM employees WHERE NAME= '小明' AND age = 22 AND POSITION ='JAVA';
複製代碼

如上, 三條 SQL 都用到了索引

最佳左前綴法則

EXPLAIN SELECT * FROM employees WHERE NAME = '李磊';
複製代碼

如圖,能夠看到用到了索引


EXPLAIN SELECT * FROM employees WHERE POSITION = 'Python';
複製代碼

如圖,能夠看到沒有用索引


EXPLAIN SELECT * FROM employees WHERE age = 23 AND POSITION ='Python';
複製代碼

如圖,能夠看到也沒有用索引

總結: 若是索引了多列,要遵照最左前綴法則。指的是查詢從索引的最左前列開始而且不跳過索引中的列。

不在索引列上作任何操做

例如 計算、函數、(自動or手動)類型轉換等,會致使索引失效而轉向全表掃描

EXPLAIN SELECT * FROM employees WHERE left(NAME,1) = '韓';
複製代碼

如圖,能夠看到沒有用索引

不能使用索引中範圍條件右邊的列

EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age = 22 AND position ='JAVA';
經過上邊實踐,知道該語句用到了索引
複製代碼
EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age > 22 AND position ='JAVA';
複製代碼

咱們能夠計算下 key_len 長度,能夠得知 索引只用到了 nameage, position 沒用用到索引,因此在複合索引中使用了範圍條件右邊的列索引會失效。

使用覆蓋索引

只訪問索引的查詢(索引列包含查詢列),減小 SELECT * 語句

EXPLAIN SELECT * FROM employees WHERE name= '小明' AND age = 22 AND POSITION ='Java';
複製代碼

EXPLAIN SELECT NAME, age, POSITION FROM employees WHERE NAME= '小明' AND age = 22 AND POSITION ='Java';
複製代碼

能夠看到,從 NULL 變成了 Using index

使用不等於(!=或者<>)時索引失效

EXPLAIN SELECT * FROM employees WHERE NAME != '小明'
複製代碼

IS NULL,IS NOT NULL 也沒法使用索引

EXPLAIN SELECT * FROM employees WHERE name IS NOT NULL
複製代碼

LIKE 以通配符開頭索引會失效

EXPLAIN SELECT * FROM employees WHERE name LIKE '%李'
複製代碼

  • 通配符放結尾索引不會失效
EXPLAIN SELECT * FROM employees WHERE name LIKE '李%'
複製代碼

  • 或者使用覆蓋索引,查詢字段必須是創建覆蓋索引字段
EXPLAIN SELECT NAME, age, position FROM employees WHERE name LIKE '%李'
複製代碼

注:當覆蓋索引指向的字段是 varchar(380) 及 380 以上的字段時,覆蓋索引會失效!

字符串不加單引號索引失效

EXPLAIN SELECT * FROM employees WHERE NAME = 1000;
複製代碼

少用or,用它鏈接不少狀況下索引會失效

EXPLAIN SELECT * FROM employees WHERE name = '李磊' OR NAME = '韓梅梅';
複製代碼

總結

假設 a、b、c 爲複合索引

WHERE 語句 索引使用狀況
WHERE a = '小明' 使用到 a
WHERE a = '小明' AND b = '李磊' 使用到 a 、b
WHERE a = '小明' AND b = '李磊' AND c = '韓梅梅' 使用到 a、b、c
WHERE b = '李磊' 或者 WHERE b = '李磊' AND c = '韓梅梅' 或者 WHERE c = '韓梅梅' 沒有用到
WHERE a = '小明' AND c = '韓梅梅' a 用到了,c 沒有用到,由於 b 中間斷了
WHERE a = '小明' AND b > '李磊' AND c = '韓梅梅' a、b 用到了,c 不能用在範圍後
WHERE a = '小明' AND b = '李磊%' AND c = '韓梅梅' 使用到 a、b、c
WHERE a = '小明' AND b = '%李磊' AND c = '韓梅梅' 只用到 a
WHERE a = '小明' AND b = '%李磊%' AND c = '韓梅梅' 只用到 a
WHERE a = '小明' AND b = '李%磊%' AND c = '韓梅梅' 使用到 a、b、c
相關文章
相關標籤/搜索