MySQL 優化 —— EXPLAIN 執行計劃詳解

引言

本博客大部分內容翻譯自MySQL 官網 Understanding the Query Execution Plan 專題。另外有一些補充,則來自於網課以及《高性能MySQL(第三版)》。

根據我們的表、字段、索引、以及 where 子句中的條件等信息,MySQL 優化器會考慮各種技術來更高效地執行查找。一個大表中的查找不一定要讀取所有記錄;多表連接也不一定需要比較每條聯合記錄。優化器選擇的執行最優查詢的操作集,稱爲“查詢執行計劃”也可以說是 EXPLAIN 計劃。我們的目標就是找到那些可以將查詢優化地更好的點,然後通過學習 SQL 語法和索引等技術,來改善執行計劃。

一、EXPLAIN 介紹

EXPLAIN 語句提供了 MySQL 如何執行語句的信息:

1、MySQL5.6 之後 EXPLAIN 可以和 SELECT DELETE INSERT REPLACE UPDATE 語句等一起工作;

2、當 EXPLAIN 和一個可解釋的語句一起使用時,MySQL 就會展示來自優化器的關於語句執行計劃的信息。即,MySQL 會解釋它將會怎樣執行語句,包括表是如何連接的,以什麼方式排序的等信息。

3、When EXPLAIN is used with FOR CONNECTION connection_id  rather than an explainable statement, it displays the execution plan for the statement executing in the named connection.(這句暫不翻譯)

4、對於 SELECT 語句, EXPLAIN 提供了額外的執行計劃信息,可以用 SHOW WARNINGS 來查看。參考:Section 8.8.3, “Extended EXPLAIN Output Format”.

5、EXPLAIN對於檢查涉及分區表的查詢非常有用。參考:Section 22.3.5, “Obtaining Information About Partitions”.

6、FORMAT 選項可以用於選擇輸出格式。TRADITIONAL 以表格的形式展示。如果沒有指定 FORMAT 選項,TRADITIONAL 就是默認的。JSON 格式會以 json 格式展示 EXPLAIN 信息。例如:EXPLAIN FORMAT = JSON SELECT... 。

在 EXPLAIN 的幫助下,你可以清楚的知道爲了讓查詢變得更快,該在哪裏給表添加索引。你也可以知道優化器是否以最佳的順序連接各個表。爲了讓優化器使用 SELECT 語句中表的命名順序連接各表,以 SELECT  STRAIGHT_JOIN(而不是SELECT)開頭即可。(參考:Section 13.2.9, “SELECT Statement”)但是,STRAIGHT_JOIN 可能會妨礙索引的使用,因爲它禁用了半連接轉換(because it disables semijoin transformations. )。參考:Section 8.2.2.1, “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”.

優化器跟蹤(The optimizer trace有時可能提供與 EXPLAIN 互補的信息。但是,優化器跟蹤的格式和內容會受不同版本的影響。更多細節,參考:MySQL Internals: Tracing the Optimizer.

如果你對本應該使用索引而沒有用到索引的情況感到疑惑,執行一下 ANALYZE TABLE 來更新表統計信息。例如列的基數(cardinality of keys),這會影響優化器做出的選擇。參考:Section 13.7.2.1, “ANALYZE TABLE Statement”.

注意:

EXPLAIN 還可以用來獲取表的列的信息

EXPLAIN  tb1_name  與  DESCRIBE  tb1_name  、 SHOW  COLUMNS  FROM  tb1_name  是等價的。

更多信息,參考:Section 13.8.1, “DESCRIBE Statement”,和  Section 13.7.5.5, “SHOW COLUMNS Statement”

二、EXPLAIN 的輸出格式

EXPLAIN會爲 select 語句中的每張表返回一行信息。並會以MySQL處理語句時讀取這些表的順序羅列它們。

MySQL 解決所有 join 的方法是使用一個“嵌套循環關聯”的方法。也就是說,MySQL會從第一張表中讀取一條記錄,然後找到第二張表中與之匹配的記錄,然後再找第三張表,依此類推。當所有的表處理完畢,MySQL會輸出查詢的列並回溯表列表(table list),直到找到一個有更多行的表(譯者注:連接表的時候,主表查詢出的記錄往往是最多的,從主表開始關聯查詢,再回溯到主表,可能官網想表達的是這個意思)。下一條記錄會從該表中讀取,並且繼續處理下一張表。

EXPLAIN 輸出包含了分區信息(partitions 列)。同樣,對於 SELECT 語句,EXPLAIN 會生成擴展信息,只要在 EXPLAIN 執行完成後,直接執行 SHOW WARNINGS 即可。參考:Section 8.8.3, “Extended EXPLAIN Output Format”

注意

舊的MySQL版本中, 分區和擴展信息使用 EXPLAIN PARTITIONS 和 EXPLAIN EXTENDED 輸出。這些語法依然向後兼容,但是分區和擴展輸出現在默認都是開啓的了。所以 PARTITIONS 和 EXTENDED 關鍵字完全多餘。未來版本也會移除的。

不可以在EXPLAIN語句中同時使用 PARTITIONS 和 EXTENDED 關鍵字。另外,哪一個都不能和 FORMAT 選項一起使用。

MySQL Workbench (譯者注:這是一款由 MySQL 官方出品的,類似 Navicat 的數據庫管理工具)有一個 Visual Explain 功能,可以提供可視化的 EXPLAIN 輸出信息。參考:Tutorial: Using Explain to Improve Query Performance.

2.1 EXPLAIN 輸出字段(EXPLAIN Output Columns

這一節描述了 EXPLAIN 的輸出字段。後面的兩節則提供了更多的關於 type 和 Extra 字段的信息。

EXPLAIN輸出的每一行都對應一張表。下面的表提供了EXPLAIN的輸出字段,第一列是字段名稱,第二列是當 FORMAT = JSON 時的輸出字段名稱:

Column JSON Name Meaning
id select_id The SELECT identifier:查詢id
select_type None The SELECT type:查詢類型
table table_name The table for the output row:對應的表
partitions partitions The matching partitions:匹配的分區
type access_type The join type:訪問類型
possible_keys possible_keys The possible indexes to choose:可能用到的索引
key key The index actually chosen:真正被用到的索引
key_len key_length The length of the chosen key:用到的索引長度
ref ref The columns compared to the index:與索引比較的列
rows rows Estimate of rows to be examined:大約要檢索的行數
filtered filtered Percentage of rows filtered by table condition:按表條件過濾的行的百分比
Extra None Additional information:附加信息

1、id(JSON 名:select_id)

SELECT 標識符(SELECT identifier)。這是一個連續的數字,用以標識查詢中的 SELECT 。如果引用了其他行的聯合結果集(union result of other rows),那麼 id 會爲 NULL。這種情況下,該行的 table 字段會顯示爲 <union M, N> 這樣的形式,表示該行代表了 id 值爲 M 和 N 的行的聯合(the row refers to the union of the rows with id values of M and )。

重點:id 是一個自然數編號,如1、2,但有時也可以是NULL。如上所述,NULL的時候,就是引用了一個 UNION 結果集

當 id 爲數字的時候,編號大的會先執行。有時候,編號會相同,相同編號就從上到下執行

2、select_type(JSON 名:無)

查詢類型。MySQL將查詢分爲簡單和複雜類型,複雜類型可分爲三大類簡單子查詢FROM子查詢以及UNION查詢。select_type 就是用於區分這三類複雜查詢。可選值如下(紅色標記爲常見值):

select_type Value JSON Name Meaning
SIMPLE None 簡單查詢(沒有任何 UNION 或 子查詢)。
PRIMARY None 主查詢,如果查詢中包含任何複雜的子部分,那麼最外層查詢被標記PRIMARY
UNION None UNION 中的第二個或後面的SELECT語句
DEPENDENT UNION dependent (true) UNION 中的第二個或後面的SELECT語句, 依賴於外部查詢
UNION RESULT union_result 從UNION 的結果獲取數據的SELECT。
SUBQUERY None SELECT子句或WHERE子句中的子查詢
DEPENDENT SUBQUERY dependent (true) 子查詢中的第一個 SELECT, 依賴於外層查詢
DERIVED None 派生表。FROM子句中的子查詢。MySQL會遞歸執行這些子查詢,把結果放在臨時表裏
MATERIALIZED materialized_from_subquery Materialized subquery  物化子查詢。參考《MySQL高級 —— 查詢性能優化》4.1節
UNCACHEABLE SUBQUERY cacheable (false) 非緩存子查詢,結果不能被緩存的子查詢,必須被外部查詢的每一行重新求得
UNCACHEABLE UNION cacheable (false) 非緩存子查詢(uncacheable subquery)的 UNION 中的第二個或後面的 SELECT

SUBQUERY還可以被標記爲DEPENDENT SUBQUERY,這一般是指SELECT依賴於外層查詢發現的數據(很可能是依賴於FROM派生表的外層SELECT)。參考:Section 13.2.10.7, “Correlated Subqueries” 。

DEPENDENT SUBQUERY 的取值與 UNCACHEABLE SUBQUERY(由於用戶變量等原因) 的取值不同。對於 DEPENDENT SUBQUERY ,對於來自其外部查詢的變量的每組不同值,子查詢只重新計算一次。而對於 UNCACHEABLE SUBQUERY ,對外部查詢的每行記錄,該子查詢都會計算一遍。

子查詢緩存與緩存中的查詢結果緩存不一樣(具體描述參考 Section 8.10.3.1, “How the Query Cache Operates”)。子查詢緩存發生在查詢執行過程中,而查詢結果緩存只在查詢執行完畢時纔會存儲結果。

當你在 EXPLAIN 語句中指定了 FORMAT = JSON ,輸出的結果並沒有一個對應 select_type 的單獨屬性;query_block 屬性對應給定的 SELECT 。與剛纔顯示的大多數 SELECT 子查詢類型等價的屬性都是有的,並且在合適的時機就會展示。不過並沒有與 SIMPLE 和 PRIMARY 等價的 JSON 值。

select_type 屬性值對於非 SELECT 語句,會展示影響表的語句類型如 DELETE 語句的 select_type 就是 DELETE

3、table(JSON 名:table_name)

explain 輸出的每一行都對應一個表別名或表名。它可以是下面的值中的一個:

<union M, N> : 這一行引用了 id 值爲 M 和 N 的表的聯合。

<derived N> : 這一行引用了 id 值爲 N 的表所派生的表。派生的表可能是一個結果集,比如,FROM 子句中的子查詢。

<subquery N> : 這一行引用了 id 值爲 N 的物化子查詢的結果。參考:Section 8.2.2.2, “Optimizing Subqueries with Materialization”.

4、partitions(JSON 名:partitions)

查詢的記錄將會在哪個分區中匹配。NULL 代表沒有分區表。參考: Section 22.3.5, “Obtaining Information About Partitions”.

5、type(JSON 名:access_type)

關聯類型,但更準確的說法是——訪問類型,換言之就是MySQL決定如何查找表中的行。參考 2.2 節。

6、possible_keys(JSON 名:possible_keys)

該屬性可以表明查詢中,對應表有哪些索引可以使用。注意這個屬性完全不依賴於表在 explain 輸出中的顯示順序。也就是說,以生成的表順序 ,possible_keys 中的有些索引可能實際中並不會用到。

如果該屬性是 NULL (或者在 JSON 格式中是 undefined ),代表沒有相關的索引。這時,你可能就應該努力通過調試 WHERE 子句來提升你的查詢性能,檢查是否涉及到了一些字段或者適合索引查詢的字段。如果有,就創建一個合適的索引,然後再次通過 EXPLAIN 進行檢驗。

查看一個表有哪些索引,可以使用 SHOW INDEX FROM tbl_name 語句。

7、key(JSON 名:key)

這一列表示 MySQL 決定採用哪個索引來優化對該表的訪問。如果 MySQL 決定使用 possible_keys 中的一個索引去查找記錄,那麼這個索引就會列在 key 屬性中。

key 中也會出現 possible_key 中沒有出現的索引。發生這種情況,很可能是 possible_keys 沒有找到適合查詢的索引,但是所有查詢的字段都在索引中。也就是說,查詢使用了覆蓋索引。因此,儘管它不用於決定要查詢哪些行,但卻依然可以用於查詢字段,因爲索引掃描依然比行掃描更高效。換句話說,possible_keys 揭示了哪一個索引能有助於高效地行查找,而 key 顯示的是優化採用哪一個索引可以最小化查詢成本

對於InnoDB ,即使查詢列表中有主鍵,二級索引也可能覆蓋所查詢的字段,因爲InnoDB用每個二級索引存儲了主鍵值。如果列是NULL, MySQL就找不到索引來更有效地執行查詢。

要強制MySQL使用或忽略在 possiblele_keys 中列出的索引,請在查詢中使用 FORCE INDEXUSE INDEX 或 IGNORE INDEX 。參考: Section 8.9.4, “Index Hints”.

對於 MyISAM,運行 ANALYZE TABLE 可以幫助優化器選擇更好的索引。對於 MyISAM 表來說, myisamchk --analyze 也是一樣的。參考: Section 13.7.2.1, “ANALYZE TABLE Statement”, 和 Section 7.6, “MyISAM Table Maintenance and Crash Recovery”.

8、key_len(JSON 名:key_length)

該字段表示 MySQL 在索引裏使用的字節數。

因爲key_len是通過查找表的定義而被計算出,而不是表中的數據,因此它顯示了在索引字段中可能的最大長度,而不是表中數據使用的實際字節數。key_len 的值可以讓你判斷 MySQL 究竟用到了複合索引的哪幾個索引列。如果 key 屬性的值爲 NULL , 那麼 key_len 肯定也是 NULL 。

由於索引的存儲格式,那些可以爲 NULL 的字段的索引長度要比非空字段的索引長度大一些。

MySQL並不總是顯示一個索引真正使用了多少。例如,如果對一個前綴模式匹配(例如 '張%')執行LIKE查詢,它會顯示列的完整寬度正在被使用。

計算 key_len 的簡易方法:

int 類型在MySQL中以4個字節存儲,key_len 爲 4,如果列值允許爲 NULL,那麼需要 + 1,即 key_len 爲 5.

double 類型以8個字節存儲,key_len 爲 8,如果允許 NULL,那麼同樣 +1, 即 key_len 爲 9.

char(n) 定長字符串,首先需要看字符集,常見的utf8以3個字節存儲每個字符,gbk用2個,latin用1個。key_len 就等於每個字節長度乘以允許最大字符數n,如果允許NULL,key_len 也要 +1。例如 char(20) DEFAULT NULL,編碼爲utf8 ,那麼 key_len 就是 3 × 20 + 1 = 61。如果不允許爲 NULL ,就是60。

varchar(n)變長字符串,每個字符:utf8爲3字節、gbk爲2字節、latin爲1字節。由於是變長,因此 key_len 要 +2,如果允許 NULL,同樣 +1。其他和 char計算方式一樣。例如,varchar(20) DEFAULT NULL,編碼 utf8,那麼 key_len 就是:

3 × 20 + 2 + 1 = 63,如果不允許爲 NULL,就是62。

上面的說明只是單獨計算每種列值類型的方法,如果是複合索引,那麼key_len 就是用到的索引列長度和。

9、ref(JSON 名:ref)

ref 列顯示了常量或哪些列與 key 列中的索引進行了比較。只有 type 列是 ref 的時候,ref 列纔會有值。

簡單的說,就是 key 中的索引,如果與一個常量比較,那麼 ref 會顯示 const,如果是與其他表的某個列進行比較,那麼就會顯示該列名。

如果 ref 屬性的值是 func ,那麼用到的值就是某些函數的結果。想要知道是哪個函數,在 EXPLAIN 執行後使用 SHOW WARNINGS ,查看EXPLAIN 的擴展信息。

函數實際上可能是一個運算符,比如算術運算符。

10、rows(JSON 名:rows)

rows 列表示MySQL認爲執行查詢必須檢查的行數。這個數字是內嵌關聯循環計劃裏的循環數目。也就是說,它不是最終的結果集裏的行數,而是MySQL爲了找到符合條件的結果集而必須讀取的行的平均數。

對於 InnoDB 表,這個數是一個估值,而且可能並不總是準確的。

11、filtered(JSON 名:filtered)

filtered 屬性表示被篩選條件過濾掉的記錄條數佔全表的估計百分比。最大值是100,意味着記錄全部被過濾掉。從100開始遞減的值表示過濾的量在增加。rows 屬性表示了需要檢查的估計行數,rows 乘 filtered 表示了將會被後面的表關聯的記錄條數。例如,如果 rows 是1000,filtered 是 50.00(50%),那麼要與後面的表連接的記錄條數就是 1000 × 50% = 500。

對於filtered ,原文的描述是:The filtered column indicates an estimated percentage of table rows that will be filtered by the table condition. The maximum value is 100, which means no filtering of rows occurred.  這裏面有一個語義上的陷阱,即 filtered 究竟表示的是 “被過濾掉的” ?還是 “過濾後(留下來)的” ,經過本人測試,filtered 表示的是前者,即 “被過濾掉的” ,這樣後面的語義也就基本自洽了。而 filtering 則表示 “過濾後(留下來)的” 。

12、Extra(JSON 名:none)

這一列顯示了關於 MySQL如何處理查詢的額外信息。對於不同值的描述,參考:Extra Information. 或參考下面 2.3 節。

2.2 EXPLAIN type訪問類型EXPLAIN Join Types

type 屬性描述了表之間是如何連接(或關聯)的。在 JSON 格式輸出中,對應 access_type 屬性。下面的列表描述了訪問類型,順序從“最理想類型”到“最糟糕的類型”:

system > const > eq_ref > ref > range > index > ALL

2.2.1 system(不常見)

表只有一行(=系統表)。是 const 連接類型的一種特殊情況。

2.2.2 const

表最多隻有 1 條匹配記錄,在查詢開始時就會讀取該表。因爲只有一行,所以這一行中列的值可以被其他優化器視爲常量。const 訪問類型非常快,因爲他們只會被讀取一次。MySQL能將這個查詢轉換爲一個常量,然後可以高效地將表從連接操作中移除。

const 會在你使用整個主鍵(all parts of a PRIMARY KEY)唯一索引(UNIQUE index)去比較一個常量的時候用到。在下面的查詢中,tb1_name 就是一張 const 表:

SELECT * FROM tbl_name WHERE primary_key=1;

SELECT * FROM tbl_name
  WHERE primary_key_part1=1 AND primary_key_part2=2;

2.2.3 eq_ref

使用這種索引查找,MySQL知道最多隻返回一條符合條件的記錄。它會在所有的索引部分都被用到的時候以及索引是主鍵非空唯一索引時出現到,它會將它們與某個參考值做比較。MySQL 對於這類訪問類型的優化做的非常好,因爲MySQL知道無須估計匹配行的範圍或在找到匹配行後再繼續查找

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;

2.2.4 ref

這是一種索引訪問(有時也叫“索引查找”)它返回所有匹配某個單個值的行,是查找和掃描的混合體。此類索引訪問只有當使用非唯一性索引唯一性索引的非唯一性前綴時纔會發生。把它叫做 ref 是因爲索引要跟某個參考值相比較。這個參考值可以是一個常數,或是來自多表查詢的結果值。如果該篩選列可以匹配少量的記錄,那 ref 還算是一個不錯的連接類型。

ref_or_null 是ref 之上的一個變體,它意味着MySQL必須在初次查找的結果裏進行第二次查找以找出NULL條目。

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;

2.2.5 full_text(不常見)

這種連接方式會在使用 FULLTEXT 索引的時候用到。

2.2.6 ref_or_null(不常見)

這種連接方式和 ref 類似,除此之外, MySQL 還會額外搜索包含 NULL 值的記錄。這種連接類型的優化絕大多數是在處理子查詢的時候。在下面的例子中, MySQL 會使用 ref_or_null 來處理 ref_table:

SELECT * FROM ref_table
  WHERE key_column=expr OR key_column IS NULL;

參考:Section 8.2.1.13, “IS NULL Optimization”

2.2.7 index_merge(不常見)

這種連接類型表示使用了索引合併優化(Index Merge optimization)。這種情況下,explain 中的 key 屬性會羅列出被用到的索引,key_len 屬性會列出用到的索引的最長的索引部分。參考:Section 8.2.1.3, “Index Merge Optimization”.

2.2.8 unique_subquery(不常見)

這種類型在類似下面的一些使用 IN 的子查詢時取代了 eq_ref:

value IN (SELECT primary_key FROM single_table WHERE some_expr)

 unique_subquery 只是一個索引查找函數,它完全取代了子查詢,以提高效率。

2.2.9 index_subquery(不常見)

這種連接類型有點像 unique_subquery 。它取代了 IN 子查詢,但它只在子查詢中有非唯一索引時纔會起作用,類似下面這樣:

value IN (SELECT key_column FROM single_table WHERE some_expr)

2.2.10 range 

這種連接類型會使用索引查詢給定範圍內的記錄。EXPLAIN 輸出中的 key 屬性表示了哪個 索引列 被用到。key_len 包含了被用到的最長的索引部分。ref 屬性爲 NULL。

range 類型會在索引列使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、或 IN() 任意一種操作符去比較常量的時候被用到。當使用 IN或 OR 列表的時候,顯示的範圍掃描,其實並不能和 > 這類比較符的性能等同,雖然它們在EXPLAIN中顯示的類型都是 range,但是 IN() 列表其實屬於等值列表。參考《MySQL高級 —— 高性能索引》6.2 節。

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);

2.2.11 index

index 類型除了會掃描索引樹之外,其他和 ALL 是一樣的。會有兩種情況出現:

1、如果索引是一個覆蓋索引,那麼這種類型的查詢就只會掃描索引樹。這種情況下, Extra 屬性會顯示 Using Index。一個只掃描索引的方式比 ALL 更快,這是因爲索引數據肯定要比表中數據要少。

2、以索引次序掃描全表。Extra 不會顯示 Uses Index。

index 類型的主要優點是避免了排序,最大缺點是要承擔按索引次序讀取整個表的開銷。

MySQL 會在查詢只用到了單一索引列的時候用到 index 這種類型。

2.2.12 ALL

這就是人們常說的“全表掃描”,這種類型會對前面各表的組合記錄都進行全表掃描。如果表是第一個沒有被標記爲 const 的表,這通常是不好的,在所有其他情況下通常是非常糟糕的。通常你可以通過增加索引來避免 ALL 。但也有例外,例如在查詢中使用了 LIMIT,或在 Extra 列中顯示“Using distinct/not exists”。

2.3 EXPLAIN Extra 信息(EXPLAIN Extra Information

Extra 屬性顯示了MySQL如何執行查詢的額外信息。

2.3.1 Using index

此值表示MySQL將使用覆蓋索引,以避免訪問表。不要把覆蓋索引和 type = index 訪問類型混淆了。

2.3.2 Using where

這意味着MySQL服務器將在存儲引擎檢索行後再進行過濾。當它讀取索引時,就能被存儲引擎檢驗,因此不是所有帶有 WHERE子句的查詢都會顯示“Using where” 。有時“Using where” 的出現就是一個暗示:查詢可受益於不同的索引。

2.3.3 Using temporary

這意味着MySQL在對查詢結果排序時會使用一個臨時表。

2.3.4 Using filesort

這意味着MySQL會對結果使用一個外部索引排序,而不是按照索引次序從表裏讀取行。MySQL有兩種文件排序算法,兩種方式都可以在內存或磁盤上完成。EXPLAIN 不會告訴你 MySQL將使用哪一種文件排序,也不會告訴你排序會在內存裏還是在磁盤上完成。

2.3.5 Range checked for each record (index map:N)

這個值意味着沒有好用的索引,新的索引將在連接的每一行上重新估算。N是顯示在possible_keys 列中索引的位圖,並且是冗餘的。

 

2.4 EXPLAIN 輸出的解釋

EXPLAIN輸出可以給你在連接各種表查詢的時候一個非常好的指示作用。這會大致告訴你MySQL 在執行查詢的時候必須要檢查多少行記錄。如果你限制了 max_join_size 系統變量,那麼 EXPLAIN 也會被用來告訴我們一些有用的東西。參考: Section 5.1.1, “Configuring the Server”.

下面的例子顯示了多表連接是如何基於 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;

對於這個例子,做出下面的假設:

1、比較的列(譯者注:columns being compared,實際上指的就是where 子句後面作爲篩選條件的列,因爲往往需要用到 = 號等操作符,因此在官網中一般都被稱爲被比較的列)定義如下:

Table Column Data Type
tt ActualPC CHAR(10)
tt AssignedPC CHAR(10)
tt ClientID CHAR(10)
et EMPLOYID CHAR(15)
do CUSTNMBR CHAR(15)

2、表有以下這些索引:

Table Index
tt ActualPC
tt AssignedPC
tt ClientID
et EMPLOYID (primary key)
do CUSTNMBR (primary key)

3、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 (index map: 0x23)

因爲每張表的連接類型都是 ALL ,這表明MySQL 正在生成一張笛卡爾集(a Cartesian product),也就是表中的每一行都進行了組合。這會花費相當長的時間,因爲必須檢查每個表中行數的乘積。對於這個案例,乘積就是:74 × 2135 × 74 × 3872 = 45,268,558,720 行。如果表再大一點,你可以想象一下它需要花費多長時間。

這裏有個問題,如果比較的列被聲明以相同的大小和類型,那麼 MySQL 就可以更高效的使用列上的索引。在這種語境下,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 (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

這依然不夠完美,但是也稍微好了點:rows 的乘積少了 74 倍(譯者注:et 表的 rows 由 74 變爲了 1)。這一版的執行會在幾秒鐘完成。

第二處修改可以針對 tt.AssignedPC = et_1.EMPLOYID 和 tt.ClientID = do.SUTNMBR 這兩個比較中有關列長度不匹配的問題。

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連接優化器的猜測。通過將 rows 的乘積與查詢返回的實際行數進行比較,就可以檢查這些數字是否接近實際情況。如果數字與實際查詢的行數相差甚遠,你可以通過在你的 SELECT 語句中使用 STRAIGHT_JOIN 並嘗試在 FROM 子句中以不同的順序羅列所查各表來獲取更好的性能。(但是,STRAIGHT_JOIN 可能會妨礙到索引的使用,因爲它禁用了半連接轉換。參考:Section 8.2.2.1, “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”.)

在某些情況下,當EXPLAIN SELECT與子查詢一起使用時,可以執行修改數據的語句。參考:Section 13.2.10.8, “Derived Tables”.

總結

這篇譯文翻譯了很長時間,斷斷續續可能有一個月。本篇文章有些地方可能翻譯的並不準確,因此希望各位可以與原文比較閱讀,增加理解。

另外,本來想在 Extra 部分就結束本篇翻譯,沒想到 MySQL 官網在最後一節給出了一個非常親民的案例講解,可以讓我們一覽 EXPLAIN 的常規用法。這一部分也是我認爲翻譯的比較準確的部分。

因爲 EXPLAIN 語句非常重要,因此,這篇譯文我也會經常翻閱,加深理解的同時不斷糾正文中翻譯的不準確或有所偏頗之處,同時希望大家能給予意見或建議。

2020-05-29 追加的部分,分散在文章的各個小節中,主要是在讀完《高性能MySQL(第三版)》的五六章,以及附錄EXPLAIN的部分,對執行計劃和一些索引的概念有了更進一步的理解和認識。之前翻譯的不是很準確的地方做了校對和潤色,某些廢話也是能刪就刪,我還寫了很多關於索引及查詢優化相關的文章,可以和這些文章一起閱讀,結合實踐並反覆回看的話,相信一定可以成爲MySQL性能優化領域的好手。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章