你確定你讀懂了 MySQL 執行計劃嗎?

EXPLAIN

大家好,我是隻談技術不剪髮的 Tony 老師。今天給大家深入分析一下 MySQL 中的執行計劃。

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

負責生成執行計劃的組件就是優化器,優化器利用表結構、字段、索引、查詢條件、數據庫的統計信息和配置參數決定 SQL 語句的最佳執行方式。如果想要解決慢查詢的性能問題,首先應該查看它的執行計劃。

獲取執行計劃

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

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

EXPLAINDESCRIBE是同義詞,可以通用。實際應用中,DESCRIBE主要用於查看錶的結構,EXPLAIN主要用於獲取執行計劃。MySQL 可以獲取 SELECT、INSERT、DELETE、UPDATE、REPLACE 等語句的執行計劃。從 MySQL 8.0.19 開始,支持 TABLE 語句的執行計劃。

舉例來說:

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

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

EXPLAIN
當然,這種方式最終也是執行了 EXPLAIN 語句。

解讀執行計劃

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

type 字段

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

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

  • system,表中只有一行數據(系統表),這是 const 類型的特殊情況;
  • const,最多返回一條匹配的數據,在查詢的最開始讀取;
  • eq_ref,對於前面的每一行,從該表中讀取一行數據;
  • ref,對於前面的每一行,從該表中讀取匹配索引值的所有數據行;
  • fulltext,通過 FULLTEXT 索引查找數據;
  • ref_or_null,與 ref 類似,額外加上 NULL 值查找;
  • index_merge,使用索引合併優化技術,此時 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,全表掃描,通常表示存在性能問題。

consteq_ref 都意味着着通過 PRIMARY KEY 或者 UNIQUE 索引查找唯一值;它們的區別在於 const 對於整個查詢只返回一條數據,eq_ref 對於前面的結果集中的每條記錄只返回一條數據。例如以下查詢通過主鍵(key = PRIMARY)進行等值查找:

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 只返回一條數據,是一種非常快速的訪問方式,所以相當於一個常量(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)返回且僅返回一條數據(type = eq_ref)。Extra 字段中的 Using where 表示將經過條件過濾後的數據傳遞給下個表或者客戶端。

refref_or_null 以及 range 表示通過範圍查找所有匹配的索引項,然後根據需要再訪問表中的數據。通常意味着使用了非唯一索引或者唯一索引的前面部分字段進行數據訪問,例如:

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 表中的數據。

ref_or_null 和 ref 的區別在於查詢中包含了 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,意味着通過索引訪問表中的數據之前,直接通過 WHERE 語句中出現的索引字段條件過濾數據。這是 MySQL 5.6 之後引入了一種優化,叫做索引條件下推(Index Condition Pushdown)。

爲了顯示 ref_or_null,我們需要將字段 dept_id 設置爲可空,測試之後記得重新修改爲 NOT NULL:

alter table employee modify column dept_id int not null;

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

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 表示索引合併,當查詢通過多個索引 range 訪問方式返回數據時,MySQL 可以先對這些索引掃描結果合併成一個,然後通過這個索引獲取表中的數據。例如:

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 本質上也是 eq_ref 索引查找,用於優化以下形式的子查詢:

value IN (SELECT primary_key FROM single_table WHERE some_expr)

index_subquery 本質上也是 ref 範圍索引查找,用於優化以下形式的子查詢:

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 訪問類型。

  • 通過掃描索引執行全表掃描,從而按照索引的順序返回數據。此時 Extra 字段不會出現 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 表本身不大,而且我們查詢了所有的數據,這種情況下全表掃描反而是一個很好的訪問方法。但是,以下查詢顯然需要進行優化:

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 字段創建一個索引進行優化。

Extra 字段

執行計劃輸出中的 Extra 字段通常會顯示更多的信息,可以幫助我們發現性能問題的所在。上文中我們已經介紹了一些 Extra 字段的信息,需要重點關注的輸出內容包括:

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

  • Using index condition,表示通過索引訪問表之前,基於查詢條件中的索引字段進行一次過濾,只返回必要的索引項。這也就是索引條件下推優化。

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

  • Using filesort,意味着需要執行額外的排序操作,通常需要佔用大量的內存或者磁盤。例如:

    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|
    

    索引通常可以用於優化排序操作,我們可以爲索引 idx_emp_dept 增加一個 hire_date 字段來消除示例中的排序。

  • Using temporary,意味着需要創建臨時表保存中間結果。例如:

    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|
    

    示例中的分組操作需要使用臨時表,同樣可以通過增加索引進行優化。

訪問謂詞與過濾謂詞

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

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

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

MySQL 執行計劃中不會顯示每個條件對應的謂詞類型,而只是籠統地顯示使用了哪種謂詞類型。我們創建一個示例表:

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

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

然後使用 col1 進行範圍查詢:

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 表示使用索引進行範圍查找,key_len = 5 就是 col1 字段的長度(可空字段長度加 1);ref 爲空表示沒有訪問謂詞。因此,該語句中的 WHERE 條件是一個索引過濾謂詞,查詢需要遍歷整個索引並且通過索引判斷是否訪問表中的數據。

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

完整字段信息

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

列名 作用
id 語句中 SELECT 的序號。如果是 UNION 操作的結果,顯示爲 NULL;此時 table 列顯示爲 <unionM,N>。
select_type SELECT 的類型,包括:
- SIMPLE,不涉及 UNION 或者子查詢的簡單查詢;
- PRIMARY,最外層 SELECT;
- UNION,UNION 中第二個或之後的 SELECT;
- DEPENDENT UNION,UNION 中第二個或之後的 SELECT,該 SELECT 依賴於外部查詢;
- UNION RESULT,UNION 操作的結果;
- SUBQUERY,子查詢中的第一個 SELECT;
- DEPENDENT SUBQUERY,子查詢中的第一個 SELECT,該 SELECT 依賴於外部查詢;
- DERIVED,派生表,即 FROM 中的子查詢;
- DEPENDENT DERIVED,依賴於其他表的派生表;
- MATERIALIZED,物化子查詢;
- UNCACHEABLE SUBQUERY,無法緩存結果的子查詢,對於外部表中的每一行都需要重新查詢;
- UNION 中第二個或之後的 SELECT,該 UNION屬於 UNCACHEABLE SUBQUERY。
table 數據行的來源表,也有可能是以下值之一:
- <unionM,N>,id 爲 M 和 N 的 SELECT 並集運算的結果;
- <derivedN>,id 爲 N 的派生表的結果;
- <subqueryN>,id 爲 N 的物化子查詢的結果。
partitions 對於分區表而言,表示數據行所在的分區;普通表顯示爲 NULL。
type 連接類型或者訪問類型,性能從好到差依次爲:
- system,表中只有一行數據,這是 const 類型的特殊情況;
- const,最多返回一條匹配的數據,在查詢的最開始讀取;
- eq_ref,對於前面的每一行,從該表中讀取一行數據;
- ref,對於前面的每一行,從該表中讀取匹配索引值的所有數據行;
- fulltext,通過 FULLTEXT 索引查找數據;
- ref_or_null,與 ref 類似,額外加上 NULL 值查找;
- index_merge,使用索引合併優化技術,此時 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,全表掃描,通常表示存在性能問題。
possible_keys 可能用到的索引,實際上不一定使用。
key 實際使用的索引。
key_len 實際使用的索引的長度。
ref 用於和 key 中的索引進行比較的字段或者常量,從而判斷是否返回數據行。
rows 執行查詢需要檢查的行數,對於 InnoDB 是一個估計值。
filtered 根據查詢條件過濾之後行數百分比,rows × filtered 表示進入下一步處理的行數。
Extra 包含了額外的信息。例如 Using temporary 表示使用了臨時表,Using filesort 表示需要額外的排序操作等。

格式化參數

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

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

默認的格式爲 TRADITIONAL,以表格的形式顯示輸出信息;JSON 選項表示以 JSON 格式顯示信息;MySQL 8.0.16 之後支持 TREE 選項,以樹形結構輸出了比默認格式更加詳細的信息,這也是唯一能夠顯示 hash join 的格式。

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

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"
      ]
    }
  }
}

其中,大部分的節點信息和表格形式的字段能夠對應;但是也返回了一些額外的信息,尤其是各種操作的成本信息 cost_info,可以幫助我們瞭解不同執行計劃之間的成本差異。

以下語句返回了樹狀結構的執行計劃:

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)

從結果可以看出,該執行計劃使用了 Inner hash join 實現兩個表的連接查詢。

執行計劃中的分區表信息

如果 SELECT 語句使用了分區表,可以通過 EXPLAIN 命令查看涉及的具體分區。執行計劃輸出的 partitions 字段顯示了數據行所在的表分區。首先創建一個分區表:

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 進行範圍查詢時的執行計劃:

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|

結果顯示查詢訪問了分區 p0 和 p1。

獲取額外的執行計劃信息

除了直接輸出的執行計劃之外,EXPLAIN 命令還會產生一些額外信息,可以使用SHOW WARNINGS命令進行查看。例如:

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 顯示了優化器如何限定查詢語句中的表名和列名、應用了重寫和優化規則後的查詢語句以及優化過程的其他信息。

目前只有 SELECT 語句相關的額外信息可以通過 SHOW WARNINGS 語句進行查看,其他語句(DELETE、INSERT、REPLACE 和UPDATE)顯示的信息爲空。

獲取指定連接的執行計劃

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

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

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

首先獲取當前連接的會話標識符:

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

如果此時在當前會話中獲取執行計劃,將會返回錯誤信息:

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 語句支持執行計劃,當前正在執行的是 EXPLAIN 語句。

在當前會話中執行一個大表查詢:

mysql> select * from large_table;

然後在另一個會話中執行 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 命令將會返回空結果。

獲取實際運行的執行計劃

MySQL 8.0.18 增加了一個新的命令:EXPLAIN ANALYZE。該語句用於運行一個語句並且產生 EXPLAIN 結果,包括執行時間和迭代器(iterator)信息,可以獲取優化器的預期執行計劃和實際執行計劃之間的差異。

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

那麼,實際上的執行計劃和成本消耗情況呢?我們可以使用 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 輸出了以下信息:

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

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

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

查詢通過嵌套循環內連接實現;估計成本爲 1.61,估計返回 2 行數據;實際返回第一行數據的時間爲 0.238 ms,實際返回所有數據的平均時間爲 0.258 ms,實際返回了 2 行數據,嵌套循環操作執行了 1 次。

循環的實現過程是首先通過主鍵掃描 employee 表並且應用過濾迭代器:

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

其中,應用過濾迭代器返回第一行數據的時間爲 0.218 ms,包括索引掃描的 0.214 ms;返回所有數據的平均時間爲 0.233 ms,包括索引掃描的 0.228 ms;絕大部分時間都消耗在了索引掃描,總共返回了 2 條數據。

然後循環上一步返回的 2 條數據,掃描 department 表的主鍵返回其他數據:

    -> 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 表示這個迭代器需要執行 2 次;每次返回 1 行數據,所以兩個實際時間都是 0.009 ms。

以上示例的預期執行計劃和實際執行計劃基本上沒有什麼差異。但有時候並不一定如此,例如:

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

我們對 employee 表進行分析,收集字段的直方圖統計之後再查看執行計劃:

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)

估計返回的行數變成了 1,和實際執行結果相同。

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

寫作不易,需要鼓勵!歡迎關注❤️、評論📝、點贊👍

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