你確定你讀懂了 MySQL 執(zhí)行計(jì)劃嗎?
作者: 不剪發(fā)的Tony老師
畢業(yè)于北京航空航天大學(xué),十多年數(shù)據(jù)庫(kù)管理與開(kāi)發(fā)經(jīng)驗(yàn),目前在一家全球性的金融公司從事數(shù)據(jù)庫(kù)架構(gòu)設(shè)計(jì)。CSDN學(xué)院簽約講師以及GitChat專(zhuān)欄作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net
文章目錄
獲取執(zhí)行計(jì)劃
解讀執(zhí)行計(jì)劃
type 字段
Extra 字段
訪(fǎng)問(wèn)謂詞與過(guò)濾謂詞
完整字段信息
格式化參數(shù)
執(zhí)行計(jì)劃中的分區(qū)表信息
獲取額外的執(zhí)行計(jì)劃信息
獲取指定連接的執(zhí)行計(jì)劃
獲取實(shí)際運(yùn)行的執(zhí)行計(jì)劃
大家好,我是只談技術(shù)不剪發(fā)的 Tony 老師。今天給大家深入分析一下 MySQL 中的執(zhí)行計(jì)劃。
執(zhí)行計(jì)劃(execution plan,也叫查詢(xún)計(jì)劃或者解釋計(jì)劃)是 MySQL 服務(wù)器執(zhí)行 SQL 語(yǔ)句的具體步驟。例如,通過(guò)索引還是全表掃描訪(fǎng)問(wèn)表中的數(shù)據(jù),連接查詢(xún)的實(shí)現(xiàn)方式和連接的順序,分組和排序操作的實(shí)現(xiàn)方式等。
負(fù)責(zé)生成執(zhí)行計(jì)劃的組件就是優(yōu)化器,優(yōu)化器利用表結(jié)構(gòu)、字段、索引、查詢(xún)條件、數(shù)據(jù)庫(kù)的統(tǒng)計(jì)信息和配置參數(shù)決定 SQL 語(yǔ)句的最佳執(zhí)行方式。如果想要解決慢查詢(xún)的性能問(wèn)題,首先應(yīng)該查看它的執(zhí)行計(jì)劃。
獲取執(zhí)行計(jì)劃
MySQL 提供了 EXPLAIN 語(yǔ)句,用于獲取 SQL 語(yǔ)句的執(zhí)行計(jì)劃。該語(yǔ)句的基本形式如下:
{EXPLAIN | DESCRIBE | DESC}
{
SELECT statement
| TABLE statement
| DELETE statement
| INSERT statement
| REPLACE statement
| UPDATE statement
}
EXPLAIN和DESCRIBE是同義詞,可以通用。實(shí)際應(yīng)用中,DESCRIBE主要用于查看表的結(jié)構(gòu),EXPLAIN主要用于獲取執(zhí)行計(jì)劃。MySQL 可以獲取 SELECT、INSERT、DELETE、UPDATE、REPLACE 等語(yǔ)句的執(zhí)行計(jì)劃。從 MySQL 8.0.19 開(kāi)始,支持 TABLE 語(yǔ)句的執(zhí)行計(jì)劃。
舉例來(lái)說(shuō):
explain
select *
from employee;
id|select_type|table |partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
--|-----------|--------|----------|----|-------------|---|-------|---|----|--------|-----|
1|SIMPLE |employee| |ALL | | | | | 25| 100.0| |
MySQL 中的執(zhí)行計(jì)劃包含了 12 列信息,這些字段的含義我們?cè)谙挛闹羞M(jìn)行解讀。
除了使用 EXPLAIN 語(yǔ)句之外,很多管理和開(kāi)發(fā)工具都提供了查看圖形化執(zhí)行計(jì)劃的功能,例如 MySQL Workbench 中顯示以上查詢(xún)的執(zhí)行計(jì)劃如下:
當(dāng)然,這種方式最終也是執(zhí)行了 EXPLAIN 語(yǔ)句。
解讀執(zhí)行計(jì)劃
理解執(zhí)行計(jì)劃中每個(gè)字段的含義可以幫助我們知悉 MySQL 內(nèi)部的操作過(guò)程,找到性能問(wèn)題的所在并有針對(duì)性地進(jìn)行優(yōu)化。在執(zhí)行計(jì)劃的輸出信息中,最重要的字段就是 type。
type 字段
type 被稱(chēng)為連接類(lèi)型(join type)或者訪(fǎng)問(wèn)類(lèi)型(access type),它顯示了 MySQL 如何訪(fǎng)問(wèn)表中的數(shù)據(jù)。
訪(fǎng)問(wèn)類(lèi)型會(huì)直接影響到查詢(xún)語(yǔ)句的性能,性能從好到差依次為:
system,表中只有一行數(shù)據(jù)(系統(tǒng)表),這是 const 類(lèi)型的特殊情況;
const,最多返回一條匹配的數(shù)據(jù),在查詢(xún)的最開(kāi)始讀??;
eq_ref,對(duì)于前面的每一行,從該表中讀取一行數(shù)據(jù);
ref,對(duì)于前面的每一行,從該表中讀取匹配索引值的所有數(shù)據(jù)行;
fulltext,通過(guò) FULLTEXT 索引查找數(shù)據(jù);
ref_or_null,與 ref 類(lèi)似,額外加上 NULL 值查找;
index_merge,使用索引合并優(yōu)化技術(shù),此時(shí) key 列顯示使用的所有索引;
unique_subquery,替代以下情況時(shí)的 eq_ref:value IN (SELECT primary_key FROM single_table WHERE some_expr);
index_subquery,與 unique_subquery 類(lèi)似,用于子查詢(xún)中的非唯一索引:value IN (SELECT key_column FROM single_table WHERE some_expr);
range,使用索引查找范圍值;
index,與 ALL 類(lèi)型相同,只不過(guò)掃描的是索引;
ALL,全表掃描,通常表示存在性能問(wèn)題。
const 和 eq_ref 都意味著著通過(guò) PRIMARY KEY 或者 UNIQUE 索引查找唯一值;它們的區(qū)別在于 const 對(duì)于整個(gè)查詢(xún)只返回一條數(shù)據(jù),eq_ref 對(duì)于前面的結(jié)果集中的每條記錄只返回一條數(shù)據(jù)。例如以下查詢(xún)通過(guò)主鍵(key = PRIMARY)進(jìn)行等值查找:
explain
select *
from employee
where emp_id = 1;
id|select_type|table |partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra|
--|-----------|--------|----------|-----|-------------|-------|-------|-----|----|--------|-----|
1|SIMPLE |employee| |const|PRIMARY |PRIMARY|4 |const| 1| 100.0| |
const 只返回一條數(shù)據(jù),是一種非??焖俚脑L(fǎng)問(wèn)方式,所以相當(dāng)于一個(gè)常量(constant)。
以下語(yǔ)句通過(guò)主鍵等值連接兩個(gè)表:
explain
select *
from employee e
join department d
on (e.dept_id = d.dept_id )
where e.emp_id in(1, 2);
id|select_type|table|partitions|type |possible_keys |key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|------|--------------------|-------|-------|--------------|----|--------|-----------|
1|SIMPLE |e | |range |PRIMARY,idx_emp_dept|PRIMARY|4 | | 2| 100.0|Using where|
1|SIMPLE |d | |eq_ref|PRIMARY |PRIMARY|4 |hrdb.e.dept_id| 1| 100.0| |
對(duì)于 employee 中返回的每一行(table = e),department 表通過(guò)主鍵(key = PRIMARY)返回且僅返回一條數(shù)據(jù)(type = eq_ref)。Extra 字段中的 Using where 表示將經(jīng)過(guò)條件過(guò)濾后的數(shù)據(jù)傳遞給下個(gè)表或者客戶(hù)端。
ref、ref_or_null 以及 range 表示通過(guò)范圍查找所有匹配的索引項(xiàng),然后根據(jù)需要再訪(fǎng)問(wèn)表中的數(shù)據(jù)。通常意味著使用了非唯一索引或者唯一索引的前面部分字段進(jìn)行數(shù)據(jù)訪(fǎng)問(wèn),例如:
explain
select *
from employee e
where e.dept_id = 1;
id|select_type|table|partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra|
--|-----------|-----|----------|----|-------------|------------|-------|-----|----|--------|-----|
1|SIMPLE |e | |ref |idx_emp_dept |idx_emp_dept|4 |const| 3| 100.0| |
explain
select *
from employee e
join department d
on (e.dept_id = d.dept_id )
where d.dept_id = 1;
id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra|
--|-----------|-----|----------|-----|-------------|------------|-------|-----|----|--------|-----|
1|SIMPLE |d | |const|PRIMARY |PRIMARY |4 |const| 1| 100.0| |
1|SIMPLE |e | |ref |idx_emp_dept |idx_emp_dept|4 |const| 3| 100.0| |
以上兩個(gè)查詢(xún)語(yǔ)句都是通過(guò)索引 idx_emp_dept 返回 employee 表中的數(shù)據(jù)。
ref_or_null 和 ref 的區(qū)別在于查詢(xún)中包含了 IS NULL 條件。例如:
alter table employee modify column dept_id int null;
explain
select *
from employee e
where e.dept_id = 1 or dept_id is null;
id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|-----------|-------------|------------|-------|-----|----|--------|---------------------|
1|SIMPLE |e | |ref_or_null|idx_emp_dept |idx_emp_dept|5 |const| 4| 100.0|Using index condition|
其中,Extra 字段顯示為 Using index condition,意味著通過(guò)索引訪(fǎng)問(wèn)表中的數(shù)據(jù)之前,直接通過(guò) WHERE 語(yǔ)句中出現(xiàn)的索引字段條件過(guò)濾數(shù)據(jù)。這是 MySQL 5.6 之后引入了一種優(yōu)化,叫做索引條件下推(Index Condition Pushdown)。
為了顯示 ref_or_null,我們需要將字段 dept_id 設(shè)置為可空,測(cè)試之后記得重新修改為 NOT NULL:
alter table employee modify column dept_id int not null;
range 通常出現(xiàn)在使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE 或者 IN() 運(yùn)算符和索引字段進(jìn)行比較時(shí),例如:
explain
select *
from employee e
where e.email like 'zhang%';
id|select_type|table|partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|-----|-------------|------------|-------|---|----|--------|---------------------|
1|SIMPLE |e | |range|uk_emp_email |uk_emp_email|302 | | 2| 100.0|Using index condition|
index_merge 表示索引合并,當(dāng)查詢(xún)通過(guò)多個(gè)索引 range 訪(fǎng)問(wèn)方式返回?cái)?shù)據(jù)時(shí),MySQL 可以先對(duì)這些索引掃描結(jié)果合并成一個(gè),然后通過(guò)這個(gè)索引獲取表中的數(shù)據(jù)。例如:
explain
select *
from employee e
where dept_id = 1 or job_id = 1;
id|select_type|table|partitions|type |possible_keys
|key
|key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|-----------|-------------------|-------------------|-------|---|----|--------|---------------------------------------------|
1|SIMPLE |e |
|index_merge|PRIMARY,idx_emp_job|PRIMARY,idx_emp_job|4,4 | | 2|
100.0|Using union(PRIMARY,idx_emp_job); Using where|
其中,字段 key 顯示了使用的索引列表;Extra 中的 Using union(PRIMARY,idx_emp_job) 是索引合并的算法,這里采用了并集算法(查詢(xún)條件使用了 or 運(yùn)算符)。
unique_subquery 本質(zhì)上也是 eq_ref 索引查找,用于優(yōu)化以下形式的子查詢(xún):
value IN (SELECT primary_key FROM single_table WHERE some_expr)
index_subquery 本質(zhì)上也是 ref 范圍索引查找,用于優(yōu)化以下形式的子查詢(xún):
value IN (SELECT key_column FROM single_table WHERE some_expr)
index表示掃描整個(gè)索引,以下兩種情況會(huì)使用這種訪(fǎng)問(wèn)方式:
查詢(xún)可以直接通過(guò)索引返回所需的字段信息,也就是 index-only scan。此時(shí) Extra 字段顯示為 Using index。例如:
explain
select dept_id
from employee;
id|select_type|table |partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra |
--|-----------|--------|----------|-----|-------------|------------|-------|---|----|--------|-----------|
1|SIMPLE |employee| |index| |idx_emp_dept|4 | | 25| 100.0|Using index|
查詢(xún)所需的 dept_id 字段通過(guò)掃描索引 idx_emp_dept 即可獲得,所以采用了 index 訪(fǎng)問(wèn)類(lèi)型。
通過(guò)掃描索引執(zhí)行全表掃描,從而按照索引的順序返回?cái)?shù)據(jù)。此時(shí) Extra 字段不會(huì)出現(xiàn) Using index。
explain
select *
from employee force index (idx_emp_name)
order by emp_name;
id|select_type|table |partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra|
--|-----------|--------|----------|-----|-------------|------------|-------|---|----|--------|-----|
1|SIMPLE |employee| |index| |idx_emp_name|202 | | 25| 100.0| |
為了演示 index 訪(fǎng)問(wèn)方式,我們使用了強(qiáng)制索引(force index);否則,MySQL 選擇使用全表掃描(ALL)。
ALL表示全表掃描,這是一種 I/O 密集型的操作,通常意味著存在性能問(wèn)題。例如:
explain
select *
from employee;
id|select_type|table |partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
--|-----------|--------|----------|----|-------------|---|-------|---|----|--------|-----|
1|SIMPLE |employee| |ALL | | | | | 25| 100.0| |
因?yàn)?employee 表本身不大,而且我們查詢(xún)了所有的數(shù)據(jù),這種情況下全表掃描反而是一個(gè)很好的訪(fǎng)問(wèn)方法。但是,以下查詢(xún)顯然需要進(jìn)行優(yōu)化:
explain
select *
from employee
where salary = 10000;
id|select_type|table |partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra |
--|-----------|--------|----------|----|-------------|---|-------|---|----|--------|-----------|
1|SIMPLE |employee| |ALL | | | | | 25| 10.0|Using where|
顯然,針對(duì)這種查詢(xún)語(yǔ)句,我們可以通過(guò)為 salary 字段創(chuàng)建一個(gè)索引進(jìn)行優(yōu)化。
Extra 字段
執(zhí)行計(jì)劃輸出中的 Extra 字段通常會(huì)顯示更多的信息,可以幫助我們發(fā)現(xiàn)性能問(wèn)題的所在。上文中我們已經(jīng)介紹了一些 Extra 字段的信息,需要重點(diǎn)關(guān)注的輸出內(nèi)容包括:
Using where,表示將經(jīng)過(guò) WHERE 條件過(guò)濾后的數(shù)據(jù)傳遞給下個(gè)數(shù)據(jù)表或者返回客戶(hù)端。如果訪(fǎng)問(wèn)類(lèi)型為 ALL 或者 index,而 Extra 字段不是 Using where,意味著查詢(xún)語(yǔ)句可能存在問(wèn)題(除非就是想要獲取全部數(shù)據(jù))。
Using index condition,表示通過(guò)索引訪(fǎng)問(wèn)表之前,基于查詢(xún)條件中的索引字段進(jìn)行一次過(guò)濾,只返回必要的索引項(xiàng)。這也就是索引條件下推優(yōu)化。
Using index,表示直接通過(guò)索引即可返回所需的字段信息(index-only scan),不需要訪(fǎng)問(wèn)表。對(duì)于 InnoDB,如果通過(guò)主鍵獲取數(shù)據(jù),不會(huì)顯示 Using index,但是仍然是 index-only scan。此時(shí),訪(fǎng)問(wèn)類(lèi)型為 index,key 字段顯示為 PRIMARY。
Using filesort,意味著需要執(zhí)行額外的排序操作,通常需要占用大量的內(nèi)存或者磁盤(pán)。例如:
explain
select *
from employee
where dept_id =3
order by hire_date;
id|select_type|table |partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|--------|----------|----|-------------|------------|-------|-----|----|--------|--------------|
1|SIMPLE |employee| |ref |idx_emp_dept |idx_emp_dept|4 |const| 2| 100.0|Using filesort|
索引通??梢杂糜趦?yōu)化排序操作,我們可以為索引 idx_emp_dept 增加一個(gè) hire_date 字段來(lái)消除示例中的排序。
Using temporary,意味著需要?jiǎng)?chuàng)建臨時(shí)表保存中間結(jié)果。例如:
explain
select dept_id,job_id, sum(salary)
from employee
group by dept_id, job_id;
id|select_type|table |partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra |
--|-----------|--------|----------|----|-------------|---|-------|---|----|--------|---------------|
1|SIMPLE |employee| |ALL | | | | | 25| 100.0|Using temporary|
示例中的分組操作需要使用臨時(shí)表,同樣可以通過(guò)增加索引進(jìn)行優(yōu)化。
訪(fǎng)問(wèn)謂詞與過(guò)濾謂詞
在 SQL 中,WHERE 條件也被稱(chēng)為謂詞(predicate)。MySQL 數(shù)據(jù)庫(kù)中的謂詞存在以下三種使用方式:
訪(fǎng)問(wèn)謂詞(access predicate),在執(zhí)行計(jì)劃的輸出中對(duì)應(yīng)于 key_len 和 ref 字段。訪(fǎng)問(wèn)謂詞代表了索引葉子節(jié)點(diǎn)遍歷的開(kāi)始和結(jié)束條件。
索引過(guò)濾謂詞(index filter predicate),在執(zhí)行計(jì)劃中對(duì)應(yīng)于 Extra 字段的 Using index
condition。索引過(guò)濾謂詞在遍歷索引葉子節(jié)點(diǎn)時(shí)用于判斷是否返回該索引項(xiàng),但是不會(huì)用于判斷遍歷的開(kāi)始和結(jié)束條件,也就不會(huì)縮小索引掃描的范圍。
表級(jí)過(guò)濾謂詞(table level filter predicate),在執(zhí)行計(jì)劃中對(duì)應(yīng)于 Extra 字段的 Using where。謂詞中的非索引字段條件在表級(jí)別進(jìn)行判斷,意味著數(shù)據(jù)庫(kù)需要訪(fǎng)問(wèn)表中的數(shù)據(jù)然后再應(yīng)用該條件。
一般來(lái)說(shuō),對(duì)于相同的查詢(xún)語(yǔ)句,訪(fǎng)問(wèn)謂詞的性能好于索引過(guò)濾謂詞,索引過(guò)濾謂詞的性能好于表級(jí)過(guò)濾謂詞。
MySQL 執(zhí)行計(jì)劃中不會(huì)顯示每個(gè)條件對(duì)應(yīng)的謂詞類(lèi)型,而只是籠統(tǒng)地顯示使用了哪種謂詞類(lèi)型。我們創(chuàng)建一個(gè)示例表:
create table test (
id int not null auto_increment primary key,
col1 int,
col2 int,
col3 int);
insert into test(col1, col2, col3)
values (1,1,1), (2,4,6), (3,6,9);
create index test_idx on test (col1, col2);
analyze table test;
以下語(yǔ)句使用 col1 和 col2 作為查詢(xún)條件:
explain
select *
from test
where col1=1 and col2=1;
id|select_type|table|partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra|
--|-----------|-----|----------|----|-------------|--------|-------|-----------|----|--------|-----|
1|SIMPLE |test | |ref |test_idx |test_idx|10 |const,const| 1| 100.0| |
其中,Extra 字段為空;key = test_idx 表示使用索引進(jìn)行查找,key_len = 10 就是 col1 和 col2 兩個(gè)字段的長(zhǎng)度(可空字段長(zhǎng)度加 1);ref = const,const 表示使用了索引中的兩個(gè)字段和常量進(jìn)行比較,從而判斷是否返回?cái)?shù)據(jù)行。因此,該語(yǔ)句中的 WHERE 條件是一個(gè)訪(fǎng)問(wèn)謂詞。
接下來(lái)我們?nèi)匀皇褂?col1 和 col2 作為查詢(xún)條件,但是修改一下返回的字段:
explain
select id, col1, col2
from test
where col1=1 and col2=1;
id|select_type|table|partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|----|-------------|--------|-------|-----------|----|--------|-----------|
1|SIMPLE |test | |ref |test_idx |test_idx|10 |const,const| 1| 100.0|Using index|
其中,Extra 字段中的 Using index 不是 Using index condition,它是一個(gè) index-only scan,因?yàn)樗械牟樵?xún)結(jié)果都可以通過(guò)索引直接返回(包括 id);其他字段的信息和上面的示例相同。因此,該語(yǔ)句中的 WHERE 條件也是一個(gè)訪(fǎng)問(wèn)謂詞。
然后使用 col1 進(jìn)行范圍查詢(xún):
explain
select *
from test
where col1 between 1 and 2;
id|select_type|table|partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|-----|-------------|--------|-------|---|----|--------|---------------------|
1|SIMPLE |test | |range|test_idx |test_idx|5 | | 2| 100.0|Using index condition|
其中,Extra 字段中顯示為 Using index condition;key = test_idx 表示使用索引進(jìn)行范圍查找,key_len = 5 就是 col1 字段的長(zhǎng)度(可空字段長(zhǎng)度加 1);ref 為空表示沒(méi)有訪(fǎng)問(wèn)謂詞。因此,該語(yǔ)句中的 WHERE 條件是一個(gè)索引過(guò)濾謂詞,查詢(xún)需要遍歷整個(gè)索引并且通過(guò)索引判斷是否訪(fǎng)問(wèn)表中的數(shù)據(jù)。
最后使用 col1 和 col3 作為查詢(xún)條件:
explain
select *
from test
where col1=1 and col3=1;
id|select_type|table|partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|----|-------------|--------|-------|-----|----|--------|-----------|
1|SIMPLE |test | |ref |test_idx |test_idx|5 |const| 1| 33.33|Using where|
其中,Extra 字段中顯示為 Using where,表示訪(fǎng)問(wèn)表中的數(shù)據(jù)然后再應(yīng)用查詢(xún)條件 col3=1;key = test_idx
表示使用索引進(jìn)行查找,key_len = 5 就是 col1 字段的長(zhǎng)度(可空字段長(zhǎng)度加 1);ref = const
表示常量等值比較;filtered = 33.33 意味著經(jīng)過(guò)查詢(xún)條件比較之后只保留三分之一的數(shù)據(jù)。因此,該語(yǔ)句中的 WHERE
條件是一個(gè)表級(jí)過(guò)濾謂詞,意味著數(shù)據(jù)庫(kù)需要訪(fǎng)問(wèn)表中的數(shù)據(jù)然后再應(yīng)用該條件。
完整字段信息
下表列出了 MySQL 執(zhí)行計(jì)劃中各個(gè)字段的作用:
格式化參數(shù)
MySQL EXPLAIN 語(yǔ)句支持使用 FORMAT 選項(xiàng)指定不同的輸出格式:
{EXPLAIN | DESCRIBE | DESC}
FORMAT = {TRADITIONAL | JSON | TREE}
explainable_stmt
默認(rèn)的格式為 TRADITIONAL,以表格的形式顯示輸出信息;JSON 選項(xiàng)表示以 JSON 格式顯示信息;MySQL 8.0.16 之后支持 TREE 選項(xiàng),以樹(shù)形結(jié)構(gòu)輸出了比默認(rèn)格式更加詳細(xì)的信息,這也是唯一能夠顯示 hash join 的格式。
例如,以下語(yǔ)句輸出了 JSON 格式的執(zhí)行計(jì)劃:
explain
format=json
select *
from employee
where emp_id = 1;
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "1.00"
},
"table": {
"table_name": "employee",
"access_type": "const",
"possible_keys": [
"PRIMARY"
],
"key": "PRIMARY",
"used_key_parts": [
"emp_id"
],
"key_length": "4",
"ref": [
"const"
],
"rows_examined_per_scan": 1,
"rows_produced_per_join": 1,
"filtered": "100.00",
"cost_info": {
"read_cost": "0.00",
"eval_cost": "0.10",
"prefix_cost": "0.00",
"data_read_per_join": "568"
},
"used_columns": [
"emp_id",
"emp_name",
"sex",
"dept_id",
"manager",
"hire_date",
"job_id",
"salary",
"bonus",
"email"
]
}
}
}
其中,大部分的節(jié)點(diǎn)信息和表格形式的字段能夠?qū)?yīng);但是也返回了一些額外的信息,尤其是各種操作的成本信息 cost_info,可以幫助我們了解不同執(zhí)行計(jì)劃之間的成本差異。
以下語(yǔ)句返回了樹(shù)狀結(jié)構(gòu)的執(zhí)行計(jì)劃:
explain
format=tree
select *
from employee e1
join employee e2
on e1.salary = e2.salary;
-> Inner hash join (e2.salary = e1.salary) (cost=65.51 rows=63)
-> Table scan on e2 (cost=0.02 rows=25)
-> Hash
-> Table scan on e1 (cost=2.75 rows=25)
從結(jié)果可以看出,該執(zhí)行計(jì)劃使用了 Inner hash join 實(shí)現(xiàn)兩個(gè)表的連接查詢(xún)。
執(zhí)行計(jì)劃中的分區(qū)表信息
如果 SELECT 語(yǔ)句使用了分區(qū)表,可以通過(guò) EXPLAIN 命令查看涉及的具體分區(qū)。執(zhí)行計(jì)劃輸出的 partitions 字段顯示了數(shù)據(jù)行所在的表分區(qū)。首先創(chuàng)建一個(gè)分區(qū)表:
create table trb1 (id int primary key, name varchar(50), purchased date)
partition by range(id)
(
partition p0 values less than (3),
partition p1 values less than (7),
partition p2 values less than (9),
partition p3 values less than (11)
);
insert into trb1 values
(1, 'desk organiser', '2003-10-15'),
(2, 'CD player', '1993-11-05'),
(3, 'TV set', '1996-03-10'),
(4, 'bookcase', '1982-01-10'),
(5, 'exercise bike', '2004-05-09'),
(6, 'sofa', '1987-06-05'),
(7, 'popcorn maker', '2001-11-22'),
(8, 'aquarium', '1992-08-04'),
(9, 'study desk', '1984-09-16'),
(10, 'lava lamp', '1998-12-25');
然后查看使用 id 進(jìn)行范圍查詢(xún)時(shí)的執(zhí)行計(jì)劃:
explain
select * from trb1
where id < 5;
id|select_type|table|partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|-----|-------------|-------|-------|---|----|--------|-----------|
1|SIMPLE |trb1 |p0,p1 |range|PRIMARY |PRIMARY|4 | | 4| 100.0|Using where|
結(jié)果顯示查詢(xún)?cè)L問(wèn)了分區(qū) p0 和 p1。
獲取額外的執(zhí)行計(jì)劃信息
除了直接輸出的執(zhí)行計(jì)劃之外,EXPLAIN 命令還會(huì)產(chǎn)生一些額外信息,可以使用SHOW WARNINGS命令進(jìn)行查看。例如:
explain
select *
from department d
where exists (select 1 from employee e where e.dept_id = d.dept_id );
id|select_type|table|partitions|type|possible_keys|key
|key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|----|-------------|------------|-------|--------------|----|--------|--------------------------|
1|SIMPLE |d | |ALL |PRIMARY | |
| | 6| 100.0| |
1|SIMPLE |e | |ref |idx_emp_dept |idx_emp_dept|4
|hrdb.d.dept_id| 5| 100.0|Using index; FirstMatch(d)|
show warnings\G
*************************** 1. row ***************************
Level: Note
Code: 1276
Message: Field or reference 'hrdb.d.dept_id' of SELECT #2 was resolved in SELECT #1
*************************** 2. row ***************************
Level: Note
Code: 1003
Message: /* select#1 */ select `hrdb`.`d`.`dept_id` AS
`dept_id`,`hrdb`.`d`.`dept_name` AS `dept_name` from `hrdb`.`department`
`d` semi join (`hrdb`.`employee` `e`) where (`hrdb`.`e`.`dept_id` =
`hrdb`.`d`.`dept_id`)
2 rows in set (0.00 sec)
SHOW WARNINGS 命令輸出中的 Message 顯示了優(yōu)化器如何限定查詢(xún)語(yǔ)句中的表名和列名、應(yīng)用了重寫(xiě)和優(yōu)化規(guī)則后的查詢(xún)語(yǔ)句以及優(yōu)化過(guò)程的其他信息。
目前只有 SELECT 語(yǔ)句相關(guān)的額外信息可以通過(guò) SHOW WARNINGS 語(yǔ)句進(jìn)行查看,其他語(yǔ)句(DELETE、INSERT、REPLACE 和UPDATE)顯示的信息為空。
獲取指定連接的執(zhí)行計(jì)劃
EXPLAIN 語(yǔ)句也可以用于獲取指定連接中正在執(zhí)行的 SQL 語(yǔ)句的執(zhí)行計(jì)劃,語(yǔ)法如下:
EXPLAIN [FORMAT = {TRADITIONAL | JSON | TREE}] FOR CONNECTION connection_id;
其中,connection_id 是連接標(biāo)識(shí)符,可以通過(guò)字典表 INFORMATION_SCHEMA PROCESSLIST 或者 SHOW PROCESSLIST 命令獲取。如果某個(gè)會(huì)話(huà)中存在長(zhǎng)時(shí)間運(yùn)行的慢查詢(xún)語(yǔ)句,在另一個(gè)會(huì)話(huà)中執(zhí)行該命令可以獲得相關(guān)的診斷信息。
首先獲取當(dāng)前連接的會(huì)話(huà)標(biāo)識(shí)符:
mysql> SELECT CONNECTION_ID();
+-----------------+
| CONNECTION_ID() |
+-----------------+
| 30 |
+-----------------+
1 row in set (0.00 sec)
如果此時(shí)在當(dāng)前會(huì)話(huà)中獲取執(zhí)行計(jì)劃,將會(huì)返回錯(cuò)誤信息:
mysql> EXPLAIN FOR CONNECTION 30;
ERROR 3012 (HY000): EXPLAIN FOR CONNECTION command is supported only for SELECT/UPDATE/INSERT/DELETE/REPLACE
因?yàn)橹挥?SELECT、UPDATE、INSERT、DELETE、REPLACE 語(yǔ)句支持執(zhí)行計(jì)劃,當(dāng)前正在執(zhí)行的是 EXPLAIN 語(yǔ)句。
在當(dāng)前會(huì)話(huà)中執(zhí)行一個(gè)大表查詢(xún):
mysql> select * from large_table;
然后在另一個(gè)會(huì)話(huà)中執(zhí)行 EXPLAIN 命令:
explain for connection 30;
id|select_type|table |partitions|type|possible_keys|key|key_len|ref|rows |filtered|Extra|
--|-----------|-----------|----------|----|-------------|---|-------|---|------|--------|-----|
1|SIMPLE |large_table| |ALL | | | | |244296| 100.0| |
如果指定會(huì)話(huà)沒(méi)有正在運(yùn)行的語(yǔ)句,EXPLAIN 命令將會(huì)返回空結(jié)果。
獲取實(shí)際運(yùn)行的執(zhí)行計(jì)劃
MySQL 8.0.18 增加了一個(gè)新的命令:EXPLAIN ANALYZE。該語(yǔ)句用于運(yùn)行一個(gè)語(yǔ)句并且產(chǎn)生 EXPLAIN 結(jié)果,包括執(zhí)行時(shí)間和迭代器(iterator)信息,可以獲取優(yōu)化器的預(yù)期執(zhí)行計(jì)劃和實(shí)際執(zhí)行計(jì)劃之間的差異。
{EXPLAIN | DESCRIBE | DESC} ANALYZE select_statement
例如,以下 EXPLAIN 語(yǔ)句返回了查詢(xún)計(jì)劃和成本估算:
explain
format=tree
select *
from employee e
join department d
on (e.dept_id = d.dept_id )
where e.emp_id in(1, 2);
-> Nested loop inner join (cost=1.61 rows=2)
-> Filter: (e.emp_id in (1,2)) (cost=0.91 rows=2)
-> Index range scan on e using PRIMARY (cost=0.91 rows=2)
-> Single-row index lookup on d using PRIMARY (dept_id=e.dept_id) (cost=0.30 rows=1)
那么,實(shí)際上的執(zhí)行計(jì)劃和成本消耗情況呢?我們可以使用 EXPLAIN ANALYZE 語(yǔ)句查看:
explain analyze
select *
from employee e
join department d
on (e.dept_id = d.dept_id )
where e.emp_id in(1, 2);
-> Nested loop inner join (cost=1.61 rows=2) (actual time=0.238..0.258 rows=2 loops=1)
-> Filter: (e.emp_id in (1,2)) (cost=0.91 rows=2) (actual time=0.218..0.233 rows=2 loops=1)
-> Index range scan on e using PRIMARY (cost=0.91 rows=2) (actual time=0.214..0.228 rows=2 loops=1)
-> Single-row index lookup on d using PRIMARY
(dept_id=e.dept_id) (cost=0.30 rows=1) (actual time=0.009..0.009 rows=1
loops=2)
對(duì)于每個(gè)迭代器,EXPLAIN ANALYZE 輸出了以下信息:
估計(jì)執(zhí)行成本,某些迭代器不計(jì)入成本模型;
估計(jì)返回行數(shù);
返回第一行的實(shí)際時(shí)間(ms);
返回所有行的實(shí)際時(shí)間(ms),如果存在多次循環(huán),顯示平均時(shí)間;
實(shí)際返回行數(shù);
循環(huán)次數(shù)。
在輸出結(jié)果中的每個(gè)節(jié)點(diǎn)包含了下面所有節(jié)點(diǎn)的匯總信息,所以最終的估計(jì)信息和實(shí)際信息如下:
-> Nested loop inner join (cost=1.61 rows=2) (actual time=0.238..0.258 rows=2 loops=1)
查詢(xún)通過(guò)嵌套循環(huán)內(nèi)連接實(shí)現(xiàn);估計(jì)成本為 1.61,估計(jì)返回 2 行數(shù)據(jù);實(shí)際返回第一行數(shù)據(jù)的時(shí)間為 0.238 ms,實(shí)際返回所有數(shù)據(jù)的平均時(shí)間為 0.258 ms,實(shí)際返回了 2 行數(shù)據(jù),嵌套循環(huán)操作執(zhí)行了 1 次。
循環(huán)的實(shí)現(xiàn)過(guò)程是首先通過(guò)主鍵掃描 employee 表并且應(yīng)用過(guò)濾迭代器:
-> Filter: (e.emp_id in (1,2)) (cost=0.91 rows=2) (actual time=0.218..0.233 rows=2 loops=1)
-> Index range scan on e using PRIMARY (cost=0.91 rows=2) (actual time=0.214..0.228 rows=2 loops=1)
其中,應(yīng)用過(guò)濾迭代器返回第一行數(shù)據(jù)的時(shí)間為 0.218 ms,包括索引掃描的 0.214 ms;返回所有數(shù)據(jù)的平均時(shí)間為 0.233 ms,包括索引掃描的 0.228 ms;絕大部分時(shí)間都消耗在了索引掃描,總共返回了 2 條數(shù)據(jù)。
然后循環(huán)上一步返回的 2 條數(shù)據(jù),掃描 department 表的主鍵返回其他數(shù)據(jù):
-> Single-row index lookup on d using PRIMARY (dept_id=e.dept_id) (cost=0.30 rows=1) (actual time=0.009..0.009 rows=1 loops=2)
其中,loops=2 表示這個(gè)迭代器需要執(zhí)行 2 次;每次返回 1 行數(shù)據(jù),所以?xún)蓚€(gè)實(shí)際時(shí)間都是 0.009 ms。
以上示例的預(yù)期執(zhí)行計(jì)劃和實(shí)際執(zhí)行計(jì)劃基本上沒(méi)有什么差異。但有時(shí)候并不一定如此,例如:
explain analyze
select *
from employee e
join department d
on (e.dept_id = d.dept_id )
where e.salary = 10000;
-> Nested loop inner join (cost=3.63 rows=3) (actual time=0.427..0.444 rows=1 loops=1)
-> Filter: (e.salary = 10000.00) (cost=2.75 rows=3) (actual time=0.406..0.423 rows=1 loops=1)
-> Table scan on e (cost=2.75 rows=25) (actual time=0.235..0.287 rows=25 loops=1)
-> Single-row index lookup on d using PRIMARY
(dept_id=e.dept_id) (cost=0.29 rows=1) (actual time=0.018..0.018 rows=1
loops=1)
我們使用 salary 字段作為過(guò)濾條件,該字段沒(méi)有索引。執(zhí)行計(jì)劃中的最大問(wèn)題在于估計(jì)返回的行數(shù)是 3,而實(shí)際返回的行數(shù)是 1;這是由于缺少字段的直方圖統(tǒng)計(jì)信息。
我們對(duì) employee 表進(jìn)行分析,收集字段的直方圖統(tǒng)計(jì)之后再查看執(zhí)行計(jì)劃:
analyze table employee update histogram on salary;
explain analyze
select *
from employee e
join department d
on (e.dept_id = d.dept_id )
where e.salary = 10000;
-> Nested loop inner join (cost=3.10 rows=1) (actual time=0.092..0.105 rows=1 loops=1)
-> Filter: (e.salary = 10000.00) (cost=2.75 rows=1) (actual time=0.082..0.093 rows=1 loops=1)
-> Table scan on e (cost=2.75 rows=25) (actual time=0.056..0.080 rows=25 loops=1)
-> Single-row index lookup on d using PRIMARY
(dept_id=e.dept_id) (cost=0.35 rows=1) (actual time=0.009..0.009 rows=1
loops=1)
估計(jì)返回的行數(shù)變成了 1,和實(shí)際執(zhí)行結(jié)果相同。
??除了本文介紹的各種 EXPLAIN 語(yǔ)句之外,MySQL 還提供了優(yōu)化器跟蹤(optimizer trace)功能,可以獲取關(guān)于優(yōu)化器的更多信息,具體可以參考 MySQL Internals:Tracing the Optimizer。
寫(xiě)作不易,需要鼓勵(lì)!歡迎關(guān)注??、評(píng)論??、點(diǎn)贊??