作者:IT王小二
一條查詢 SQL 語句為什麼會執行很慢?執行的時候走了什麼索引?多表查詢的時候 SQL 大體執行流程是怎麼樣的?Explain 執行計劃詳解給各位客官一一道來。
一、示例MySQL安裝方式和版本
首先需要 MySQL 安裝的客官看這兩篇,小二演示使用的是 Docker 的安裝方式:
小二安裝完的 MySQL 資料庫版本為 5.7.36
。
二、Explain工具簡介
通常我們在生產環境會碰到查詢速度比較慢的 SQL 語句,那麼怎麼知道 SQL 語句為什麼執行慢呢?這個時候就需要用到 Explain 執行計劃來分析我們的語句了。
通過使用 Explain 關鍵字可以模擬優化器執行 SQL 查詢語句,執行會返回執行計劃的資訊,而不是執行這條SQL,當然如果 from 中包含子查詢,仍會執行該子查詢,將結果放入臨時表中 。
總的來說,通過 Explain 從而知道 MySQL 是如何處理你的 SQL 語句的,分析查詢語句或是表結構的效能瓶頸。那麼通過 Explain 執行計劃可以知道:
- 表的讀取順序
- 資料讀取操作的操作型別
- 哪些索引可能被使用
- 哪些索引實際被使用
- 表之間的引用
- 每張表估計有多少行會被執行
三、本文用到的示例表
CREATE TABLE `author` (
`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 `author` (`id`, `name`, `update_time`) VALUES (1,'itwxe','2022-01-12 19:27:18'), (2,'admin','2022-01-12 19:27:18'), (3,'superAdmin','2022-01-12 19:27:18');
CREATE TABLE `blog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_title` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `blog` (`id`, `title`) VALUES (1,'blog1'),(2,'blog2'),(3,'blog3');
CREATE TABLE `author_blog` (
`id` int(11) NOT NULL,
`author_id` int(11) NOT NULL,
`blog_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_author_blog_id` (`author_id`,`blog_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `author_blog` (`id`, `author_id`, `blog_id`) VALUES (1,1,1),(2,2,1),(3,1,2);
四、Explain的變種
Explain 的語法非常簡單,只需要在查詢語句前面加一個 EXPLAIN
關鍵字即可。
explain select * from author where id = 1;
如果你發現你的版本少了 partitions
或者 filtered
列欄位資訊的話,那是因為在 MySQL5.7 的某個小版本之前的版本是沒有這兩個欄位的。rows * filtered/100
可以估算出將要和 explain 中前一個表進行連線的行數(前一個表指 explain 中的 id 值比當前表 id 值小的表)。
那麼低版本想要看到這兩個欄位也是有辦法的,這就是小二說的 Explain 的兩個變種。
- explain extended:會在低版本 MySQL explain 的基礎上額外提供一些查詢優化的資訊 filtered 列。
- explain partitions:會在低版本 MySQL explain 的基礎上額外提供一個 partitions 欄位,如果查詢是基於分割槽表的話,會顯示查詢將訪問的分割槽。
在 explain 緊隨其後執行的 show warnings
命令可以得到優化之後的查詢語句,從而看到優化器優化了什麼東西,當然優化後的 SQL 並不一定符合 SQL的語法,但是 MySQL 可以識別並可以執行。
五、Explain中的列
下面小二就來說說 Explain 中每個列的資訊,當然每個列的值只會介紹常見的幾個,如果沒提到各位小夥伴可以檢視 Explain 官方文件:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
1. id
id 列表示的編號是 select 的序列號,有幾個 select 就有幾個 id,並且 id 的順序是按 select 出現的順序增長的,id 列越大執行優先順序越高,id 相同則從上往下執行,id 為 NULL 最後執行。
explain select * from author where id = (select author_id from author_blog where id = 2);
可以看到子查詢 id 為 2,所以先執行子查詢再執行外層查詢。
2. select_type
select_type 列表示對應行查詢的型別。
1)SIMPLE:簡單查詢,不包含子查詢和 union。
explain select * from author where id = 1;
2)PRIMARY:複雜查詢中最外層的 select。
3)SUBQUERY:包含在 select 中的子查詢(不在 from 子句中)。
4)DERIVED:包含在 from 子句中的子查詢。MySQL 會將結果存放在一個臨時表中,也稱為派生表(derived的英文含義)。
用這個例子來了解 PRIMARY、SUBQUERY 和 DERIVED 型別。
# 關閉 MySQL5.7 新特性對衍生表的合併優化
set session optimizer_switch = 'derived_merge=off';
explain select (select 1 from blog where id = 1) from (select 1 from author where id = 1) tmp;
別問小二為啥寫這種語句,一切的一切都是為了演示效果啊~~~
開啟 MySQL5.7 新特性預設對衍生表的合併優化,就可以看到不一樣的效果了。
set session optimizer_switch = 'derived_merge=on';
5)UNION:在 union 中的第二個和隨後的 select。
6)UNION RESULT:從 union 臨時表檢索結果的 select。
用這個例子來了解 UNION、UNION RESULT 型別。
explain select id from author where id = 1 union select id from blog where id = 2;
3. table
table 列表示 explain 的一行正在訪問哪個表。
當 from 子句中有子查詢時,table 列是 <derivenN>
格式,表示當前查詢依賴 id=N 的查詢,於是先執行 id=N 的查詢。
當有 union 時,UNION RESULT 的 table 列的值為 <union1,2>
,1和2表示參與 union 的 select 行 id。
4. type
type 列表示關聯型別或訪問型別,即 MySQL 決定如何查詢表中的行,查詢資料行記錄的大概範圍。
依次從最優到最差分別為:system > const > eq_ref > ref > range > index > ALL ,一般來說,得保證查詢達到 range 級別,最好達到 ref 級別。
除了上面幾個擁有優先順序的值之外,NULL 表示 MySQL 能夠在優化階段分解查詢語句,在執行階段用不著再訪問表或索引。例如:在索引列中選取最小值,可以單獨查詢索引來完成,不需要在執行時訪問表。
explain select min(id) from author;
1)const、system:MySQL 能對查詢的某部分進行優化並將其轉化成一個常量(可以看 show warnings 的結果)。用於 primary key 或 unique key 的所有列與常數比較時,所以表最多有一個匹配行,讀取1次,速度比較快。system是const的特例,表裡只有一條資料匹配時為system
explain select * from (select * from author where id = 1) tmp;
show warnings;
2)eq_ref:primary key 或 unique key 索引的所有部分被連線使用 ,最多隻會返回一條符合條件的記錄。這可能是在 const 之外最好的聯接型別了,簡單的 select 查詢不會出現這種 type。
explain select * from author_blog left join blog on author_blog.blog_id = blog.id;
3)ref:相比 eq_ref,不使用唯一索引,而是使用普通索引或者聯合索引的部分字首,索引要和某個值相比較,可能會找到多個符合條件的行。
- . 簡單 select 查詢,title 是普通索引(非唯一索引)
explain select * from blog where title = 'blog1';
- 關聯表查詢,idx_author_blog_id 是 author_id 和 blog_id 的聯合索引,這裡使用到了 author_blog 的左邊字首 author_id 部分。
explain select author_id from author left join author_blog on author.id = author_blog.author_id;
4)range:範圍掃描通常出現在 in,between,>,<,>=,<=
等操作中,使用一個索引來檢索給定範圍的行。
explain select * from author where id > 1;
5)index:掃描全索引就能拿到結果,一般是掃描某個二級索引或者聯合索引,這種掃描不會從索引樹根節點開始快速查詢,而是直接對二級索引或者聯合索引的葉子節點遍歷和掃描,速度還是比較慢的,這種查詢一般為使用 覆蓋索引(索引覆蓋),二級索引一般比較小,所以這種通常比 ALL 快一些。
覆蓋索引(索引覆蓋)定義:首先得說明的是覆蓋索引不是一種索引型別,而是二級索引或者聯合索引就包含所需要查詢的所有欄位,不需要再 回表 進行查詢資料行獲取其它欄位值,這種情況一般可以說是用到了覆蓋索引。
當然為啥不需要再回表,相信看過小二 深入理解MySQL索引底層資料結構 這篇文章的都曉得的哈,不知道的小夥伴可以看看哦。
explain select * from blog;
6)ALL:即全表掃描,掃描你的聚簇索引的所有葉子節點。通常情況下這需要增加索引來進行優化了。
explain select * from author;
5. possible_keys
possible_keys 列表示查詢 可能使用 哪些索引來查詢,但是最終查詢可能不使用索引。
explain 時可能出現 possible_keys 列有可以使用的索引,但是 key 列顯示 NULL 的情況,這種情況是因為 MySQL 經過查詢成本計算,MySQL 認為索引對此查詢速度不如全表掃描,最終選擇了全表查詢。
如果該列是 NULL,則沒有相關的索引。在這種情況下,可以通過檢查 where 子句看是否可以創造一個適當的索引來提高查詢效能,然後用 explain 檢視效果。
6. key
key 列表示 MySQL 實際使用 哪個索引來對該表進行查詢。
如果沒有使用索引,則該列是 NULL。如果想強制 MySQL 使用或忽視 possible_keys 列中的索引,在查詢中使用 force index
(強制走某個索引)、ignore index
(強制不走某個索引)。
例如:blog 表強制走 idx_title 索引和強制不走 idx_title 索引。
7. key_len
key_ len 列表示 MySQL 在索引裡使用的位元組數,通過這個值可以算出具體使用了索引中的哪些列。
explain select * from author_blog where author_id = 1;
explain select * from author_blog where author_id = 1 and blog_id = 2;
在 author_blog 表中 idx_author_blog_id 是 author_id 和 blog_id 的聯合索引,author_id 和 blog_id 都為 int 型別,int 型別佔用 4 個位元組。通過 key_len=4 可以推斷出第一條 SQL 使用了 author_id 列,而第二條 SQL 中 key_len=8 可以對推斷出使用了 author_id 列和 blog_id 列。
key_lenth 計算機規則:
-
字串,char(n) 和 varchar(n),5.0.3以後版本中,n 均代表字元數,而不是位元組數,如果是 utf-8,一個數字 或字母佔1個位元組,一個漢字佔3個位元組。
- char(n):如果存漢字長度就是
3 * n
位元組。 - varchar(n):如果存漢字則長度是
3 * n + 2
位元組。因為 varchar 是變長字串,加的2位元組用來儲存字串長度。
- char(n):如果存漢字長度就是
-
數值型別
- tinyint:1位元組
- smallint:2位元組
- int:4位元組
- bigint:8位元組
-
時間型別
- date:3位元組
- timestamp:4位元組
- datetime:8位元組
-
如果欄位允許為 NULL,需要1位元組記錄是否為 NULL。例如當允許 char(n) 為 NULL 時,存漢字長度為
3 * n + 1
。
索引最大長度是768位元組,當字串過長時,mysql會做一個類似左字首索引的處理,將前半部分的字元提取出來做索引。
8. ref
ref 列表示在 key 列記錄的索引中,表查詢值所用到的列或常量,常見的有:const(常量),欄位名(例如film.id)。
9. rows
rows 列表示 MySQL 估計要讀取並檢測的行數,需要注意的是,是估計值,並非最後結果集裡的行數。
10. Extra
Extra 列表示的是一些額外資訊,常見重要值如下:
1)Using index:使用覆蓋索引。
explain select blog_id from author_blog where author_id = 2;
2)Using where:使用 where 語句來處理結果,並且查詢的列未被索引覆蓋。
explain select * from author where name = 'itwxe';
3)Using index condition:查詢的列不完全被索引覆蓋,需要回表查詢。
explain select * from author_blog where author_id > 1;
4)Using temporary:MySQL 需要建立一張臨時表來處理查詢。出現這種情況一般是要進行優化的,首先是想到用索引來優化。
- author.name 沒有索引,此時建立了一張臨時表來 distinct。
explain select distinct name from author;
- blog.title 建立了 idx_title 索引,此時查詢是 extra 是 Using index 而不是 Using temporary。
explain select distinct title from blog;
5)Using filesort:將用外部排序而不是索引排序,需要注意的是不要被這個 Using filesort 名字欺騙了,並非出現這個就會使用磁碟排序,而是資料較小時從記憶體排序,否則需要在磁碟排序。這種情況下一般也是要考慮使用索引來優化的。
- author.name 沒有索引,會查詢整個 author 表,並儲存排序關鍵字和行指標,然後排序關鍵字並按順序檢索行資訊。
explain select * from author order by name;
- blog.title 建立了 idx_title 索引,此時查詢時 extra 是 using index。
explain select * from blog order by title;
6)Select tables optimized away:使用某些聚合函式(比如 max、min)來訪問存在索引的某個欄位時出現。
explain select min(id) from author;
說到這裡 Explain 執行計劃的每個列和常見的值都介紹完了,小夥伴們下期見~~~
都讀到這裡了,來個 點贊、評論、關注、收藏 吧!