7.2.1 EXPLAIN 語法(得到SELECT 的相關信息)
EXPLAIN tbl_name
或者:
EXPLAIN SELECT select_options
EXPLAIN 語句可以被當作 DESCRIBE 的同義詞來用,也可以用來獲取一個MySQL要執行的 SELECT 語句的相關信息。
EXPLAIN tbl_name 語法和 DESCRIBE tbl_name 或 SHOW COLUMNS FROM tbl_name 一樣。
當在一個 SELECT 語句前使用關鍵字 EXPLAIN 時,MySQL會解釋了即將如何運行該 SELECT 語句,它顯示了表如何連接、連接的順序等信息。
本章節主要講述了第二種 EXPLAIN 用法。
在 EXPLAIN 的幫助下,您就知道什麼時候該給表添加索引,以使用索引來查找記錄從而讓 SELECT 運行更快。
如果由於不恰當使用索引而引起一些問題的話,可以運行 ANALYZE TABLE 來更新該表的統計信息,例如鍵的基數,它能幫您在優化方面做出更好的選擇。詳情請看"14.5.2.1 ANALYZE TABLE Syntax"。
您還可以查看優化程序是否以最佳的順序來連接數據表。為了讓優化程序按照 SELECT 語句中的表名的順序做連接,可以在查詢的開始使用 SELECT STRAIGHT_JOIN 而不只是 SELECT。
EXPLAIN 返回了一行記錄,它包括了 SELECT 語句中用到的各個表的信息。這些表在結果中按照MySQL即將執行的查詢中讀取的順序列出來。MySQL用一次掃描多次連接(single-sweep, multi-join) 的方法來解決連接。這意味著MySQL從第一個表中讀取一條記錄,然後在第二個表中查找到對應的記錄,然後在第三個表中查找,依次類推。當所有的表都掃描完了,它輸出選擇的字段並且回溯所有的表,直到找不到為止,因為有的表中可能有多條匹配的記錄下一條記錄將從該表讀取,再從下一個表開始繼續處理。
在MySQL version 4.1中,EXPLAIN 輸出的結果格式改變了,使得它更適合例如 UNION 語句、子查詢以及派生表的結構。更令人注意的是,它新增了2個字段: id 和 select_type。當你使用早於MySQL 4.1的版本就看不到這些字段了。
EXPLAIN 結果的每行記錄顯示了每個表的相關信息,每行記錄都包含以下幾個字段:
id
本次 SELECT 的標識符。在查詢中每個 SELECT 都有一個順序的數值。
select_type
SELECT 的類型,可能會有以下幾種:
SIMPLE
簡單的 SELECT (沒有使用 UNION 或子查詢)
PRIMARY
最外層的 SELECT。
UNION
第二層,在SELECT 之後使用了 UNION 。
DEPENDENT UNION
UNION 語句中的第二個 SELECT,依賴於外部子查詢
SUBQUERY
子查詢中的第一個 SELECT
DEPENDENT SUBQUERY
子查詢中的第一個 SUBQUERY 依賴於外部的子查詢
DERIVED
派生表 SELECT(FROM 子句中的子查詢)
table
記錄查詢引用的表。
type
表連接類型。以下列出了各種不同類型的表連接,依次是從最好的到最差的:
system
表只有一行記錄(等於系統表)。這是 const 表連接類型的一個特例。
const
表中最多只有一行匹配的記錄,它在查詢一開始的時候就會被讀取出來。由於只有一行記錄,在余下的優化程序裡該行記錄的字段值可以被當作是一個恆定值。const 表查詢起來非常快,因為只要讀取一次!const 用於在和 PRIMARY KEY 或 UNIQUE 索引中有固定值比較的情形。下面的幾個查詢中,tbl_name 就是 const 表了:
SELECT * FROM tbl_name WHERE primary_key=1;
SELECT * FROM tbl_name
WHERE primary_key_part1=1 AND primary_key_part2=2;
eq_ref
從該表中會有一行記錄被讀取出來以和從前一個表中讀取出來的記錄做聯合。與 const 類型不同的是,這是最好的連接類型。它用在索引所有部分都用於做連接並且這個索引是一個 PRIMARY KEY 或 UNIQUE 類型。eq_ref 可以用於在進行"="做比較時檢索字段。比較的值可以是固定值或者是表達式,表達示中可以使用表裡的字段,它們在讀表之前已經准備好了。以下的幾個例子中,MySQL使用了 eq_ref 連接來處理 ref_table:
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
該表中所有符合檢索值的記錄都會被取出來和從上一個表中取出來的記錄作聯合。ref 用於連接程序使用鍵的最左前綴或者是該鍵不是 PRIMARY KEY 或 UNIQUE 索引(換句話說,就是連接程序無法根據鍵值只取得一條記錄)的情況。當根據鍵值只查詢到少數幾條匹配的記錄時,這就是一個不錯的連接類型。ref 還可以用於檢索字段使用 = 操作符來比較的時候。以下的幾個例子中,MySQL將使用 ref 來處理 ref_table:
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;
ref_or_null
這種連接類型類似 ref,不同的是MySQL會在檢索的時候額外的搜索包含 NULL 值的記錄。這種連接類型的優化是從MySQL 4.1.1開始的,它經常用於子查詢。在以下的例子中,MySQL使用 ref_or_null 類型來處理 ref_table:
SELECT * FROM ref_table
WHERE key_column=expr OR key_column IS NULL;
詳情請看"7.2.6 How MySQL Optimizes IS NULL"。
index_merge
這種連接類型意味著使用了 Index Merge 優化方法。這種情況下,key字段包括了所有使用的索引,key_len 包括了使用的鍵的最長部分。詳情請看"7.2.5 How MySQL Optimizes OR Clauses"。
unique_subquery
這種類型用例如一下形式的 IN 子查詢來替換 ref:
value IN (SELECT primary_key FROM single_table WHERE some_expr)
unique_subquery 只是用來完全替換子查詢的索引查找函數效率更高了。
index_subquery
這種連接類型類似 unique_subquery。它用子查詢來代替 IN,不過它用於在子查詢中沒有唯一索引的情況下,例如以下形式:
value IN (SELECT key_column FROM single_table WHERE some_expr)
range
只有在給定范圍的記錄才會被取出來,利用索引來取得一條記錄。key 字段表示使用了哪個索引。key_len 字段包括了使用的鍵的最長部分。這種類型時 ref 字段值是 NULL。range 用於將某個字段和一個定植用以下任何操作符比較時 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, 或 IN:
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 一樣,不同的是它只掃描索引樹。它通常會比 ALL 快點,因為索引文件通常比數據文件小。MySQL在查詢的字段知識單獨的索引的一部分的情況下使用這種連接類型。
ALL
將對該表做全部掃描以和從前一個表中取得的記錄作聯合。這時候如果第一個表沒有被標識為 const 的話就不大好了,在其他情況下通常是非常糟糕的。正常地,可以通過增加索引使得能從表中更快的取得記錄以避免 ALL。
possible_keys
possible_keys 字段是指MySQL在搜索表記錄時可能使用哪個索引。注意,這個字段完全獨立於 EXPLAIN 顯示的表順序。這就意味著 possible_keys 裡面所包含的索引可能在實際的使用中沒用到。如果這個字段的值是 NULL,就表示沒有索引被用到。這種情況下,就可以檢查 WHERE 子句中哪些字段那些字段適合增加索引以提高查詢的性能。就這樣,創建一下索引,然後再用 EXPLAIN 檢查一下。詳細的查看章節"14.2.2 ALTER TABLE Syntax"。想看表都有什麼索引,可以通過 SHOW INDEX FROM tbl_name 來看。
key
key 字段顯示了MySQL實際上要用的索引。當沒有任何索引被用到的時候,這個字段的值就是 NULL。想要讓MySQL強行使用或者忽略在 possible_keys 字段中的索引列表,可以在查詢語句中使用關鍵字FORCE INDEX, USE INDEX, 或 IGNORE INDEX。如果是 MyISAM 和 BDB 類型表,可以使用 ANALYZE TABLE 來幫助分析使用使用哪個索引更好。如果是 MyISAM 類型表,運行命令 myisamchk --analyze 也是一樣的效果。詳細的可以查看章節"14.5.2.1 ANALYZE TABLE Syntax"和"5.7.2 Table Maintenance and Crash Recovery"。
key_len
key_len 字段顯示了MySQL使用索引的長度。當 key 字段的值為 NULL 時,索引的長度就是 NULL。注意,key_len 的值可以告訴你在聯合索引中MySQL會真正使用了哪些索引。
ref
ref 字段顯示了哪些字段或者常量被用來和 key 配合從表中查詢記錄出來。
rows
rows 字段顯示了MySQL認為在查詢中應該檢索的記錄數。
Extra
本字段顯示了查詢中MySQL的附加信息。以下是這個字段的幾個不同值的解釋:
Distinct
MySQL當找到當前記錄的匹配聯合結果的第一條記錄之後,就不再搜索其他記錄了。
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 中查找記錄。當在 t2 中找到一條匹配的記錄時,這就意味著 t2.id 肯定不會都是 NULL,就不會再在 t2 中查找相同 id 值的其他記錄了。也可以這麼說,對於 t1 中的每個記錄,MySQL只需要在 t2 中做一次查找,而不管在 t2 中實際有多少匹配的記錄。
range checked for each record (index map: #)
MySQL沒找到合適的可用的索引。取代的辦法是,對於前一個表的每一個行連接,它會做一個檢驗以決定該使用哪個索引(如果有的話),並且使用這個索引來從表裡取得記錄。這個過程不會很快,但總比沒有任何索引時做表連接來得快。
Using filesort
MySQL需要額外的做一遍從而以排好的順序取得記錄。排序程序根據連接的類型遍歷所有的記錄,並且將所有符合 WHERE 條件的記錄的要排序的鍵和指向記錄的指針存儲起來。這些鍵已經排完序了,對應的記錄也會按照排好的順序取出來。詳情請看"7.2.9 How MySQL Optimizes ORDER BY"。
Using index
字段的信息直接從索引樹中的信息取得,而不再去掃描實際的記錄。這種策略用於查詢時的字段是一個獨立索引的一部分。
Using temporary
MySQL需要創建臨時表存儲結果以完成查詢。這種情況通常發生在查詢時包含了GROUP BY 和 ORDER BY 子句,它以不同的方式列出了各個字段。
Using where
WHERE 子句將用來限制哪些記錄匹配了下一個表或者發送給客戶端。除非你特別地想要取得或者檢查表種的所有記錄,否則的話當查詢的 Extra 字段值不是 Using where 並且表連接類型是 ALL 或 index 時可能表示有問題。
如果你想要讓查詢盡可能的快,那麼就應該注意 Extra 字段的值為Using filesort 和 Using temporary 的情況。
你可以通過 EXPLAIN 的結果中 rows 字段的值的乘積大概地知道本次連接表現如何。它可以粗略地告訴我們MySQL在查詢過程中會查詢多少條記錄。如果是使用系統變量 max_join_size 來取得查詢結果,這個乘積還可以用來確定會執行哪些多表 SELECT 語句。詳情請看"7.5.2 Tuning Server Parameters"。
下面的例子展示了如何通過 EXPLAIN 提供的信息來較大程度地優化多表聯合查詢的性能。
假設有下面的 SELECT 語句,正打算用 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;
在這個例子中,先做以下假設:
要比較的字段定義如下:
Table
Column
Column Type
tt
ActualPC
CHAR(10)
tt
AssignedPC
CHAR(10)
tt
ClIEntID
CHAR(10)
et
EMPLOYID
CHAR(15)
do
CUSTNMBR
CHAR(15)
數據表的索引如下:
Table
Index
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, NULL NULL NULL 3872
ClIEntID,
ActualPC
range checked for each record (key map: 35)
由於字段 type 的對於每個表值都是 ALL,這個結果意味著MySQL對所有的表做一個迪卡爾積;這就是說,每條記錄的組合。這將需要花很長的時間,因為需要掃描每個表總記錄數乘積的總和。在這情況下,它的積是 74 * 2135 * 74 * 3872 = 45,268,558,720 條記錄。如果數據表更大的話,你可以想象一下需要多長的時間。
在這裡有個問題是當字段定義一樣的時候,MySQL就可以在這些字段上更快的是用索引(對 ISAM 類型的表來說,除非字段定義完全一樣,否則不會使用索引)。在這個前提下,VARCHAR 和 CHAR是一樣的除非它們定義的長度不一致。由於 tt.ActualPC 定義為 CHAR(10),et.EMPLOYID 定義為 CHAR(15),二者長度不一致。
為了解決這個問題,需要用 ALTER TABLE 來加大 ActualPC 的長度從10到15個字符:
MySQL> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);
現在 tt.ActualPC 和 et.EMPLOYID 都是 VARCHAR(15)
了。再來執行一次 EXPLAIN 語句看看結果:
table type possible_keys key key_len ref rows Extra
tt ALL AssignedPC, NULL NULL NULL 3872 Using
ClIEntID, where
ActualPC
do ALL PRIMARY NULL NULL NULL 2135
range checked for each record (key map: 1)
et_1 ALL PRIMARY NULL NULL NULL 74
range checked for each record (key map: 1)
et eq_ref PRIMARY PRIMARY 15 tt.ActualPC 1
這還不夠,它還可以做的更好:現在 rows 值乘積已經少了74倍。這次查詢需要用2秒鐘。
第二個改變是消除在比較 tt.AssignedPC = et_1.EMPLOYID 和 tt.ClIEntID = do.CUSTNMBR 中字段的長度不一致問題:
MySQL> 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, ActualPC 15 et.EMPLOYID 52 Using
ClIEntID, where
ActualPC
et_1 eq_ref PRIMARY PRIMARY 15 tt.AssignedPC 1
do eq_ref PRIMARY PRIMARY 15 tt.ClIEntID 1
這看起來已經是能做的最好的結果了。
遺留下來的問題是,MySQL默認地認為字段tt.ActualPC 的值是均勻分布的,然而表 tt 並非如此。幸好,我們可以很方便的讓MySQL分析索引的分布:
MySQL> ANALYZE TABLE tt;
到此為止,表連接已經優化的很完美了,EXPLAIN 的結果如下:
table type possible_keys key key_len ref rows Extra
tt ALL AssignedPC NULL NULL NULL 3872 Using
ClIEntID, where
ActualPC
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的連接優化程序大致猜測的,請檢查這個值跟真實值是否基本一致。如果不是,可以通過在 SELECT 語句中使用 STRAIGHT_JOIN 來取得更好的性能,同時可以試著在 FROM
分句中用不同的次序列出各個表。