本文已經收錄到github倉庫,倉庫用於分享Java相關知識總結,包括Java基礎、MySQL、Springboot、mybatis、Redis、rabbitMQ等等,歡迎大家提pr和star!
簡介
本文主要講述如何通過 explain 命令獲取 select 語句的執行計劃,通過 explain 可以知道 select 語句以下資訊:
- 表的載入順序
- sql 的查詢型別
- 可能用到哪些索引,實際上用到哪些索引
- 讀取的行數
- ...
explain 執行計劃包含欄位資訊如下:分別是 id
、select_type
、table
、partitions
、type
、possible_keys
、key
、key_len
、ref
、rows
、filtered
、Extra
12個欄位。
通過explain extended + show warnings可以在原本explain的基礎上額外提供一些查詢優化的資訊,得到優化以後的可能的查詢語句(不一定是最終優化的結果)。
先搭建測試環境:
CREATE TABLE `blog` (
`blog_id` int NOT NULL AUTO_INCREMENT COMMENT '唯一博文id--主鍵',
`blog_title` varchar(255) NOT NULL COMMENT '博文標題',
`blog_body` text NOT NULL COMMENT '博文內容',
`blog_time` datetime NOT NULL COMMENT '博文釋出時間',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`blog_state` int NOT NULL COMMENT '博文狀態--0 刪除 1正常',
`user_id` int NOT NULL COMMENT '使用者id',
PRIMARY KEY (`blog_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8
CREATE TABLE `user` (
`user_id` int NOT NULL AUTO_INCREMENT COMMENT '使用者唯一id--主鍵',
`user_name` varchar(30) NOT NULL COMMENT '使用者名稱--不能重複',
`user_password` varchar(255) NOT NULL COMMENT '使用者密碼',
PRIMARY KEY (`user_id`),
KEY `name` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8
CREATE TABLE `discuss` (
`discuss_id` int NOT NULL AUTO_INCREMENT COMMENT '評論唯一id',
`discuss_body` varchar(255) NOT NULL COMMENT '評論內容',
`discuss_time` datetime NOT NULL COMMENT '評論時間',
`user_id` int NOT NULL COMMENT '使用者id',
`blog_id` int NOT NULL COMMENT '博文id',
PRIMARY KEY (`discuss_id`)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8
id
表示查詢中執行select子句或者操作表的順序,id
的值越大,代表優先順序越高,越先執行。
explain select discuss_body
from discuss
where blog_id = (
select blog_id from blog where user_id = (
select user_id from user where user_name = 'admin'));
三個表依次巢狀,發現最裡層的子查詢 id
最大,最先執行。
select_type
表示 select
查詢的型別,主要是用於區分各種複雜的查詢,例如:普通查詢
、聯合查詢
、子查詢
等。
- SIMPLE:表示最簡單的 select 查詢語句,在查詢中不包含子查詢或者交併差集等操作。
- PRIMARY:查詢中最外層的SELECT(存在子查詢的外層的表操作為PRIMARY)。
- SUBQUERY:子查詢中首個SELECT。
- DERIVED:被驅動的SELECT子查詢(子查詢位於FROM子句)。
- UNION:在SELECT之後使用了UNION。
table
查詢的表名,並不一定是真實存在的表,有別名顯示別名,也可能為臨時表。當from子句中有子查詢時,table列是 <derivenN>
的格式,表示當前查詢依賴 id為N的查詢,會先執行 id為N的查詢。
partitions
查詢時匹配到的分割槽資訊,對於非分割槽表值為NULL
,當查詢的是分割槽表時,partitions
顯示分割槽表命中的分割槽情況。
type
查詢使用了何種型別,它在 SQL
優化中是一個非常重要的指標。
system
當表僅有一行記錄時(系統表),資料量很少,往往不需要進行磁碟IO,速度非常快。比如,Mysql系統表proxies_priv在Mysql服務啟動時候已經載入在記憶體中,對這個表進行查詢不需要進行磁碟 IO。
const
單表操作的時候,查詢使用了主鍵或者唯一索引。
eq_ref
多表關聯查詢的時候,主鍵和唯一索引作為關聯條件。如下圖的sql,對於user表(外迴圈)的每一行,user_role表(內迴圈)只有一行滿足join條件,只要查詢到這行記錄,就會跳出內迴圈,繼續外迴圈的下一輪查詢。
ref
查詢條件列使用了索引而且不為主鍵和唯一索引。雖然使用了索引,但該索引列的值並不唯一,這樣即使使用索引查詢到了第一條資料,仍然不能停止,要在目標值附近進行小範圍掃描。但它的好處是不需要掃全表,因為索引是有序的,即便有重複值,也是在一個非常小的範圍內做掃描。
ref_or_null
類似 ref,會額外搜尋包含NULL
值的行。
index_merge
使用了索引合併優化方法,查詢使用了兩個以上的索引。新建comment表,id為主鍵,value_id為非唯一索引,執行explain select content from comment where value_id = 1181000 and id > 1000;
,執行結果顯示查詢同時使用了id和value_id索引,type列的值為index_merge。
range
有範圍的索引掃描,相對於index的全索引掃描,它有範圍限制,因此要優於index。像between、and、'>'、'<'、in和or都是範圍索引掃描。
index
index包括select索引列,order by主鍵兩種情況。
order by主鍵。這種情況會按照索引順序全表掃描資料,拿到的資料是按照主鍵排好序的,不需要額外進行排序。
select索引列。type為index,而且extra欄位為using index,也稱這種情況為索引覆蓋。所需要取的資料都在索引列,無需回表查詢。
all
全表掃描,查詢沒有用到索引,效能最差。
possible_keys
此次查詢中可能選用的索引。但這個索引並不定一會是最終查詢資料時所被用到的索引。
key
此次查詢中確切使用到的索引。
rows
估算要找到所需的記錄,需要讀取的行數。評估SQL
效能的一個比較重要的資料,mysql
需要掃描的行數,很直觀的顯示 SQL
效能的好壞,一般情況下 rows
值越小越好。
filtered
儲存引擎返回的資料在經過過濾後,剩下滿足條件的記錄數量的比例。
extra
表示額外的資訊說明。為了方便測試,這裡新建兩張表。
CREATE TABLE `t_order` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int DEFAULT NULL,
`order_id` int DEFAULT NULL,
`order_status` tinyint DEFAULT NULL,
`create_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_userid_order_id_createdate` (`user_id`,`order_id`,`create_date`)
) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8
CREATE TABLE `t_orderdetail` (
`id` int NOT NULL AUTO_INCREMENT,
`order_id` int DEFAULT NULL,
`product_name` varchar(100) DEFAULT NULL,
`cnt` int DEFAULT NULL,
`create_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_orderid_productname` (`order_id`,`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=152 DEFAULT CHARSET=utf8
using where
查詢的列未被索引覆蓋,where篩選條件非索引的前導列。對儲存引擎返回的結果進行過濾(Post-filter,後過濾),一般發生在MySQL伺服器,而不是儲存引擎層。
using index
查詢的列被索引覆蓋,並且where篩選條件符合最左字首原則,通過索引查詢就能直接找到符合條件的資料,不需要回表查詢資料。
Using where&Using index
查詢的列被索引覆蓋,但無法通過索引查詢找到符合條件的資料,不過可以通過索引掃描找到符合條件的資料,也不需要回表查詢資料。
包括兩種情況:
where篩選條件不符合最左字首原則
where篩選條件是索引列前導列的一個範圍
null
查詢的列未被索引覆蓋,並且where篩選條件是索引的前導列,也就是用到了索引,但是部分欄位未被索引覆蓋,必須回表查詢這些欄位,Extra中為NULL。
using index condition
索引下推(index condition pushdown,ICP),先使用where條件過濾索引,過濾完索引後找到所有符合索引條件的資料行,隨後用 WHERE 子句中的其他條件去過濾這些資料行。
不使用ICP的情況(set optimizer_switch='index_condition_pushdown=off'
),如下圖,在步驟4中,沒有使用where條件過濾索引:
使用ICP的情況(set optimizer_switch='index_condition_pushdown=on'
):
下面的例子使用了ICP:
explain select user_id, order_id, order_status
from t_order where user_id > 1 and user_id < 5\G;
關掉ICP之後(set optimizer_switch='index_condition_pushdown=off'
),可以看到extra列為using where,不會使用索引下推。
using temporary
使用了臨時表儲存中間結果,常見於 order by 和 group by 中。典型的,當group by和order by同時存在,且作用於不同的欄位時,就會建立臨時表,以便計算出最終的結果集。
filesort
檔案排序。表示無法利用索引完成排序操作,以下情況會導致filesort:
- order by 的欄位不是索引欄位
- select 查詢欄位不全是索引欄位
- select 查詢欄位都是索引欄位,但是 order by 欄位和索引欄位的順序不一致
using join buffer
Block Nested Loop,需要進行巢狀迴圈計算。兩個關聯表join,關聯欄位均未建立索引,就會出現這種情況。比如內層和外層的type均為ALL,rows均為4,需要迴圈進行4*4次計算。常見的優化方案是,在關聯欄位上新增索引,避免每次巢狀迴圈計算。
本文參考了一些優秀的部落格,感興趣的可以瞭解下:
碼字不易,如果本文寫的不錯,可以點個贊,讓我知道,支援我寫出更好的文章!