本文是翻譯而來的,原文地址:EXPLAIN Output Format
explain語句會爲我們顯示select語句的執行計劃相關的信息。
每個在select語句中用到的table,explain語句都會爲其返回一行信息,而且這些table的順序是按照MYSQL查詢時的順序輸出的。MYSQL使用內嵌循環的方式來解決所有連表。這意味着,MYSQL會先從第一張表中讀取一行數據,然後去第二張表中尋找匹配的行,然後以此類推。當所有的table都處理後,MYSQL輸出查詢的字段,然後通過explain輸出的數據表列表回溯,直到它找到一個匹配了最多行的表。下一行從該表開始讀取直到處理下一張表。
如果使用了extended關鍵字,那麼explain會產生額外的信息,這些信息可以使用show warnings語句來查看。(注意:在explain語句中,不能同時使用extended和partitions這兩個關鍵字)
一、explain的輸出字段
這一小節介紹explain輸出的各個字段的含義,下一小節會更加詳細的介紹type和extra字段。
explain輸出的每一行都對應的select查詢所用到的table。每一行的字段和每個字段的含義如下表所示:
字段 | 含義 |
---|---|
id | 查詢的標識 |
select_type | 查詢的類型 |
table | 該行信息對應的table |
partitions | 匹配到的分區 |
type | 連接類型 |
possible_keys | 可以選擇使用的索引 |
key | 實際使用的索引 |
key_len | 實際使用的索引的長度 |
ref | 與索引對照的字段 |
rows | 預估將要掃描的行 |
filtered | 通過條件過濾的行數的佔比 |
extra | 額外信息 |
id
查詢標識,它是一系列連續的數字。但如果該行表示的是從其他聯合查詢返回的結果,那麼它的值將會是null,而且該行的table字段的值會是這個樣子:<unionM,N>,它表示,改行是從id爲M和N的聯合查詢中返回的結果。
select_type
查詢的類型,它的值有如下幾種:
select_type的值 | 該值的含義 |
---|---|
SIMPLE | 簡單的查詢,沒有使用UNION或者子查詢 |
PRIMARY | 最外層的查詢 |
UNION | 在UNION語句中的第二層或者之後的查詢 |
DEPENDENT UNION | UNION語句中的第二層或者之後的查詢,取決於對外的查詢 |
UNION RESULT | UNION查詢的結果 |
SUBQUERY | 第一層子查詢 |
DEPENDENT SUBQUERY | 第一層子查詢,取決於對外的查詢 |
DERIVED | 衍生的查詢 |
UNCACHEABLE SUBQUERY | 不可緩存的子查詢 |
UNCACHEABLE UNION | UNION語句的第二層查詢的select_type是UNCACHEABLE SUBQUERY |
DEPENDENT SUBQUERY不同於UNCACHEABLE SUBQUERY。對於DEPENDENT SUBQUERY來說,只有外層的值發生改變時,纔會重新查詢,並返回結果。而UNCACHEABLE SUBQUERY,外層的每一行,都會導致它重新執行查詢。
可緩存的子查詢並不是意味着把查詢結果緩存在緩存池中。子查詢的結果緩存只會發生在查詢期間,當查詢結束後,子查詢在緩存中的數據就會被銷燬。
table
該行所對應的表名稱。但也有可能是如下值:
<unionM,N>:從union語句的結果中查詢,這些union語句的id是從M到N;
<derivedN>:該行是從id爲N的查詢結果中衍生出來的。
partitions
被匹配到的分區。該字段只有當使用關鍵字 PARTITIONS時,纔會出現。對於非分區表,該字段的值是NULL。
type
連接類型。這塊內容詳見下一小節。
possible_keys
該字段顯示哪些索引是本次查詢可以使用的。需要注意的是:該字段完全取決於所在行在explain輸出中的順序。這就意味着,有些索引雖然在該字段中顯示了,但在實際的查詢中,並不會用得上。
如果這個字段的值爲NULL,那就意味着沒有相關的索引。這種情況下,你可能需要通過修改你的where子句,使用那些有建立索引的字段來匹配,以此來優化查詢。也可以自己建立一個新的索引,然後再用explain檢查一下。
可以使用 SHOW INDEX FROM tbl_name 語句來查看錶中有哪些索引。
key
該字段顯示的是在查詢中,實際使用的索引。如果mysql決定使用possible_keys中的一個索引來檢索數據,那麼該字段的值就會在possible_keys字段中。
也有可能,該字段會出現一個不在possible_keys中的值。這種情況一般出現在,possible_keys中的所有索引都不適合用於本次查詢,但存在一個索引,涵蓋了所有要查詢的字段,所以,雖然直接檢索這個索引,就可以得到想要的數據,索引檢索遠遠比數據檢索快得多。
如要要強制使用或者強制忽略某個在possible_keys中的索引,可以在查詢中使用 FORCE INDEX、USE INDEX、IGNORE INDEX來實現。
對於MYISAM或者NDB來說,執行ANALYZE TABLE可以幫助優化器選擇更好的索引。對於MYISAM表,執行myisamchk –analyze和執行ANALYZE TABLE是一樣的。
key_len
該字段顯示本次查詢,mysql使用的索引的長度。如果key字段的值是NULL ,那麼該字段的值也會是NULL 。注意的是:該字段可以幫助你判斷,在一次查詢中,使用了聯合索引中的幾個字段來檢索數據。
ref
該字段顯示哪些字段或者常數與key一起從表中選擇行。
rows
這字段顯示mysql覺得將要有多少行的數據需要被檢索。
對於Innodb表來說,這個字段的值是估算的,並不是總是精確的。
filtered
該字段顯示mysql預估被過濾掉的數據佔比。rows字段顯示預估的行數,rows * filtered / 100 表示的是有多少行數據和前一個表連表。這一字段只有在使用explain extended時才顯示。
extra
該字段顯示mysql在本次查詢中的額外的信息。在下下小節中,會詳細說明該字段的含義。
二、explain的連接類型
explain語句輸出結果中的type字段描述表與表之間是如何連接的。接下來會按從最優到最差列出所有的連接的類型:
system
該表中只有一行數據,這其實是const類型的一個特例。
const
該表中最多有一行數據匹配。由於只有一行數據,所以對於優化器來說,該行中的數據可以當成常量來處理。讀取常量表的數據,只需讀取一次,所以是很快的。
當你匹配主鍵中的所有字段或者唯一鍵中的所有字段時,就會使用const的連接類型。在下面的查詢中,tbl_name表可以被當做是常量表。
SELECT * FROM tbl_name WHERE primary_key=1;
SELECT * FROM tbl_name WHERE primary_key_part1=1 AND primary_key_part2=2;
eq_ref
對於每個來自於前面的表的行組合,從該表中讀取一行。除了system和const類型,這可能是最好的連接類型。當部分主鍵或者部分唯一建被使用時,連接類型就會是eq_ref。
當索引字段用等號(=)來匹配的時候,就會使用eq_ref類型。用來比較的值,可以是常數或者是其他表格的字段。以下列舉了幾個使用eq_ref的列子:
SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1;
ref
對於每個來自於前面的表的行組合,所有有匹配索引值的行將從這張表中讀取。如果聯接只使用鍵的最左邊的前綴,或如果鍵不是UNIQUE或PRIMARY KEY(換句話說,如果聯接不能基於關鍵字選擇單個行的話)則使用ref。如果使用的鍵僅僅匹配少量行,該聯接類型是不錯的。
當索引字段用等號(=)或者(<=>)來匹配的時候,就會使用ref類型。以下列舉了幾個使用ref的列子:
SELECT * FROM ref_table WHERE key_column=expr;
SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1;
fulltext
使用全文索引。
ref_or_null
這種連接類型與ref類似,但是mysql會額外判斷是否含有null的行。在子查詢中,優化器最經常使用這種連接類型。下面這個列子,mysql使用的就是ref_or_null
SELECT * FROM ref_table WHERE key_column=expr OR key_column IS NULL;
index_merge
這種類型表明優化器使用了索引合併。這種情況下,key字段的值會是一個列表,列出本次查詢使用的所有索引,key_len字段也會是一個列表,列出每個索引使用的長度。
unique_subquery
在某些子查詢中,會使用這種類型代替ref類型,如下面這種情況:
value IN (SELECT primary_key FROM single_table WHERE some_expr)
unique_subquery只是之中使用索引來代替子查詢,以此提高查詢效率的方法。
index_subquery
這種類型和unique_subquery有點類似。它也是用來代替子查詢,只是沒有唯一索引。如下列子:
value IN (SELECT key_column FROM single_table WHERE some_expr)
range
使用一個索引來檢索一個範圍的數據。key字段顯示哪個索引被使用,key_len顯示使用索引的長度,這種情況下,ref字段的值爲null。
當索引使用=, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, 或者 IN()來匹配時,使用range類型。如下列子:
SELECT * FROM tbl_name WHERE key_column = 10;
SELECT * FROM tbl_name WHERE key_column BETWEEN 10 and 20;
SELECT * FROM tbl_name WHERE key_column IN (10,20,30);
SELECT * FROM tbl_name WHERE key_part1= 10 AND key_part2 IN (10,20,30);
index
這種類型和all類型有點類似,只不過這種方式掃描的是索引樹。有兩種情況會產生這種類型:
- 如果要查詢的列都在一個索引中,那麼mysql將只掃描索引樹來獲取數據。這種情況下,extra字段會顯示”Using index”。index會比all類型快的原因在於,索引樹的大小一般比真實的數據表小。
- 雖然是一個全表掃描,但會按着索引樹的順序,先掃描索引再去表中獲取數據。這種情況下,extra字段不會顯示”Using index”。
all
對於每個來自於先前的表的行組合,進行一次全表掃描。如果表是第一個沒有標記爲const的表,那麼性能會比較差,而且在一般在所有的情況,性能都會極差。一般可以通過添加索引來避免產生all類型。
三、explain的extra字段信息
extra字段提供mysql在本次查詢中的額外信息。下面列出該字段可能會出現的值,如果你想讓你的查詢儘可能的快,一定要留心該字段中出現”Using filesort”和”Using temporary”。
* Child of ‘table’ pushed join@1*
在NDB引擎中,該表被當做是一個其他表的子表。只有在NDB引擎7.2.0版本後會出現。
const row not found
比如這類查詢:SELECT …… FROM tbl_name,然後表是空的。
Distinct
當mysql只需要查找唯一的值,如果它找到一行匹配的數據,它就會停止尋找這類數據。
Full scan on NULL key
在子查詢中,如果優化沒有使用索引來找出數據。
Impossible Having
having字段一直返回的是false,或者不能選擇任何行。
Impossible WHERE noticed after reading const tables
mysql把所有的讀完所有的常量表後發現where一直都是沒有作用的。
No matching min/max row
沒有相關的行來記錄這類查詢的數據:SELECT MIN(…) FROM … WHERE condition。
no matching row in const table
在一個連表查詢中 ,所連接的表是空表,或者該表中沒有匹配的數據。
No tables used
查詢沒有from字段。
Not exists
在本次查詢中,mysql可以執行left join,但根據left join的條件,爲當前的行組合查找到一條數據之後,就知道沒有其他匹配的數據了。以下給出一個可以被優化成這個方式的實例
SELECT * FROM t1 LEFT JOIN t2 ON t1.id=t2.id WHERE t2.id IS NULL;
假設t2.id被定義爲not null。在這種情況下,mysql先掃面t1,然後通過t1的id去t2中匹配數據。當mysql在t2中id與t1相等的行,但它知道t2的id將永遠不會是null,那麼mysql就不會繼續在t2表中需找與t1表id相同的行了。換句話說,每掃描一行t1的數據,當去t2中尋找匹配的數據時,mysql只需要找到一行id與t1相同的數據,就不需要繼續掃描了,不論t2表中有多少行的id與t1中相等。
Range checked for each record (index map : N)
mysql發現沒有一個好的索引可以使用,但是當前面表中的數據已知之後,有些索引是可以用的。這種情況下,mysql會爲前面表的每個行組合判斷是否可以使用rang或者index_merge的方法去匹配行數據。這種方式並不快,但是總比沒有使用索引的連表來得快。
索引是從1開始編號的,展示的順序和使用show index時是一樣的。索引映射值N是一個位掩碼錶明哪些索引會被考慮使用。舉個列子,如果值是0x19(二進制是11001),則表明,索引編號爲1、4和5會被考慮使用。
Scanned N databases
這表明當查詢表中數據時,有多少個目錄掃面了服務器執行,該值可以是0、1或者all。
Select tables optimized away
該查詢只包含聚合的函數(min()、max()),那些只是用一個索引就可以解決的,或者是count(*),而且沒有groub by字段。那麼優化器只需要返回一行數據。
Skip_open_table,Open_frm_only,Open_trigger_only,Open_full_table
這些值表明在表中查詢數據時,文件打開的優化選擇,這些值的含義如下:
- Skip_open_table:數據表對應的文件不需要被打開,它的信息已經變成變量包含檢索數據庫目錄的查詢中。
- Open_frm_only:只需要打開數據表對應的.frm文件就可以了。
- Open_trigger_only:只需要打開數據表對應的.TRG文件就可以了。
- Open_full_table:沒有優化的信息可以使用,表格對應的.frm、.MYD和.MYI文件都需要被打開。
unique row not found
對於這樣的查詢:select …… from tbl_name,沒有任何行可以滿足唯一索引或者主鍵的條件。
Using filesort
MySQL需要額外的一次操作,以找出如何按排序順序檢索行。這次排序是通過掃面所有的行,根據連接類型,先把那些符合刷選添加的行對應的key的值和行號存起來。然後排序這些key,排序完以後,安裝key的順序把行數據返回。
Using index
字段信息只需要從索引樹種獲取,而不需要做一次額外的查詢去讀取真實的行數據。當查詢中需要的字段都在索引中時,可以使用這種方法。
如果這時候Extra字段還顯示“Using where”,這意味着該索引被用來查找行數據對應的指針。如果沒有Using where ,優化器只需要讀取索引樹。舉個例子,如果索引覆蓋了要查詢的字段,那麼優化器則是掃描索引數,而不是用它來查詢數據。
對於InnoDB的表來說,它有用戶定義的聚合索引,這種索引即使Extra字段中沒有顯示“Using where”時,也會被使用。這種情況出現在,當type字段是index,且key字段是PRIMARY時。
Using index for group-by
獲取表數據的方式和Using index類似,Using index for group-by表明mysql發現有一個索引可以返回group by子句或者distinct子句的所有字段,這樣就不用爲存儲排序的key而額外掃描一次表。另外,對於分組操作,索引是最有效的方法,只有少數的索引需要被讀取。
Using join buffer
在join前面的表中的部分數據被存放到緩存中,這樣,在後面的表查詢中就可以從緩存中獲取數據。
Using sort_union(…),Using union(…),Using interesect(…)
當type爲index_merge時,這些信息表明索引查找出來的數據是如何合併的。
Using temporary
爲了執行本次查詢,mysql需要建一個臨時表來存放結果集。出現這種情況,有代表性的列子是,查詢中包含group by和order by子句,而且這兩個子句包含的字段是不同的。
Using where
WHERE子句用於限制哪一個行匹配下一個表或發送到客戶。除非你專門從表中索取或檢查所有行,如果Extra值不爲Using where並且表聯接類型爲ALL或index,查詢可能會有一些錯誤。即使where子句中所有的字段都包含在一個索引中 ,也可以能會看到Using where,因爲有些字段可能會是NULL。
Using where with pushed condition
這種情況只會出現在NDB表中。
四、explain輸出解釋
從explain的輸出結果中,你可以得到一個很好地啓示。它會大致的告訴你,mysql會檢查多少行來完成本次查詢。如果你通過系統變量max_join_size來限制查詢,那麼explain的輸出也可以告訴你在select語句中的哪些表被查詢了,哪些表被放棄了。
以下列子展示一個多表的連表查詢,通過explain提供的信息,是如何漸漸地被優化的。
假設你有一個查詢語句如下,然後你想通過explain來檢測該語句。
EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
tt.ProjectReference, tt.EstimatedShipDate,
tt.ActualShipDate, tt.ClientID,
tt.ServiceCodes, tt.RepetitiveID,
tt.CurrentProcess, tt.CurrentDPPerson,
tt.RecordVolume, tt.DPPrinted, et.COUNTRY,
et_1.COUNTRY, do.CUSTNAME
FROM tt, et, et AS et_1, do
WHERE tt.SubmitTime IS NULL
AND tt.ActualPC = et.EMPLOYID
AND tt.AssignedPC = et_1.EMPLOYID
AND tt.ClientID = do.CUSTNMBR;
對於這個列子,做如下假設:
這些表的字段定義如下:
表名 | 字段 | 字段類型 |
---|---|---|
tt | ActualPC | CHAR(10) |
tt | AssignedPC | CHAR(10) |
ttd | ClientID | CHAR(10) |
et | EMPLOYID | CHAR(15) |
do | CUSTNMBR | CHAR(15) |
該表有如下索引
表名 | 索引 |
---|---|
tt | ActualPC |
tt | AssignedPC |
tt | ClientID |
et | EMPLOYID (primary key) |
do | CUSTNMBR (primary key) |
tt表的ActualPC字段的值不是均勻分佈的。
一開始,沒有任何的優化,explain的結果如下:
table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|
et | ALL | PRIMARY | NULL | NULL | NULL | 74 | |
do | ALL | PRIMARY | NULL | NULL | NULL | 2135 | |
et_1 | ALL | PRIMARY | NULL | NULL | NULL | 74 | |
tt | ALL | AssignedPC,ClientID,ActualPC | NULL | NULL | NULL | 3872 | Range checked for each record (index map: 0x23) |
由於所有的type都是All,這個結果表明,對於每個表,mysql都產生了笛卡爾積。這表明,每個組合的行,都會使用相當長的時間,由於每張表的每行數據都要被檢索。對於上述情況,會產生 74 × 2135 × 74 × 3872 = 45,268,558,720行。如果表格更大一些,你可以想象這會執行多長的時間。
這裏的一個問題是,如果這些字段有相同的類型,那麼mysql可以使用索引來使查詢更加的高效。VARCHAR和CHAR,如果定義的長度相同,那麼會被認爲是相同的類型。tt.ActuralPC被定義爲CHAR(10),et.EMPLOYID被定爲CHAR(15),所以長度不匹配。爲了解決這個問題,用alter table命令把ActualPC的長度變成15。
ALTER TABLE tt MODIFY ActualPC VARCHAR(15);
現在,tt.ActuralPC和et.EMPLOYID的長度都是15了。再次執行上述語句,結果如下:
table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|
tt | ALL | AssignedPC,ClientID,ActualPC | NULL | NULL | NULL | 3872 | Using where |
do | ALL | PRIMARY | NULL | NULL | NULL | 2135 | Range checked for each record (index map: 0x1) |
et_1 | ALL | PRIMARY | NULL | NULL | NULL | 74 | Range checked for each record (index map: 0x1) |
et | eq_ref | PRIMARY | PRIMARY | 15 | tt.ActualPC | 1 |
這並不完美,但是已經好多了。它產生的行數,比之前少了74倍。這個版本需要執行幾秒鐘。
第二可以做如下改變,消除tt.AssignedPC = et_1.EMPLOYID 和tt.ClientID = do.CUSTNMBR的長度不匹配問題。
ALTER TABLE tt MODIFY AssignedPC VARCHAR(15),MODIFY ClientID VARCHAR(15);
做了這一改變後,再次執行explain,結果如下:
table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|
et | ALL | PRIMARY | NULL | NULL | NULL | 74 | |
tt | ref | AssignedPC,ClientID,ActualPC | ActualPC | 15 | et.EMPLOYID | 52 | Using index |
et_1 | eq_ref | PRIMARY | PRIMARY | 15 | tt.AssignedPC | 1 | |
do | eq_ref | PRIMARY | PRIMARY | 15 | tt.ClientID | 1 |
到目前爲止,這個查詢已經儘可能的優化了。剩下的問題是,mysql會默認假設tt.ActualPC字段上的數據是均勻分佈的,然後tt表的情況並不是這樣。幸運的是,這是很容易告訴mysql去分析鍵的分佈。
ANALYZE TABLE tt
加上額外的索引信息,該連表會變得完美。explain的輸出結果如下:
table | type | possible_keys | key | key_len | ref | rows | Extra |
---|---|---|---|---|---|---|---|
tt | ALL | AssignedPC,ClientID,ActualPC | NULL | NULL | NULL | 3872 | Using index |
et | eq_ref | PRIMARY | PRIMARY | 15 | tt.ActualPC | 1 | |
et_1 | eq_ref | PRIMARY | PRIMARY | 15 | tt.AssignedPC | 1 | |
do | eq_ref | PRIMARY | PRIMARY | 15 | tt.ClientID | 1 |
explain輸出的rows字段的值,是根據mysql優化器的分析得出的。判斷這個rows字段的值是否接近於實際的值,可以將該值與本次查詢返回的行數進行比較。如果這兩個值相差很大,你可以在select子句中加入STRAIGHT_JOIN語句,然後嘗試把join的表的順序變換一下,以此來減小差距。