作者: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 執行計划的每個列和常見的值都介紹完了,小伙伴們下期見~~~
都讀到這里了,來個 點贊、評論、關注、收藏 吧!