你確定你讀懂了 MySQL 執(zhí)行計劃嗎?

作者: 不剪發(fā)的Tony老師
畢業(yè)于北京航空航天大學(xué),十多年數(shù)據(jù)庫管理與開發(fā)經(jīng)驗,目前在一家全球性的金融公司從事數(shù)據(jù)庫架構(gòu)設(shè)計。CSDN學(xué)院簽約講師以及GitChat專欄作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net


文章目錄

            獲取執(zhí)行計劃
            解讀執(zhí)行計劃
                type 字段
                Extra 字段
                訪問謂詞與過濾謂詞
                完整字段信息
            格式化參數(shù)
            執(zhí)行計劃中的分區(qū)表信息
            獲取額外的執(zhí)行計劃信息
            獲取指定連接的執(zhí)行計劃
            獲取實際運行的執(zhí)行計劃

大家好,我是只談技術(shù)不剪發(fā)的 Tony 老師。今天給大家深入分析一下 MySQL 中的執(zhí)行計劃。

執(zhí)行計劃(execution plan,也叫查詢計劃或者解釋計劃)是 MySQL 服務(wù)器執(zhí)行 SQL 語句的具體步驟。例如,通過索引還是全表掃描訪問表中的數(shù)據(jù),連接查詢的實現(xiàn)方式和連接的順序,分組和排序操作的實現(xiàn)方式等。

負(fù)責(zé)生成執(zhí)行計劃的組件就是優(yōu)化器,優(yōu)化器利用表結(jié)構(gòu)、字段、索引、查詢條件、數(shù)據(jù)庫的統(tǒng)計信息和配置參數(shù)決定 SQL 語句的最佳執(zhí)行方式。如果想要解決慢查詢的性能問題,首先應(yīng)該查看它的執(zhí)行計劃。
獲取執(zhí)行計劃

MySQL 提供了 EXPLAIN 語句,用于獲取 SQL 語句的執(zhí)行計劃。該語句的基本形式如下:

{EXPLAIN | DESCRIBE | DESC}
{
    SELECT statement
  | TABLE statement
  | DELETE statement
  | INSERT statement
  | REPLACE statement
  | UPDATE statement
}

 

EXPLAIN和DESCRIBE是同義詞,可以通用。實際應(yīng)用中,DESCRIBE主要用于查看表的結(jié)構(gòu),EXPLAIN主要用于獲取執(zhí)行計劃。MySQL 可以獲取 SELECT、INSERT、DELETE、UPDATE、REPLACE 等語句的執(zhí)行計劃。從 MySQL 8.0.19 開始,支持 TABLE 語句的執(zhí)行計劃。

舉例來說:

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í)行計劃包含了 12 列信息,這些字段的含義我們在下文中進(jìn)行解讀。

除了使用 EXPLAIN 語句之外,很多管理和開發(fā)工具都提供了查看圖形化執(zhí)行計劃的功能,例如 MySQL Workbench 中顯示以上查詢的執(zhí)行計劃如下:
 

當(dāng)然,這種方式最終也是執(zhí)行了 EXPLAIN 語句。
解讀執(zhí)行計劃

理解執(zhí)行計劃中每個字段的含義可以幫助我們知悉 MySQL 內(nèi)部的操作過程,找到性能問題的所在并有針對性地進(jìn)行優(yōu)化。在執(zhí)行計劃的輸出信息中,最重要的字段就是 type。
type 字段

type 被稱為連接類型(join type)或者訪問類型(access type),它顯示了 MySQL 如何訪問表中的數(shù)據(jù)。

訪問類型會直接影響到查詢語句的性能,性能從好到差依次為:

    system,表中只有一行數(shù)據(jù)(系統(tǒng)表),這是 const 類型的特殊情況;
    const,最多返回一條匹配的數(shù)據(jù),在查詢的最開始讀?。?br>     eq_ref,對于前面的每一行,從該表中讀取一行數(shù)據(jù);
    ref,對于前面的每一行,從該表中讀取匹配索引值的所有數(shù)據(jù)行;
    fulltext,通過 FULLTEXT 索引查找數(shù)據(jù);
    ref_or_null,與 ref 類似,額外加上 NULL 值查找;
    index_merge,使用索引合并優(yōu)化技術(shù),此時 key 列顯示使用的所有索引;
    unique_subquery,替代以下情況時的 eq_ref:value IN (SELECT primary_key FROM single_table WHERE some_expr);
    index_subquery,與 unique_subquery 類似,用于子查詢中的非唯一索引:value IN (SELECT key_column FROM single_table WHERE some_expr);
    range,使用索引查找范圍值;
    index,與 ALL 類型相同,只不過掃描的是索引;
    ALL,全表掃描,通常表示存在性能問題。

const 和 eq_ref 都意味著著通過 PRIMARY KEY 或者 UNIQUE 索引查找唯一值;它們的區(qū)別在于 const 對于整個查詢只返回一條數(shù)據(jù),eq_ref 對于前面的結(jié)果集中的每條記錄只返回一條數(shù)據(jù)。例如以下查詢通過主鍵(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問方式,所以相當(dāng)于一個常量(constant)。

以下語句通過主鍵等值連接兩個表:

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

 

對于 employee 中返回的每一行(table = e),department 表通過主鍵(key = PRIMARY)返回且僅返回一條數(shù)據(jù)(type = eq_ref)。Extra 字段中的 Using where 表示將經(jīng)過條件過濾后的數(shù)據(jù)傳遞給下個表或者客戶端。

ref、ref_or_null 以及 range 表示通過范圍查找所有匹配的索引項,然后根據(jù)需要再訪問表中的數(shù)據(jù)。通常意味著使用了非唯一索引或者唯一索引的前面部分字段進(jìn)行數(shù)據(jù)訪問,例如:

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

 

以上兩個查詢語句都是通過索引 idx_emp_dept 返回 employee 表中的數(shù)據(jù)。

ref_or_null 和 ref 的區(qū)別在于查詢中包含了 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,意味著通過索引訪問表中的數(shù)據(jù)之前,直接通過 WHERE 語句中出現(xiàn)的索引字段條件過濾數(shù)據(jù)。這是 MySQL 5.6 之后引入了一種優(yōu)化,叫做索引條件下推(Index Condition Pushdown)。

為了顯示 ref_or_null,我們需要將字段 dept_id 設(shè)置為可空,測試之后記得重新修改為 NOT NULL:

alter table employee modify column dept_id int not null;

 

range 通常出現(xiàn)在使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE 或者 IN() 運算符和索引字段進(jìn)行比較時,例如:

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)查詢通過多個索引 range 訪問方式返回數(shù)據(jù)時,MySQL 可以先對這些索引掃描結(jié)果合并成一個,然后通過這個索引獲取表中的數(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) 是索引合并的算法,這里采用了并集算法(查詢條件使用了 or 運算符)。

unique_subquery 本質(zhì)上也是 eq_ref 索引查找,用于優(yōu)化以下形式的子查詢:

value IN (SELECT primary_key FROM single_table WHERE some_expr)

 

index_subquery 本質(zhì)上也是 ref 范圍索引查找,用于優(yōu)化以下形式的子查詢:

value IN (SELECT key_column FROM single_table WHERE some_expr)

 

index表示掃描整個索引,以下兩種情況會使用這種訪問方式:

    查詢可以直接通過索引返回所需的字段信息,也就是 index-only scan。此時 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|
 

    查詢所需的 dept_id 字段通過掃描索引 idx_emp_dept 即可獲得,所以采用了 index 訪問類型。

    通過掃描索引執(zhí)行全表掃描,從而按照索引的順序返回數(shù)據(jù)。此時 Extra 字段不會出現(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 訪問方式,我們使用了強制索引(force index);否則,MySQL 選擇使用全表掃描(ALL)。

ALL表示全表掃描,這是一種 I/O 密集型的操作,通常意味著存在性能問題。例如:

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

 

因為 employee 表本身不大,而且我們查詢了所有的數(shù)據(jù),這種情況下全表掃描反而是一個很好的訪問方法。但是,以下查詢顯然需要進(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|

 

顯然,針對這種查詢語句,我們可以通過為 salary 字段創(chuàng)建一個索引進(jìn)行優(yōu)化。
Extra 字段

執(zhí)行計劃輸出中的 Extra 字段通常會顯示更多的信息,可以幫助我們發(fā)現(xiàn)性能問題的所在。上文中我們已經(jīng)介紹了一些 Extra 字段的信息,需要重點關(guān)注的輸出內(nèi)容包括:

    Using where,表示將經(jīng)過 WHERE 條件過濾后的數(shù)據(jù)傳遞給下個數(shù)據(jù)表或者返回客戶端。如果訪問類型為 ALL 或者 index,而 Extra 字段不是 Using where,意味著查詢語句可能存在問題(除非就是想要獲取全部數(shù)據(jù))。

    Using index condition,表示通過索引訪問表之前,基于查詢條件中的索引字段進(jìn)行一次過濾,只返回必要的索引項。這也就是索引條件下推優(yōu)化。

    Using index,表示直接通過索引即可返回所需的字段信息(index-only scan),不需要訪問表。對于 InnoDB,如果通過主鍵獲取數(shù)據(jù),不會顯示 Using index,但是仍然是 index-only scan。此時,訪問類型為 index,key 字段顯示為 PRIMARY。

    Using filesort,意味著需要執(zhí)行額外的排序操作,通常需要占用大量的內(nèi)存或者磁盤。例如:

    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 增加一個 hire_date 字段來消除示例中的排序。

    Using temporary,意味著需要創(chuàng)建臨時表保存中間結(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|
 

    示例中的分組操作需要使用臨時表,同樣可以通過增加索引進(jìn)行優(yōu)化。

訪問謂詞與過濾謂詞

在 SQL 中,WHERE 條件也被稱為謂詞(predicate)。MySQL 數(shù)據(jù)庫中的謂詞存在以下三種使用方式:

    訪問謂詞(access predicate),在執(zhí)行計劃的輸出中對應(yīng)于 key_len 和 ref 字段。訪問謂詞代表了索引葉子節(jié)點遍歷的開始和結(jié)束條件。
    索引過濾謂詞(index filter predicate),在執(zhí)行計劃中對應(yīng)于 Extra 字段的 Using index condition。索引過濾謂詞在遍歷索引葉子節(jié)點時用于判斷是否返回該索引項,但是不會用于判斷遍歷的開始和結(jié)束條件,也就不會縮小索引掃描的范圍。
    表級過濾謂詞(table level filter predicate),在執(zhí)行計劃中對應(yīng)于 Extra 字段的 Using where。謂詞中的非索引字段條件在表級別進(jìn)行判斷,意味著數(shù)據(jù)庫需要訪問表中的數(shù)據(jù)然后再應(yīng)用該條件。

一般來說,對于相同的查詢語句,訪問謂詞的性能好于索引過濾謂詞,索引過濾謂詞的性能好于表級過濾謂詞。

MySQL 執(zhí)行計劃中不會顯示每個條件對應(yīng)的謂詞類型,而只是籠統(tǒng)地顯示使用了哪種謂詞類型。我們創(chuàng)建一個示例表:

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;

 

以下語句使用 col1 和 col2 作為查詢條件:

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 兩個字段的長度(可空字段長度加 1);ref = const,const 表示使用了索引中的兩個字段和常量進(jìn)行比較,從而判斷是否返回數(shù)據(jù)行。因此,該語句中的 WHERE 條件是一個訪問謂詞。

接下來我們?nèi)匀皇褂?col1 和 col2 作為查詢條件,但是修改一下返回的字段:

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,它是一個 index-only scan,因為所有的查詢結(jié)果都可以通過索引直接返回(包括 id);其他字段的信息和上面的示例相同。因此,該語句中的 WHERE 條件也是一個訪問謂詞。

然后使用 col1 進(jì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 字段的長度(可空字段長度加 1);ref 為空表示沒有訪問謂詞。因此,該語句中的 WHERE 條件是一個索引過濾謂詞,查詢需要遍歷整個索引并且通過索引判斷是否訪問表中的數(shù)據(jù)。

最后使用 col1 和 col3 作為查詢條件:

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,表示訪問表中的數(shù)據(jù)然后再應(yīng)用查詢條件 col3=1;key = test_idx 表示使用索引進(jìn)行查找,key_len = 5 就是 col1 字段的長度(可空字段長度加 1);ref = const 表示常量等值比較;filtered = 33.33 意味著經(jīng)過查詢條件比較之后只保留三分之一的數(shù)據(jù)。因此,該語句中的 WHERE 條件是一個表級過濾謂詞,意味著數(shù)據(jù)庫需要訪問表中的數(shù)據(jù)然后再應(yīng)用該條件。
完整字段信息

下表列出了 MySQL 執(zhí)行計劃中各個字段的作用:
 

 

格式化參數(shù)

MySQL EXPLAIN 語句支持使用 FORMAT 選項指定不同的輸出格式:

{EXPLAIN | DESCRIBE | DESC}
FORMAT = {TRADITIONAL | JSON | TREE}
explainable_stmt

 

默認(rèn)的格式為 TRADITIONAL,以表格的形式顯示輸出信息;JSON 選項表示以 JSON 格式顯示信息;MySQL 8.0.16 之后支持 TREE 選項,以樹形結(jié)構(gòu)輸出了比默認(rèn)格式更加詳細(xì)的信息,這也是唯一能夠顯示 hash join 的格式。

例如,以下語句輸出了 JSON 格式的執(zhí)行計劃:

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é)點信息和表格形式的字段能夠?qū)?yīng);但是也返回了一些額外的信息,尤其是各種操作的成本信息 cost_info,可以幫助我們了解不同執(zhí)行計劃之間的成本差異。

以下語句返回了樹狀結(jié)構(gòu)的執(zhí)行計劃:

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í)行計劃使用了 Inner hash join 實現(xiàn)兩個表的連接查詢。
執(zhí)行計劃中的分區(qū)表信息

如果 SELECT 語句使用了分區(qū)表,可以通過 EXPLAIN 命令查看涉及的具體分區(qū)。執(zhí)行計劃輸出的 partitions 字段顯示了數(shù)據(jù)行所在的表分區(qū)。首先創(chuàng)建一個分區(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)行范圍查詢時的執(zhí)行計劃:

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é)果顯示查詢訪問了分區(qū) p0 和 p1。
獲取額外的執(zhí)行計劃信息

除了直接輸出的執(zhí)行計劃之外,EXPLAIN 命令還會產(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)化器如何限定查詢語句中的表名和列名、應(yīng)用了重寫和優(yōu)化規(guī)則后的查詢語句以及優(yōu)化過程的其他信息。

目前只有 SELECT 語句相關(guān)的額外信息可以通過 SHOW WARNINGS 語句進(jìn)行查看,其他語句(DELETE、INSERT、REPLACE 和UPDATE)顯示的信息為空。
獲取指定連接的執(zhí)行計劃

EXPLAIN 語句也可以用于獲取指定連接中正在執(zhí)行的 SQL 語句的執(zhí)行計劃,語法如下:

EXPLAIN [FORMAT = {TRADITIONAL | JSON | TREE}] FOR CONNECTION connection_id;

 

其中,connection_id 是連接標(biāo)識符,可以通過字典表 INFORMATION_SCHEMA PROCESSLIST 或者 SHOW PROCESSLIST 命令獲取。如果某個會話中存在長時間運行的慢查詢語句,在另一個會話中執(zhí)行該命令可以獲得相關(guān)的診斷信息。

首先獲取當(dāng)前連接的會話標(biāo)識符:

mysql> SELECT CONNECTION_ID();
+-----------------+
| CONNECTION_ID() |
+-----------------+
|              30 |
+-----------------+
1 row in set (0.00 sec)

 

如果此時在當(dāng)前會話中獲取執(zhí)行計劃,將會返回錯誤信息:

mysql> EXPLAIN FOR CONNECTION 30;
ERROR 3012 (HY000): EXPLAIN FOR CONNECTION command is supported only for SELECT/UPDATE/INSERT/DELETE/REPLACE

 

因為只有 SELECT、UPDATE、INSERT、DELETE、REPLACE 語句支持執(zhí)行計劃,當(dāng)前正在執(zhí)行的是 EXPLAIN 語句。

在當(dāng)前會話中執(zhí)行一個大表查詢:

mysql> select * from large_table;

 

然后在另一個會話中執(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|     |

 

如果指定會話沒有正在運行的語句,EXPLAIN 命令將會返回空結(jié)果。
獲取實際運行的執(zhí)行計劃

MySQL 8.0.18 增加了一個新的命令:EXPLAIN ANALYZE。該語句用于運行一個語句并且產(chǎn)生 EXPLAIN 結(jié)果,包括執(zhí)行時間和迭代器(iterator)信息,可以獲取優(yōu)化器的預(yù)期執(zhí)行計劃和實際執(zhí)行計劃之間的差異。

{EXPLAIN | DESCRIBE | DESC} ANALYZE select_statement

 

例如,以下 EXPLAIN 語句返回了查詢計劃和成本估算:

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)

 

那么,實際上的執(zhí)行計劃和成本消耗情況呢?我們可以使用 EXPLAIN ANALYZE 語句查看:

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)

 

對于每個迭代器,EXPLAIN ANALYZE 輸出了以下信息:

    估計執(zhí)行成本,某些迭代器不計入成本模型;
    估計返回行數(shù);
    返回第一行的實際時間(ms);
    返回所有行的實際時間(ms),如果存在多次循環(huán),顯示平均時間;
    實際返回行數(shù);
    循環(huán)次數(shù)。

在輸出結(jié)果中的每個節(jié)點包含了下面所有節(jié)點的匯總信息,所以最終的估計信息和實際信息如下:

-> Nested loop inner join  (cost=1.61 rows=2) (actual time=0.238..0.258 rows=2 loops=1)

 

查詢通過嵌套循環(huán)內(nèi)連接實現(xiàn);估計成本為 1.61,估計返回 2 行數(shù)據(jù);實際返回第一行數(shù)據(jù)的時間為 0.238 ms,實際返回所有數(shù)據(jù)的平均時間為 0.258 ms,實際返回了 2 行數(shù)據(jù),嵌套循環(huán)操作執(zhí)行了 1 次。

循環(huán)的實現(xiàn)過程是首先通過主鍵掃描 employee 表并且應(yīng)用過濾迭代器:

    -> 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)用過濾迭代器返回第一行數(shù)據(jù)的時間為 0.218 ms,包括索引掃描的 0.214 ms;返回所有數(shù)據(jù)的平均時間為 0.233 ms,包括索引掃描的 0.228 ms;絕大部分時間都消耗在了索引掃描,總共返回了 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 表示這個迭代器需要執(zhí)行 2 次;每次返回 1 行數(shù)據(jù),所以兩個實際時間都是 0.009 ms。

以上示例的預(yù)期執(zhí)行計劃和實際執(zhí)行計劃基本上沒有什么差異。但有時候并不一定如此,例如:

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 字段作為過濾條件,該字段沒有索引。執(zhí)行計劃中的最大問題在于估計返回的行數(shù)是 3,而實際返回的行數(shù)是 1;這是由于缺少字段的直方圖統(tǒng)計信息。

我們對 employee 表進(jìn)行分析,收集字段的直方圖統(tǒng)計之后再查看執(zhí)行計劃:

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)

 

估計返回的行數(shù)變成了 1,和實際執(zhí)行結(jié)果相同。

    ??除了本文介紹的各種 EXPLAIN 語句之外,MySQL 還提供了優(yōu)化器跟蹤(optimizer trace)功能,可以獲取關(guān)于優(yōu)化器的更多信息,具體可以參考 MySQL Internals:Tracing the Optimizer。

寫作不易,需要鼓勵!歡迎關(guān)注??、評論??、點贊??