數據庫是程序員必備的一項基本技能,基本每次面試必問。對於剛出校門的程序員,你只要學會如何使用就行了,但越往後工作越發現,僅僅會寫sql語句是萬萬不行的。寫出的sql,如果性能不好,達不到要求,可能會阻塞整個系統,那對於整個系統來講是致命的。
所以如何判斷你的sql寫的好不好呢?畢竟只有先知道sql寫的好不好,才能再去考慮如何優化的問題。
MySQL官方就給我們提供了很多sql分析的工具,這裏我們主要說一下EXPLAIN。
以下是基於MySQL5.7.28版本進行分析的,不同版本之間略有差異。
1.1 概念
使用EXPLAIN關鍵字可以模擬優化器執行sql語句,從而知道MySQL是如何處理你的語句,分析你的查詢語句或者表結構的性能瓶頸。
用法:EXPLAIN+ sql語句
EXPLAIN執行後返回的信息如下:
各個字段的大致含義如下:
- id: SELECT 查詢的標識符. 每個 SELECT 都會自動分配一個唯一的標識符.
- select_type: SELECT 查詢的類型.
- table: 查詢的是哪個表
- partitions: 匹配的分區
- type: join 類型
- possible_keys: 此次查詢中可能選用的索引
- key: 此次查詢中確切使用到的索引.
- key_len: 查詢優化器使用了索引的字節數.
- ref: 哪個字段或常數與 key 一起被使用
- rows: 顯示此查詢一共掃描了多少行. 這個是一個估計值.
- filtered: 表示此查詢條件所過濾的數據的百分比
- extra: 額外的信息
1.2 準備工作
新建一個數據庫test,執行下面的sql語句
CREATE TABLE t1(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t2(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t3(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t4(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000)));
INSERT INTO t2(content) VALUES(CONCAT('t2_',FLOOR(1+RAND()*1000)));
INSERT INTO t3(content) VALUES(CONCAT('t3_',FLOOR(1+RAND()*1000)));
INSERT INTO t4(content) VALUES(CONCAT('t4_',FLOOR(1+RAND()*1000)));
下面一一解釋各列的含義。
1.3 id
select查詢的序列號,包含一組數字,表示查詢中執行select子句的順序或操作表的順序。大致分爲下面幾種情況
(1)id相同,執行順序由上至下
上面的查詢語句,三個id都爲1,具有相同的優先級,執行順序由上而下,具體執行順序由優化器決定,這裏執行順序爲t1,t2,t3。
(2)id不同,數字越大優先級越高
如果sql中存在子查詢,那麼id的序號會遞增,id越大越先被執行。如上圖,執行順序是t3、t1、t2,也就是說,最裏面的子查詢最先執行,由裏往外執行。
在我測試的時候,無意中發現,下面的語句,一個使用的是IN關鍵字,一個使用的=運算符,但使用EXPLAIN執行後,結果天壤之別。
這說明使用IN嵌套子查詢,它是按順序來執行的,也就是說每執行一次最外層子查詢,裏面的子查詢都會被重複執行,這好像和我的理解差很多啊(我一直以爲是先執行最裏面的子查詢,再執行外面的)。
具體可以看看這篇文章,我覺得講的大概算是明白了。https://segmentfault.com/a/1190000005742843。這裏就不再繼續贅述了。
千萬別用IN,使用JOIN或者EXISTS代替它
(3)id存在相同的和不同的
在上面語句的基礎上,增加一個IN的子查詢,執行結果如下
執行順序爲t3、t1、t2、t4。值越大的越先執行,相同值的從上往下執行。
1.4 select_type
select_type表示查詢的類型,主要是爲了區分普通查詢、子查詢、聯合查詢等複雜查詢。分爲以下幾種類型:
(1)SIMPLE
簡單的select查詢,查詢中不包含子查詢或者UNION。
(2)PRIMARY
查詢中若包含任何複雜的子查詢,那麼最外層的查詢被標記爲PRIMARY。
(3)DERIVED
在from子句中包含的子查詢被標記爲DERIVED(衍生),MySQL會遞歸執行這些子查詢,把結果放在臨時表中。
(4)SUBQUERY
在select或where子句中包含了子查詢,該子查詢被標記爲SUBQUERY。
(5)UNION
若第二個select查詢語句出現在UNION之後,則被標記爲UNION。若UNION包含在FROM子句的子查詢中,外層SELECT將被標記爲:DERIVED
(6)UNION RESULT
從UNION表獲取結果的SELECT。
上面的前三種在上一小節已經出現過了,看看後面這三種
可以看到id列出現了一個NULL,這是上面沒講到的。一般來說,特殊情況下,如果某行語句引用了其他多行結果集的並集,則該值可以爲 NULL。
1.5 table
這個沒啥好講的,表示這個查詢是基於哪種表的。並不一定是真實存在的表,比如上面出現的DERIVED和<union1,2>,一般來說會出現下面的取值:
(1)<union a,b>:輸出結果中編號爲 a 的行與編號爲 b 的行的結果集的並集。
(2)<derived a>:輸出結果中編號爲 a 的行的結果集,derived 表示這是一個派生結果集,如 FROM 子句中的查詢。
(3)<subquery a>:輸出結果中編號爲 a 的行的結果集,subquery 表示這是一個物化子查詢。
1.6 partitions
查詢時匹配到的分區信息,對於非分區表值爲NULL
,當查詢的是分區表時,partitions
顯示分區表命中的分區情況。
根據官方文檔,在創建表的時候,指定不同分區存放的id值範圍不同。
插入測試數據,讓id值分佈在四個分區內。
執行查詢輸出結果。
1.7 type
type是查詢的訪問類型,是較爲重要的一個指標,性能從最好到最壞依次是 system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL。
一般來說,得保證查詢至少到達range級別,最好能達到ref。
(1)system
當表僅存在一行記錄時(系統表),數據量很少,速度很快,這是一種很特殊的情況,不常見。
(2)const
當你的查詢條件是一個主鍵或者唯一索引(UNION INDEX)並且值是常量的時候,查詢速度非常快,因爲只需要讀一次表。
給t1表的content列增加一個唯一索引
(3)**eq_ref **
除了system和const,性能最好的就是eq_ref了。唯一性索引掃描,對於每個索引鍵,表中只有一條記錄與之匹配。常見於主鍵或唯一索引掃描。
(4)ref
非唯一性索引掃描,返回匹配某個單獨值的所有行。區別於eq_ref,ref表示使用除PRIMARY KEY
和UNIQUE
index 之外的索引,即非唯一索引,查詢的結果可能有多個。可以使用 = 運算符或者<=> 運算符。
在t2表的content列加上普通索引
進行查詢
(5)fulltext
查詢時使用 fulltext 索引。
(6)ref_or_null
對於某個字段既需要關聯條件,也需要null 值的情況下。查詢優化器會選擇用ref_or_null 連接查詢。
(7)index_merge
在查詢過程中需要多個索引組合使用,通常出現在有or 關鍵字的sql 中。
(8)unique_subquery
該聯接類型類似於index_subquery。子查詢中的唯一索引。在某些in子查詢裏,用於替換eq_ref,比如下面的查詢語句
value IN (SELECT primary_key FROM single_table WHERE some_expr)
(9)index_subquery
[圖片上傳中...(19.png-e55cec-1621069077487-0)]
利用索引來關聯子查詢,不再全表掃描。用於非唯一索引,子查詢可以返回重複值。類似於unique_subquery,但用於非唯一索引
value IN (SELECT key_column FROM single_table WHERE some_expr)
(10)range
只檢索給定範圍的行,使用一個索引來選擇行。key 列顯示使用了哪個索引,一般就是在你的where 語句中出現
了between、<、>、in 等的查詢,這種範圍掃描索引掃描比全表掃描要好,因爲它只需要開始於索引的某一點,而
結束語另一點,不用掃描全部索引。
舉個例子,t3表中id字段爲主鍵,有PRIMARY索引,content字段沒有建立索引,查詢時使用id作爲條件,結果如下
使用content作爲條件,結果如下
所以,只有對設置了索引的字段,做範圍檢索 type
纔是 range
。
(11)index
sql語句使用了索引,但沒有通過索引進行過濾,一般是使用了覆蓋索引或者利用索引進行了排序分組。
index和ALL都是讀全表,區別在於index是遍歷索引樹讀取,ALL是從硬盤讀取。index通常比ALL更快,因爲索引文件通常比數據文件小。
舉個例子,查詢t3表主鍵id,結果如下
(12)ALL
全表掃描,性能最差。
1.8 possible_keys
查詢時可能使用的索引,一個或多個。查詢涉及到的字段上若存在索引,則該索引將被列出。注意是可能,實際查詢時不一定會用到。
1.9 key
查詢時實際使用的索引,沒有使用索引則爲NULL。查詢時若使用了覆蓋索引,則該索引只出現在key字段中。
舉個例子,trb1表中有一個組合索引(age, name),那麼當你的查詢列和索引的個數和順序一致時,查詢結果如下:
1.10 key_len
表示索引中使用的字節數,可通過該列計算查詢中使用的索引的長度。在不損失精確性的情況下,長度越短越好。
key_len顯示的值是索引字段可能的最大長度,並非實際使用長度,即key_len是根據表定義計算得到,不是通過表內檢索。
key_len 字段能夠幫你檢查是否充分的利用上了索引。ken_len 越長,說明索引使用的越充分。
注意:key_len
只計算where
條件中用到的索引長度,而排序和分組即便是用到了索引,也不會計算到key_len
中。
舉個例子,有表trb1,存在以下字段,以及一個組合索引idx_age_name
下面查詢語句的執行結果
key_len的值爲153、158、null。如何計算:
①先看索引上字段的類型+長度。比如int=4 ; varchar(50) = 50 ; char(50) = 50。
②如果是varchar 或者char 這種字符串字段,視字符集要乘不同的值,比如utf-8 要乘3,GBK 要乘2。
③varchar 這種動態字符串要加2 個字節。
④允許爲空的字段要加1 個字節。
第一條:key_len = name的字節長度 = 50 * 3 + 2 + 1 = 153
第二條:key_len = age 的字節長度 + name 的字節長度= 4 +1 + ( 50*3 + 2 + 1)= 5 + 153 = 158。(使用的索引更充分,查詢結果更精確,但消耗更大)
第三條:索引失效了。
1.11 ref
顯示索引的哪一列被使用了,常見的取值有:const, func,null,字段名。
- 當使用常量等值查詢,顯示
const
, - 當關聯查詢時,會顯示相應關聯表的
關聯字段
- 如果查詢條件使用了
表達式
、函數
,或者條件列發生內部隱式轉換,可能顯示爲func
- 其他情況
null
舉個例子,t3表的content字段有普通索引,下面的查詢語句結果如下
1.12 rows
rows 列表示 MySQL 認爲它執行查詢時可能需要讀取的行數,一般情況下這個值越小越好!
1.13 filtered
filtered
是一個百分比的值,表示符合條件的記錄數的百分比。簡單點說,這個字段表示存儲引擎返回的數據在經過過濾後,剩下滿足條件的記錄數量的比例。
在MySQL.5.7
版本以前想要顯示filtered
需要使用explain extended
命令。MySQL.5.7
後,默認explain
直接顯示partitions
和filtered
的信息。
1.14 Extra
其他額外的信息。
(1)Using filesort
說明mysql 會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取。MySQL 中無法利用索引完成的排序操作稱爲“文件排序”。
舉個例子,trb1表建立一個組合索引
下面的查詢出現filesort :
按照組合索引的順序,是name、age、purchased,而上面的查詢語句,沒有使用中間的age,所以在order by的時候索引失效了。通常這種情況是需要進行優化的。
修改一下上面的sql語句,讓索引不失效。
(2)Using temporary
使了用臨時表保存中間結果,MySQL 在對查詢結果排序時使用臨時表。常見於排序order by 和分組查詢group by。
這條sql語句用了臨時表,又用了文件排序,在數據量非常大的時候效率是很低的,需要進行優化。
所以在使用group by 和 order by的時候,列的數量和順序儘量和索引的一樣。
(3)Using index
Using index 表示相應的select 操作中使用了覆蓋索引(Covering Index),避免訪問了表的數據行,可以提高效率。
如果同時出現using where,表明索引被用來執行索引鍵值的查找。如果沒有同時出現using where,表明索引只是用來讀取數據而非利用索引執行查找。
還是使用上面的trb1表舉例子
只出現了Using index,說明索引用來讀取數據而不是執行查找。
出現了Using where,說明索引被用來執行查找。
(4)Using where
表示查詢時有索引被用來進行where過濾。
(5)Using join buffer
查詢時使用了連接緩存。
(6)impossible where
查詢語句的where條件總是爲false,舉個例子
一般情況下不會出現這種。
關於Extra字段,有很多取值,這裏就不一一列舉了,具體可以看官方文檔。
參考資料:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html