MySQL 优化 —— EXPLAIN 执行计划详解
引言
本博客大部分內(nèi)容翻譯自MySQL 官網(wǎng)?Understanding the Query Execution Plan?專題。另外有一些補(bǔ)充,則來自于網(wǎng)課以及《高性能MySQL(第三版)》。
根據(jù)我們的表、字段、索引、以及 where 子句中的條件等信息,MySQL 優(yōu)化器會(huì)考慮各種技術(shù)來更高效地執(zhí)行查找。一個(gè)大表中的查找不一定要讀取所有記錄;多表連接也不一定需要比較每條聯(lián)合記錄。優(yōu)化器選擇的執(zhí)行最優(yōu)查詢的操作集,稱為“查詢執(zhí)行計(jì)劃”,也可以說是 EXPLAIN 計(jì)劃。我們的目標(biāo)就是找到那些可以將查詢優(yōu)化地更好的點(diǎn),然后通過學(xué)習(xí) SQL 語法和索引等技術(shù),來改善執(zhí)行計(jì)劃。
一、EXPLAIN 介紹
EXPLAIN 語句提供了 MySQL 如何執(zhí)行語句的信息:
1、MySQL5.6 之后 EXPLAIN 可以和 SELECT DELETE INSERT REPLACE UPDATE 語句等一起工作;
2、當(dāng) EXPLAIN 和一個(gè)可解釋的語句一起使用時(shí),MySQL 就會(huì)展示來自優(yōu)化器的關(guān)于語句執(zhí)行計(jì)劃的信息。即,MySQL 會(huì)解釋它將會(huì)怎樣執(zhí)行語句,包括表是如何連接的,以什么方式排序的等信息。
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、對(duì)于 SELECT 語句, EXPLAIN 提供了額外的執(zhí)行計(jì)劃信息,可以用 SHOW WARNINGS 來查看。參考:Section?8.8.3, “Extended EXPLAIN Output Format”.
5、EXPLAIN對(duì)于檢查涉及分區(qū)表的查詢非常有用。參考:Section?22.3.5, “Obtaining Information About Partitions”.
6、FORMAT 選項(xiàng)可以用于選擇輸出格式。TRADITIONAL 以表格的形式展示。如果沒有指定 FORMAT 選項(xiàng),TRADITIONAL 就是默認(rèn)的。JSON 格式會(huì)以 json 格式展示 EXPLAIN 信息。例如:EXPLAIN FORMAT = JSON SELECT... 。
在 EXPLAIN 的幫助下,你可以清楚的知道為了讓查詢變得更快,該在哪里給表添加索引。你也可以知道優(yōu)化器是否以最佳的順序連接各個(gè)表。為了讓優(yōu)化器使用 SELECT 語句中表的命名順序連接各表,以 SELECT? STRAIGHT_JOIN(而不是SELECT)開頭即可。(參考:Section?13.2.9, “SELECT Statement”)但是,STRAIGHT_JOIN?可能會(huì)妨礙索引的使用,因?yàn)樗昧税脒B接轉(zhuǎn)換(because it disables semijoin transformations.?)。參考:Section?8.2.2.1, “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”.
優(yōu)化器跟蹤(The optimizer trace)有時(shí)可能提供與 EXPLAIN 互補(bǔ)的信息。但是,優(yōu)化器跟蹤的格式和內(nèi)容會(huì)受不同版本的影響。更多細(xì)節(jié),參考:MySQL Internals: Tracing the Optimizer.
如果你對(duì)本應(yīng)該使用索引而沒有用到索引的情況感到疑惑,執(zhí)行一下 ANALYZE TABLE 來更新表統(tǒng)計(jì)信息。例如列的基數(shù)(cardinality of keys),這會(huì)影響優(yōu)化器做出的選擇。參考:Section?13.7.2.1, “ANALYZE TABLE Statement”.
注意:
EXPLAIN 還可以用來獲取表的列的信息。
EXPLAIN? tb1_name??與??DESCRIBE? tb1_name??、?SHOW? COLUMNS? FROM? tb1_name? 是等價(jià)的。
更多信息,參考:Section?13.8.1, “DESCRIBE Statement”,和??Section?13.7.5.5, “SHOW COLUMNS Statement”。
二、EXPLAIN 的輸出格式
EXPLAIN會(huì)為 select 語句中的每張表返回一行信息。并會(huì)以MySQL處理語句時(shí)讀取這些表的順序羅列它們。
MySQL 解決所有 join 的方法是使用一個(gè)“嵌套循環(huán)關(guān)聯(lián)”的方法。也就是說,MySQL會(huì)從第一張表中讀取一條記錄,然后找到第二張表中與之匹配的記錄,然后再找第三張表,依此類推。當(dāng)所有的表處理完畢,MySQL會(huì)輸出查詢的列并回溯表列表(table list),直到找到一個(gè)有更多行的表(譯者注:連接表的時(shí)候,主表查詢出的記錄往往是最多的,從主表開始關(guān)聯(lián)查詢,再回溯到主表,可能官網(wǎng)想表達(dá)的是這個(gè)意思)。下一條記錄會(huì)從該表中讀取,并且繼續(xù)處理下一張表。
EXPLAIN 輸出包含了分區(qū)信息(partitions 列)。同樣,對(duì)于 SELECT 語句,EXPLAIN 會(huì)生成擴(kuò)展信息,只要在 EXPLAIN 執(zhí)行完成后,直接執(zhí)行 SHOW WARNINGS 即可。參考:Section?8.8.3, “Extended EXPLAIN Output Format”
注意
舊的MySQL版本中, 分區(qū)和擴(kuò)展信息使用?EXPLAIN PARTITIONS?和?EXPLAIN EXTENDED?輸出。這些語法依然向后兼容,但是分區(qū)和擴(kuò)展輸出現(xiàn)在默認(rèn)都是開啟的了。所以?PARTITIONS?和?EXTENDED?關(guān)鍵字完全多余。未來版本也會(huì)移除的。
不可以在EXPLAIN語句中同時(shí)使用 PARTITIONS 和 EXTENDED 關(guān)鍵字。另外,哪一個(gè)都不能和 FORMAT 選項(xiàng)一起使用。
MySQL Workbench (譯者注:這是一款由 MySQL 官方出品的,類似 Navicat 的數(shù)據(jù)庫管理工具)有一個(gè) Visual Explain 功能,可以提供可視化的 EXPLAIN 輸出信息。參考:Tutorial: Using Explain to Improve Query Performance.
2.1 EXPLAIN 輸出字段(EXPLAIN Output Columns)
這一節(jié)描述了 EXPLAIN 的輸出字段。后面的兩節(jié)則提供了更多的關(guān)于 type 和 Extra 字段的信息。
EXPLAIN輸出的每一行都對(duì)應(yīng)一張表。下面的表提供了EXPLAIN的輸出字段,第一列是字段名稱,第二列是當(dāng) FORMAT = JSON 時(shí)的輸出字段名稱:
| id | select_id | The?SELECT?identifier:查詢id |
| select_type | None | The?SELECT?type:查詢類型 |
| table | table_name | The table for the output row:對(duì)應(yīng)的表 |
| partitions | partitions | The matching partitions:匹配的分區(qū) |
| 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:用到的索引長(zhǎng)度 |
| ref | ref | The columns compared to the index:與索引比較的列 |
| rows | rows | Estimate of rows to be examined:大約要檢索的行數(shù) |
| filtered | filtered | Percentage of rows filtered by table condition:按表?xiàng)l件過濾的行的百分比 |
| Extra | None | Additional information:附加信息 |
1、id(JSON 名:select_id)
SELECT 標(biāo)識(shí)符(SELECT identifier)。這是一個(gè)連續(xù)的數(shù)字,用以標(biāo)識(shí)查詢中的 SELECT 。如果引用了其他行的聯(lián)合結(jié)果集(union result of other rows),那么 id 會(huì)為 NULL。這種情況下,該行的 table 字段會(huì)顯示為 <union M, N> 這樣的形式,表示該行代表了?id 值為 M 和 N 的行的聯(lián)合(the row refers to the union of the rows with?id?values of?M?and?N?)。
重點(diǎn):id 是一個(gè)自然數(shù)編號(hào),如1、2,但有時(shí)也可以是NULL。如上所述,NULL的時(shí)候,就是引用了一個(gè) UNION 結(jié)果集。
當(dāng) id 為數(shù)字的時(shí)候,編號(hào)大的會(huì)先執(zhí)行。有時(shí)候,編號(hào)會(huì)相同,相同編號(hào)就從上到下執(zhí)行。
2、select_type(JSON 名:無)
查詢類型。MySQL將查詢分為簡(jiǎn)單和復(fù)雜類型,復(fù)雜類型可分為三大類:簡(jiǎn)單子查詢、FROM子查詢,以及UNION查詢。select_type 就是用于區(qū)分這三類復(fù)雜查詢。可選值如下(紅色標(biāo)記為常見值):
| SIMPLE | None | 簡(jiǎn)單查詢(沒有任何 UNION 或 子查詢)。 |
| PRIMARY | None | 主查詢,如果查詢中包含任何復(fù)雜的子部分,那么最外層查詢被標(biāo)記PRIMARY。 |
| UNION | None | UNION 中的第二個(gè)或后面的SELECT語句 |
| DEPENDENT UNION | dependent?(true) | UNION 中的第二個(gè)或后面的SELECT語句, 依賴于外部查詢 |
| UNION RESULT | union_result | 從UNION 的結(jié)果獲取數(shù)據(jù)的SELECT。 |
| SUBQUERY | None | SELECT子句或WHERE子句中的子查詢 |
| DEPENDENT SUBQUERY | dependent?(true) | 子查詢中的第一個(gè) SELECT, 依賴于外層查詢 |
| DERIVED | None | 派生表。FROM子句中的子查詢。MySQL會(huì)遞歸執(zhí)行這些子查詢,把結(jié)果放在臨時(shí)表里。 |
| MATERIALIZED | materialized_from_subquery | Materialized subquery? 物化子查詢。參考《MySQL高級(jí) —— 查詢性能優(yōu)化》4.1節(jié) |
| UNCACHEABLE SUBQUERY | cacheable?(false) | 非緩存子查詢,結(jié)果不能被緩存的子查詢,必須被外部查詢的每一行重新求得 |
| UNCACHEABLE UNION | cacheable?(false) | 非緩存子查詢(uncacheable subquery)的 UNION 中的第二個(gè)或后面的 SELECT |
SUBQUERY還可以被標(biāo)記為DEPENDENT SUBQUERY,這一般是指SELECT依賴于外層查詢發(fā)現(xiàn)的數(shù)據(jù)(很可能是依賴于FROM派生表的外層SELECT)。參考:Section?13.2.10.7, “Correlated Subqueries”?。
DEPENDENT SUBQUERY 的取值與 UNCACHEABLE SUBQUERY(由于用戶變量等原因)?的取值不同。對(duì)于 DEPENDENT SUBQUERY ,對(duì)于來自其外部查詢的變量的每組不同值,子查詢只重新計(jì)算一次。而對(duì)于 UNCACHEABLE SUBQUERY ,對(duì)外部查詢的每行記錄,該子查詢都會(huì)計(jì)算一遍。
子查詢緩存與緩存中的查詢結(jié)果緩存不一樣(具體描述參考?Section?8.10.3.1, “How the Query Cache Operates”)。子查詢緩存發(fā)生在查詢執(zhí)行過程中,而查詢結(jié)果緩存只在查詢執(zhí)行完畢時(shí)才會(huì)存儲(chǔ)結(jié)果。
當(dāng)你在 EXPLAIN 語句中指定了 FORMAT = JSON ,輸出的結(jié)果并沒有一個(gè)對(duì)應(yīng) select_type 的單獨(dú)屬性;query_block 屬性對(duì)應(yīng)給定的 SELECT 。與剛才顯示的大多數(shù) SELECT 子查詢類型等價(jià)的屬性都是有的,并且在合適的時(shí)機(jī)就會(huì)展示。不過并沒有與 SIMPLE 和 PRIMARY 等價(jià)的 JSON 值。
select_type 屬性值對(duì)于非 SELECT 語句,會(huì)展示影響表的語句類型,如 DELETE 語句的 select_type 就是 DELETE。
3、table(JSON 名:table_name)
explain 輸出的每一行都對(duì)應(yīng)一個(gè)表別名或表名。它可以是下面的值中的一個(gè):
<union M, N> : 這一行引用了 id 值為 M 和 N 的表的聯(lián)合。
<derived N> : 這一行引用了 id 值為 N 的表所派生的表。派生的表可能是一個(gè)結(jié)果集,比如,FROM 子句中的子查詢。
<subquery N> : 這一行引用了 id 值為 N 的物化子查詢的結(jié)果。參考:Section?8.2.2.2, “Optimizing Subqueries with Materialization”.
4、partitions(JSON 名:partitions)
查詢的記錄將會(huì)在哪個(gè)分區(qū)中匹配。NULL 代表沒有分區(qū)表。參考:?Section?22.3.5, “Obtaining Information About Partitions”.
5、type(JSON 名:access_type)
關(guān)聯(lián)類型,但更準(zhǔn)確的說法是——訪問類型,換言之就是MySQL決定如何查找表中的行。參考 2.2 節(jié)。
6、possible_keys(JSON 名:possible_keys)
該屬性可以表明查詢中,對(duì)應(yīng)表有哪些索引可以使用。注意這個(gè)屬性完全不依賴于表在 explain 輸出中的顯示順序。也就是說,以生成的表順序 ,possible_keys 中的有些索引可能實(shí)際中并不會(huì)用到。
如果該屬性是 NULL (或者在 JSON 格式中是 undefined ),代表沒有相關(guān)的索引。這時(shí),你可能就應(yīng)該努力通過調(diào)試 WHERE 子句來提升你的查詢性能,檢查是否涉及到了一些字段或者適合索引查詢的字段。如果有,就創(chuàng)建一個(gè)合適的索引,然后再次通過 EXPLAIN 進(jìn)行檢驗(yàn)。
查看一個(gè)表有哪些索引,可以使用 SHOW INDEX FROM tbl_name 語句。
7、key(JSON 名:key)
這一列表示 MySQL 決定采用哪個(gè)索引來優(yōu)化對(duì)該表的訪問。如果 MySQL 決定使用 possible_keys 中的一個(gè)索引去查找記錄,那么這個(gè)索引就會(huì)列在 key 屬性中。
key 中也會(huì)出現(xiàn) possible_key 中沒有出現(xiàn)的索引。發(fā)生這種情況,很可能是 possible_keys 沒有找到適合查詢的索引,但是所有查詢的字段都在索引中。也就是說,查詢使用了覆蓋索引。因此,盡管它不用于決定要查詢哪些行,但卻依然可以用于查詢字段,因?yàn)樗饕龗呙枰廊槐刃袙呙韪咝АQ句話說,possible_keys 揭示了哪一個(gè)索引能有助于高效地行查找,而 key 顯示的是優(yōu)化采用哪一個(gè)索引可以最小化查詢成本。
對(duì)于InnoDB ,即使查詢列表中有主鍵,二級(jí)索引也可能覆蓋所查詢的字段,因?yàn)?strong>InnoDB用每個(gè)二級(jí)索引存儲(chǔ)了主鍵值。如果列是NULL, MySQL就找不到索引來更有效地執(zhí)行查詢。
要強(qiáng)制MySQL使用或忽略在 possiblele_keys 中列出的索引,請(qǐng)?jiān)诓樵冎惺褂?FORCE INDEX,USE INDEX?或?IGNORE INDEX 。參考:?Section?8.9.4, “Index Hints”.
對(duì)于 MyISAM,運(yùn)行 ANALYZE TABLE 可以幫助優(yōu)化器選擇更好的索引。對(duì)于 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 在索引里使用的字節(jié)數(shù)。
因?yàn)閗ey_len是通過查找表的定義而被計(jì)算出,而不是表中的數(shù)據(jù),因此它顯示了在索引字段中可能的最大長(zhǎng)度,而不是表中數(shù)據(jù)使用的實(shí)際字節(jié)數(shù)。key_len 的值可以讓你判斷?MySQL 究竟用到了復(fù)合索引的哪幾個(gè)索引列。如果 key 屬性的值為 NULL , 那么 key_len 肯定也是 NULL 。
由于索引的存儲(chǔ)格式,那些可以為 NULL 的字段的索引長(zhǎng)度要比非空字段的索引長(zhǎng)度大一些。
MySQL并不總是顯示一個(gè)索引真正使用了多少。例如,如果對(duì)一個(gè)前綴模式匹配(例如 '張%')執(zhí)行LIKE查詢,它會(huì)顯示列的完整寬度正在被使用。
計(jì)算 key_len 的簡(jiǎn)易方法:
int 類型在MySQL中以4個(gè)字節(jié)存儲(chǔ),key_len 為 4,如果列值允許為 NULL,那么需要 + 1,即 key_len 為 5.
double 類型以8個(gè)字節(jié)存儲(chǔ),key_len 為 8,如果允許 NULL,那么同樣 +1, 即 key_len 為 9.
char(n) 定長(zhǎng)字符串,首先需要看字符集,常見的utf8以3個(gè)字節(jié)存儲(chǔ)每個(gè)字符,gbk用2個(gè),latin用1個(gè)。key_len 就等于每個(gè)字節(jié)長(zhǎng)度乘以允許最大字符數(shù)n,如果允許NULL,key_len 也要 +1。例如 char(20) DEFAULT NULL,編碼為utf8 ,那么 key_len 就是 3 × 20 + 1 = 61。如果不允許為 NULL ,就是60。
varchar(n)變長(zhǎng)字符串,每個(gè)字符:utf8為3字節(jié)、gbk為2字節(jié)、latin為1字節(jié)。由于是變長(zhǎng),因此 key_len 要 +2,如果允許 NULL,同樣 +1。其他和 char計(jì)算方式一樣。例如,varchar(20) DEFAULT NULL,編碼 utf8,那么 key_len 就是:
3 × 20 + 2 + 1 = 63,如果不允許為 NULL,就是62。
上面的說明只是單獨(dú)計(jì)算每種列值類型的方法,如果是復(fù)合索引,那么key_len 就是用到的索引列長(zhǎng)度和。
9、ref(JSON 名:ref)
ref 列顯示了常量或哪些列與 key 列中的索引進(jìn)行了比較。只有 type 列是 ref 的時(shí)候,ref 列才會(huì)有值。
簡(jiǎn)單的說,就是 key 中的索引,如果與一個(gè)常量比較,那么 ref 會(huì)顯示 const,如果是與其他表的某個(gè)列進(jìn)行比較,那么就會(huì)顯示該列名。
如果 ref 屬性的值是 func ,那么用到的值就是某些函數(shù)的結(jié)果。想要知道是哪個(gè)函數(shù),在 EXPLAIN 執(zhí)行后使用 SHOW WARNINGS ,查看EXPLAIN 的擴(kuò)展信息。
函數(shù)實(shí)際上可能是一個(gè)運(yùn)算符,比如算術(shù)運(yùn)算符。
10、rows(JSON 名:rows)
rows 列表示MySQL認(rèn)為執(zhí)行查詢必須檢查的行數(shù)。這個(gè)數(shù)字是內(nèi)嵌關(guān)聯(lián)循環(huán)計(jì)劃里的循環(huán)數(shù)目。也就是說,它不是最終的結(jié)果集里的行數(shù),而是MySQL為了找到符合條件的結(jié)果集而必須讀取的行的平均數(shù)。
對(duì)于 InnoDB 表,這個(gè)數(shù)是一個(gè)估值,而且可能并不總是準(zhǔn)確的。
11、filtered(JSON 名:filtered)
filtered 屬性表示被篩選條件過濾掉的記錄條數(shù)占全表的估計(jì)百分比。最大值是100,意味著記錄全部被過濾掉。從100開始遞減的值表示過濾的量在增加。rows 屬性表示了需要檢查的估計(jì)行數(shù),rows 乘 filtered 表示了將會(huì)被后面的表關(guān)聯(lián)的記錄條數(shù)。例如,如果 rows 是1000,filtered 是 50.00(50%),那么要與后面的表連接的記錄條數(shù)就是 1000 × 50% = 500。
對(duì)于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.? 這里面有一個(gè)語義上的陷阱,即 filtered 究竟表示的是 “被過濾掉的” ?還是 “過濾后(留下來)的” ,經(jīng)過本人測(cè)試,filtered 表示的是前者,即 “被過濾掉的” ,這樣后面的語義也就基本自洽了。而 filtering 則表示 “過濾后(留下來)的” 。
12、Extra(JSON 名:none)
這一列顯示了關(guān)于 MySQL如何處理查詢的額外信息。對(duì)于不同值的描述,參考:Extra Information. 或參考下面 2.3 節(jié)。
2.2 EXPLAIN type訪問類型(EXPLAIN Join Types)
type 屬性描述了表之間是如何連接(或關(guān)聯(lián))的。在 JSON 格式輸出中,對(duì)應(yīng) access_type 屬性。下面的列表描述了訪問類型,順序從“最理想類型”到“最糟糕的類型”:
system > const > eq_ref > ref > range > index > ALL
2.2.1 system(不常見)
表只有一行(=系統(tǒng)表)。是 const 連接類型的一種特殊情況。
2.2.2 const
表最多只有 1 條匹配記錄,在查詢開始時(shí)就會(huì)讀取該表。因?yàn)橹挥幸恍?#xff0c;所以這一行中列的值可以被其他優(yōu)化器視為常量。const 訪問類型非常快,因?yàn)樗麄冎粫?huì)被讀取一次。MySQL能將這個(gè)查詢轉(zhuǎn)換為一個(gè)常量,然后可以高效地將表從連接操作中移除。
const 會(huì)在你使用整個(gè)主鍵(all parts of a PRIMARY KEY)或唯一索引(UNIQUE index)去比較一個(gè)常量的時(shí)候用到。在下面的查詢中,tb1_name 就是一張 const 表:
SELECT * FROM tbl_name WHERE primary_key=1;SELECT * FROM tbl_nameWHERE primary_key_part1=1 AND primary_key_part2=2;2.2.3 eq_ref
使用這種索引查找,MySQL知道最多只返回一條符合條件的記錄。它會(huì)在所有的索引部分都被用到的時(shí)候以及索引是主鍵或非空唯一索引時(shí)出現(xiàn)到,它會(huì)將它們與某個(gè)參考值做比較。MySQL 對(duì)于這類訪問類型的優(yōu)化做的非常好,因?yàn)?strong>MySQL知道無須估計(jì)匹配行的范圍或在找到匹配行后再繼續(xù)查找。
eq_ref 會(huì)在索引列使用 = 號(hào)的時(shí)候用到。比較的值可以是一個(gè)常量也可以是一個(gè)從前表讀取的列(的表達(dá)式)。在下面的例子中,MySQL 可以使用 eq_ref 類型來處理 ref_table:
SELECT * FROM ref_table,other_tableWHERE ref_table.key_column=other_table.column;SELECT * FROM ref_table,other_tableWHERE ref_table.key_column_part1=other_table.columnAND ref_table.key_column_part2=1;2.2.4 ref
這是一種索引訪問(有時(shí)也叫“索引查找”),它返回所有匹配某個(gè)單個(gè)值的行,是查找和掃描的混合體。此類索引訪問只有當(dāng)使用非唯一性索引或唯一性索引的非唯一性前綴時(shí)才會(huì)發(fā)生。把它叫做 ref 是因?yàn)?strong>索引要跟某個(gè)參考值相比較。這個(gè)參考值可以是一個(gè)常數(shù),或是來自多表查詢的結(jié)果值。如果該篩選列可以匹配少量的記錄,那 ref 還算是一個(gè)不錯(cuò)的連接類型。
ref_or_null 是ref 之上的一個(gè)變體,它意味著MySQL必須在初次查找的結(jié)果里進(jìn)行第二次查找以找出NULL條目。
ref 也可以在索引列使用 = 或 <=> 號(hào)的時(shí)候被用到。下面的例子,MySQL 可以使用 ref 來處理 ref_table:
SELECT * FROM ref_table WHERE key_column=expr;SELECT * FROM ref_table,other_tableWHERE ref_table.key_column=other_table.column;SELECT * FROM ref_table,other_tableWHERE ref_table.key_column_part1=other_table.columnAND ref_table.key_column_part2=1;2.2.5 full_text(不常見)
這種連接方式會(huì)在使用 FULLTEXT 索引的時(shí)候用到。
2.2.6 ref_or_null(不常見)
這種連接方式和 ref 類似,除此之外, MySQL 還會(huì)額外搜索包含 NULL 值的記錄。這種連接類型的優(yōu)化絕大多數(shù)是在處理子查詢的時(shí)候。在下面的例子中, MySQL 會(huì)使用 ref_or_null 來處理 ref_table:
SELECT * FROM ref_tableWHERE key_column=expr OR key_column IS NULL;參考:Section?8.2.1.13, “IS NULL Optimization”.?
2.2.7 index_merge(不常見)
這種連接類型表示使用了索引合并優(yōu)化(Index Merge optimization)。這種情況下,explain 中的 key 屬性會(huì)羅列出被用到的索引,key_len 屬性會(huì)列出用到的索引的最長(zhǎng)的索引部分。參考:Section?8.2.1.3, “Index Merge Optimization”.
2.2.8 unique_subquery(不常見)
這種類型在類似下面的一些使用 IN 的子查詢時(shí)取代了 eq_ref:
value IN (SELECT primary_key FROM single_table WHERE some_expr)?unique_subquery?只是一個(gè)索引查找函數(shù),它完全取代了子查詢,以提高效率。
2.2.9 index_subquery(不常見)
這種連接類型有點(diǎn)像 unique_subquery 。它取代了 IN 子查詢,但它只在子查詢中有非唯一索引時(shí)才會(huì)起作用,類似下面這樣:
value IN (SELECT key_column FROM single_table WHERE some_expr)2.2.10 range?
這種連接類型會(huì)使用索引查詢給定范圍內(nèi)的記錄。EXPLAIN 輸出中的 key 屬性表示了哪個(gè) 索引列 被用到。key_len 包含了被用到的最長(zhǎng)的索引部分。ref 屬性為 NULL。
range 類型會(huì)在索引列使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、或 IN() 任意一種操作符去比較常量的時(shí)候被用到。當(dāng)使用 IN或 OR 列表的時(shí)候,顯示的范圍掃描,其實(shí)并不能和 > 這類比較符的性能等同,雖然它們?cè)贓XPLAIN中顯示的類型都是 range,但是 IN() 列表其實(shí)屬于等值列表。參考《MySQL高級(jí) —— 高性能索引》6.2 節(jié)。
SELECT * FROM tbl_nameWHERE key_column = 10;SELECT * FROM tbl_nameWHERE key_column BETWEEN 10 and 20;SELECT * FROM tbl_nameWHERE key_column IN (10,20,30);SELECT * FROM tbl_nameWHERE key_part1 = 10 AND key_part2 IN (10,20,30);2.2.11 index
index 類型除了會(huì)掃描索引樹之外,其他和 ALL 是一樣的。會(huì)有兩種情況出現(xiàn):
1、如果索引是一個(gè)覆蓋索引,那么這種類型的查詢就只會(huì)掃描索引樹。這種情況下, Extra 屬性會(huì)顯示 Using Index。一個(gè)只掃描索引的方式比 ALL 更快,這是因?yàn)樗饕龜?shù)據(jù)肯定要比表中數(shù)據(jù)要少。
2、以索引次序掃描全表。Extra 不會(huì)顯示 Uses Index。
index 類型的主要優(yōu)點(diǎn)是避免了排序,最大缺點(diǎn)是要承擔(dān)按索引次序讀取整個(gè)表的開銷。
MySQL 會(huì)在查詢只用到了單一索引列的時(shí)候用到 index 這種類型。
2.2.12 ALL
這就是人們常說的“全表掃描”,這種類型會(huì)對(duì)前面各表的組合記錄都進(jìn)行全表掃描。如果表是第一個(gè)沒有被標(biāo)記為 const 的表,這通常是不好的,在所有其他情況下通常是非常糟糕的。通常你可以通過增加索引來避免 ALL 。但也有例外,例如在查詢中使用了 LIMIT,或在 Extra 列中顯示“Using distinct/not exists”。
2.3 EXPLAIN Extra 信息(EXPLAIN Extra Information)
Extra 屬性顯示了MySQL如何執(zhí)行查詢的額外信息。
2.3.1 Using index
此值表示MySQL將使用覆蓋索引,以避免訪問表。不要把覆蓋索引和 type = index 訪問類型混淆了。
2.3.2 Using where
這意味著MySQL服務(wù)器將在存儲(chǔ)引擎檢索行后再進(jìn)行過濾。當(dāng)它讀取索引時(shí),就能被存儲(chǔ)引擎檢驗(yàn),因此不是所有帶有 WHERE子句的查詢都會(huì)顯示“Using where” 。有時(shí)“Using where” 的出現(xiàn)就是一個(gè)暗示:查詢可受益于不同的索引。
2.3.3 Using temporary
這意味著MySQL在對(duì)查詢結(jié)果排序時(shí)會(huì)使用一個(gè)臨時(shí)表。
2.3.4 Using filesort
這意味著MySQL會(huì)對(duì)結(jié)果使用一個(gè)外部索引排序,而不是按照索引次序從表里讀取行。MySQL有兩種文件排序算法,兩種方式都可以在內(nèi)存或磁盤上完成。EXPLAIN 不會(huì)告訴你 MySQL將使用哪一種文件排序,也不會(huì)告訴你排序會(huì)在內(nèi)存里還是在磁盤上完成。
2.3.5 Range checked for each record (index map:N)
這個(gè)值意味著沒有好用的索引,新的索引將在連接的每一行上重新估算。N是顯示在possible_keys 列中索引的位圖,并且是冗余的。
?
2.4 EXPLAIN 輸出的解釋
EXPLAIN輸出可以給你在連接各種表查詢的時(shí)候一個(gè)非常好的指示作用。這會(huì)大致告訴你MySQL 在執(zhí)行查詢的時(shí)候必須要檢查多少行記錄。如果你限制了 max_join_size 系統(tǒng)變量,那么 EXPLAIN 也會(huì)被用來告訴我們一些有用的東西。參考:?Section?5.1.1, “Configuring the Server”.
下面的例子顯示了多表連接是如何基于 EXPLAIN 提供的信息一點(diǎn)點(diǎn)優(yōu)化的。
假設(shè)你有一個(gè)查詢語句,并且你通過 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.CUSTNAMEFROM tt, et, et AS et_1, doWHERE tt.SubmitTime IS NULLAND tt.ActualPC = et.EMPLOYIDAND tt.AssignedPC = et_1.EMPLOYIDAND tt.ClientID = do.CUSTNMBR;對(duì)于這個(gè)例子,做出下面的假設(shè):
1、比較的列(譯者注:columns being compared,實(shí)際上指的就是where 子句后面作為篩選條件的列,因?yàn)橥枰玫?= 號(hào)等操作符,因此在官網(wǎng)中一般都被稱為被比較的列)定義如下:
| tt | ActualPC | CHAR(10) |
| tt | AssignedPC | CHAR(10) |
| tt | ClientID | CHAR(10) |
| et | EMPLOYID | CHAR(15) |
| do | CUSTNMBR | CHAR(15) |
2、表有以下這些索引:
| tt | ActualPC |
| tt | AssignedPC |
| tt | ClientID |
| et | EMPLOYID?(primary key) |
| do | CUSTNMBR?(primary key) |
3、tt 表的?ActualPC 字段不是均勻分布的。
首先,在所有優(yōu)化執(zhí)行之前, 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 3872ClientID,ActualPCRange checked for each record (index map: 0x23)因?yàn)槊繌埍淼倪B接類型都是 ALL ,這表明MySQL 正在生成一張笛卡爾集(a Cartesian product),也就是表中的每一行都進(jìn)行了組合。這會(huì)花費(fèi)相當(dāng)長(zhǎng)的時(shí)間,因?yàn)楸仨殭z查每個(gè)表中行數(shù)的乘積。對(duì)于這個(gè)案例,乘積就是:74 × 2135 × 74 × 3872 = 45,268,558,720 行。如果表再大一點(diǎn),你可以想象一下它需要花費(fèi)多長(zhǎng)時(shí)間。
這里有個(gè)問題,如果比較的列被聲明以相同的大小和類型,那么?MySQL 就可以更高效的使用列上的索引。在這種語境下,VARCHAR 和 CHAR 如果被設(shè)定為相同的大小,那么就被認(rèn)為是相同的。tt.ActualPC 被聲明為 CHAR(10) 而 et.EMPLOYID 聲明為 CHAR(15),所以長(zhǎng)度不匹配。
為了修復(fù)這種列長(zhǎng)度的不一致,使用 ALTER TABLE 來延長(zhǎng) ActualPC ,從 10個(gè)字符到15個(gè)字符。
mysql> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);現(xiàn)在 tt.ActualPC 和 et.EMPLOYID 都是 VARCHAR(15) 了。再次執(zhí)行 EXPLAIN 就會(huì)得到下面的結(jié)果:
table type possible_keys key key_len ref rows Extra tt ALL AssignedPC, NULL NULL NULL 3872 UsingClientID, whereActualPC do ALL PRIMARY NULL NULL NULL 2135Range checked for each record (index map: 0x1) et_1 ALL PRIMARY NULL NULL NULL 74Range checked for each record (index map: 0x1) et eq_ref PRIMARY PRIMARY 15 tt.ActualPC 1這依然不夠完美,但是也稍微好了點(diǎn):rows 的乘積少了 74 倍(譯者注:et 表的 rows 由 74 變?yōu)榱?1)。這一版的執(zhí)行會(huì)在幾秒鐘完成。
第二處修改可以針對(duì) tt.AssignedPC = et_1.EMPLOYID 和 tt.ClientID = do.SUTNMBR 這兩個(gè)比較中有關(guān)列長(zhǎng)度不匹配的問題。
mysql> ALTER TABLE tt MODIFY AssignedPC VARCHAR(15),MODIFY ClientID VARCHAR(15);這次修改之后,EXPLAIN 輸出就會(huì)變成下面這樣:
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 UsingClientID, whereActualPC et_1 eq_ref PRIMARY PRIMARY 15 tt.AssignedPC 1 do eq_ref PRIMARY PRIMARY 15 tt.ClientID 1此時(shí),查詢幾乎已經(jīng)優(yōu)化的足夠好了。遺留的問題是,默認(rèn)情況下,MySQL 假設(shè) tt.ActualPC 字段上的值是均勻分布的,但 tt 表并不是這樣的(前面的假設(shè))。幸運(yùn)的是,要告訴 MySQL 分析列值分布情況是非常簡(jiǎn)單的,你只需要這樣做:
mysql> ANALYZE TABLE tt;憑借額外的索引信息,連接查詢已經(jīng)變得完美,EXPLAIN 也變成了如下結(jié)果:
table type possible_keys key key_len ref rows Extra tt ALL AssignedPC NULL NULL NULL 3872 UsingClientID, whereActualPC 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 1EXPLAIN輸出中的rows列是來自MySQL連接優(yōu)化器的猜測(cè)。通過將 rows 的乘積與查詢返回的實(shí)際行數(shù)進(jìn)行比較,就可以檢查這些數(shù)字是否接近實(shí)際情況。如果數(shù)字與實(shí)際查詢的行數(shù)相差甚遠(yuǎn),你可以通過在你的 SELECT 語句中使用 STRAIGHT_JOIN 并嘗試在 FROM 子句中以不同的順序羅列所查各表來獲取更好的性能。(但是,STRAIGHT_JOIN 可能會(huì)妨礙到索引的使用,因?yàn)樗昧税脒B接轉(zhuǎn)換。參考:Section?8.2.2.1, “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”.)
在某些情況下,當(dāng)EXPLAIN SELECT與子查詢一起使用時(shí),可以執(zhí)行修改數(shù)據(jù)的語句。參考:Section?13.2.10.8, “Derived Tables”.
總結(jié)
這篇譯文翻譯了很長(zhǎng)時(shí)間,斷斷續(xù)續(xù)可能有一個(gè)月。本篇文章有些地方可能翻譯的并不準(zhǔn)確,因此希望各位可以與原文比較閱讀,增加理解。
另外,本來想在 Extra 部分就結(jié)束本篇翻譯,沒想到 MySQL 官網(wǎng)在最后一節(jié)給出了一個(gè)非常親民的案例講解,可以讓我們一覽 EXPLAIN 的常規(guī)用法。這一部分也是我認(rèn)為翻譯的比較準(zhǔn)確的部分。
因?yàn)?EXPLAIN 語句非常重要,因此,這篇譯文我也會(huì)經(jīng)常翻閱,加深理解的同時(shí)不斷糾正文中翻譯的不準(zhǔn)確或有所偏頗之處,同時(shí)希望大家能給予意見或建議。
2020-05-29 追加的部分,分散在文章的各個(gè)小節(jié)中,主要是在讀完《高性能MySQL(第三版)》的五六章,以及附錄EXPLAIN的部分,對(duì)執(zhí)行計(jì)劃和一些索引的概念有了更進(jìn)一步的理解和認(rèn)識(shí)。之前翻譯的不是很準(zhǔn)確的地方做了校對(duì)和潤(rùn)色,某些廢話也是能刪就刪,我還寫了很多關(guān)于索引及查詢優(yōu)化相關(guān)的文章,可以和這些文章一起閱讀,結(jié)合實(shí)踐并反復(fù)回看的話,相信一定可以成為MySQL性能優(yōu)化領(lǐng)域的好手。
?
總結(jié)
以上是生活随笔為你收集整理的MySQL 优化 —— EXPLAIN 执行计划详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IDEA——常用代码模板
- 下一篇: Redis 基础——五大类型与数据结构