1. 程式人生 > >MySQL執行計劃總結

MySQL執行計劃總結

1. 背景

在工作過程中,有時候會對慢查詢進行調優。對於MySQL的SQL語句調優,MySQL本身提供了強大的explain關鍵字用於查詢分析執行計劃。
本文對explain執行計劃進行分析與整理,文中的內容在未特別註明情況下,以MySQL5.7版本為例

2. 簡介

語法:從語法角度explain和describe/desc是相同的,只是一般更常用desc看錶結構,explain來看查詢計劃。

一個標準的explain出來的結果包含這些欄位

  • id 表示SELECT的識別符號。一般來說值越大代表執行優先順序越高,如果相同,則上面的結果比下面的結果優先順序高。

  • select_type 查詢型別

  • table 表示表名,未必是真實存在的表,可能是衍生出來的表。

  • type 表示連線型別

  • possible_keys 表示MySQL可能用於查詢行的索引,如果為NULL,通常需要考慮優化查詢語句/表索引

  • key 與possible_keys不同,key輸出的是查詢中實際會使用到的索引。key中標註的索引沒有出現在上面的possible_keys也是有可能的,通常是因為有輔助索引的欄位覆蓋了查詢欄位,這樣的話MySQL會使用索引覆蓋,效率會更高。

  • key_len 表示使用索引的位元組長度,如果上述key輸出的是NULL, key_len也會輸出NULL。可以根據key_len的值來推算多重索引實際使用了幾個字首索引列。注意,對於可以為NULL的列,儲存長度會大1。

  • ref 表示與索引一起進行查詢的列/常數。

  • rows 表示MySQL在查詢時必須檢查的行數,但是對於InnoDB表,這個值是預估的,未必精確。

  • filtered 表示在執行查詢時根據條件篩選行數佔比,這也是一個估計值。

  • extra 表示執行計劃的一些擴充套件資訊。

3. explain欄位解析

下面是以例項來逐一介紹explain中重要欄位常見輸出型別的含義與來源。
注:下文中的示例在未特別註明情況下是以MySQL5.7為例,並且優化器的開關選項為

index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on

3.1 select_type

select_type是用來表示select語句的型別
它可能的值如下表所示

注意:下面出現的一些SQL是為了本文編寫的示例,而不是在實際生產中常見的SQL。

SIMPLE
表示簡單查詢,也即不包括連線查詢/子查詢。

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select a from t\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

PRIMARY
表示最外層的查詢。

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select a from t where a = (select max(a) from t)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: SUBQUERY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
2 rows in set, 1 warning (0.00 sec)

UNION
用於表示union查詢中第二條及之後的查詢

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select * from t union all select * from t union all select * from t\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 2
  select_type: UNION
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
*************************** 3. row ***************************
           id: 3
  select_type: UNION
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
3 rows in set, 1 warning (0.00 sec)

DEPENDENT UNION
與UNION不同,這種型別表示依賴於外層查詢。

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select * from t where a in (select * from t union all select * from t)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 3. row ***************************
           id: 3
  select_type: DEPENDENT UNION
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
3 rows in set, 1 warning (0.01 sec)

這裡有必要解釋一下為什麼可以通過上面的語句構造出DEPENDENT UNION。這看上去並不依賴外部的UNION子查詢怎麼會成了DEPENDENT UNION呢?因為MySQL會把這裡的in轉寫為exist,可以通過檢視警告資訊來看看MySQL優化器對語句進行轉寫後的樣子。

mysql> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `test`.`t`.`a` AS `a` from `test`.`t` where <in_optimizer>(`test`.`t`.`a`,<exists>(/* select#2 */ select 1 from `test`.`t` where (<cache>(`test`.`t`.`a`) = `test`.`t`.`a`) union all /* select#3 */ select 1 from `test`.`t` where (<cache>(`test`.`t`.`a`) = `test`.`t`.`a`)))
1 row in set (0.00 sec)

可以看到原來的in被轉寫為exist,內層的union語句需要外層掃描到的a欄位。

UNION RESULT
表示UNION的結果

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select * from t union select * from s\G
*************************** 1. row ***************************


           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 2
  select_type: UNION
        table: s
   partitions: NULL
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 3. row ***************************
           id: NULL
  select_type: UNION RESULT
        table: <union1,2>
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: NULL
        Extra: Using temporary
3 rows in set, 1 warning (0.00 sec)

SUBQUERY
子查詢中的第一個select語句,並且不依賴於外部查詢。

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select a from t where a = (select a from t)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: SUBQUERY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
2 rows in set, 1 warning (0.00 sec)

DEPENDENT SUBQUERY
依賴子查詢,與SUBQUERY的區別就在於這種型別是依賴於外層查詢。因此我們可以很容易地構造出DEPENDENT SUBQUERY。

mysql> create table t(a int);
Query OK, 0 rows affected (0.21 sec)

mysql> create table s(b int);
Query OK, 0 rows affected (0.23 sec)
mysql> explain select a from t where a = (select * from s where b = a)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: s
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
2 rows in set, 2 warnings (0.00 sec)

DERIVED
表示一個派生表(可以簡單理解為非物理表。派生表意味著在查詢過程中會在記憶體或磁碟上建立臨時表,臨時表是不具有索引的,因此臨時表在與其他表關聯時效能會比較差。在MySQL較低版本(5.5以下)中,比較容易構造出一個派生表查詢,如下所示。但是在MySQL5.6之後版本,根據optimizer_switch中引數derived_merge是否為開啟,from子句中的子查詢可以與外部查詢綜合起來優化。因此下面的簡單示例在MySQL5.7是無效的(除非關閉derived_merge優化選項)。

mysql> create table t(a int);
Query OK, 0 rows affected (0.21 sec)
mysql> explain select * from (select * from t) q\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: system
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 0
        Extra: const row not found
*************************** 2. row ***************************
           id: 2
  select_type: DERIVED
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
        Extra:
2 rows in set (0.04 sec)

MATERIALIZED
這是MySQL5.6開始引入的一種新的select_type,主要是優化from/in子句中的子查詢。關於這個東西的中文說法常見有“物化“或者“具體化”兩種翻譯。
關於MATERIALIZATION,在官方doc上有更詳細的說明

mysql> create table t(a int);
Query OK, 0 rows affected (0.22 sec)
mysql> create table s(b int);
Query OK, 0 rows affected (0.22 sec)
mysql> insert into t select null;
Query OK, 1 row affected (0.05 sec)
Records: 1  Duplicates: 0  Warnings: 0
mysql> insert into t select null;
Query OK, 1 row affected (0.05 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> explain select * from t where a in (select * from s)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: <subquery2>
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2
     filtered: 50.00
        Extra: Using where; Using join buffer (Block Nested Loop)
*************************** 3. row ***************************
           id: 2
  select_type: MATERIALIZED
        table: s
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
3 rows in set, 1 warning (0.00 sec)

UNCACHEABLE SUBQUERY
顧名思義,便是無法快取的SUBQUERY。通常來說,SUBQUERY是隻會被執行一次的,後續相同的SUBQUERY會使用第一次執行後的快取結果。但是在某些情況下,SUBQUERY會出現無法快取,而需要每次重複執行的情況。通常是由於子查詢中帶有一些使用者變數/隨機函式(UUID/RAND)等。就拿前文示例中的SUBQUERY稍作修改

mysql> create table t(a int);
Query OK, 0 rows affected (0.23 sec)
set @tmp=1;
Query OK, 0 rows affected (0.00 sec)
mysql> explain select a from t where a = (select a from t where a = @tmp)\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: UNCACHEABLE SUBQUERY
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
2 rows in set, 1 warning (0.00 sec)

UNCACHEABLE UNION
與上文的UNCACHEABLE SUBQUERY類似,不作冗述。

至此,查詢計劃的select_type介紹完畢

3.2 table

table列內容比較簡單。一般展示的情況大致有以下幾種

  • NULL 比如select一些與資料庫表無關的內容,如select now()
  • <unionM,N> 表示由UNION操作產生的臨時表,其中M和N表示產生臨時表的源表
  • <derivedM> 表示是由id為M的表派生而來的臨時表
  • <subqueryM> 表示是由id為M的子查詢物化而來的臨時表

我們根據上文介紹的id, select_type, table列已經足以大致分析複雜查詢中的執行順序。

下面要介紹的是另一個非常重要的欄位type

3.3 type

在MySQL官方doc的type小節,type是被描述為join type連線型別的。正如《高效能MySQL》中6.4也提及了,MySQL賦予了join一詞比較豐富的含義,而不僅僅是我們通常腦海中浮現的SQL Join。每一次查詢都是一個join,所以對於所謂的“join type”連線型別,我們不妨理解為獲取資料的方式。

下面先大致看一下type列可能會有哪些不同的取值。

  • system
  • const
  • eq_ref
  • ref
  • fulltext
  • ref_or_null
  • index_merge
  • unique_subquery
  • index_subquery
  • range
  • index
  • all

一共是12種方式,除了全表掃描不使用索引外,其餘11種都使用了索引。其實這麼說也是不嚴謹的,對於InnoDB儲存引擎,它是索引組織表的,所有的記錄都存放在聚集索引中,即使掃表,也可以說是使用了索引。下面逐一進行介紹,並附以示例。

system
這是一種比較特殊的連線型別,官方文件上似乎沒特別註明,但實際上,這種型別只出現在MyISAM/Memory儲存引擎,InnoDB並不存在這種連線型別。

mysql> create table t (a int primary key) engine myisam;
Query OK, 0 rows affected (0.06 sec)

mysql> insert into t select 1;
Query OK, 1 row affected (0.02 sec)
Records: 1  Duplicates: 0  Warnings: 0

ysql> explain select * from t\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: system
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

mysql> alter table t engine innodb;
Query OK, 1 row affected (0.34 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> explain select * from t\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.01 sec)

const
const表示在資料表中最多隻能找到一條匹配行,它會再查詢剛開始的時候被讀取,之後會被優化器當作常量。const通常會出現在用常量來比較主鍵或者唯一索引的所有列的時候。

mysql> create table t (a int, b int, c varchar(15), primary key(a, b));
Query OK, 0 rows affected (0.22 sec)

mysql> insert into t select 1,2,'hello world';
Query OK, 1 row affected (0.04 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> explain select * from t where a = 1 and b =2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

mysql> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select '1' AS `a`,'2' AS `b`,'hello world' AS `c` from `test`.`t` where 1
1 row in set (0.00 sec)

此時,如果我們檢視一下warning資訊,可以發現優化器居然直接將查詢給展開了。

eq_ref
eq_ref是一種在多表連線查詢中可能會出現的連線方式。它會出現在連表查詢中檢索第二個或更後面的表時使用的條件為這個表的主鍵或者唯一非空索引的情況。

mysql> create table t (a int, b int);
Query OK, 0 rows affected (0.22 sec)

mysql> create table s (c int not null, d int not null, unique index(c,d));
Query OK, 0 rows affected (0.23 sec)

mysql> explain select * from t,s where t.a=s.c and t.b=s.d\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: s
   partitions: NULL
         type: eq_ref
possible_keys: c
          key: c
      key_len: 8
          ref: test.t.a,test.t.b
         rows: 1
     filtered: 100.00
        Extra: Using index
2 rows in set, 1 warning (0.00 sec)

通過此查詢計劃的輸出,MySQL內部此查詢的執行方式為以t表為驅動表,使用 t.a=s.c and t.b=s.d來檢索s表,s表的連線方式為eq_ref。因為滿足唯一索引的所有列都有被使用,且此唯一索引所有列都非null。

ref
可以說ref是一種退化的eq_ref。ref的出現表徵著檢索條件不能確定至多一條記錄。

mysql> create table t (a int, key(a));
Query OK, 0 rows affected (0.24 sec)

mysql> explain select * from t where a = 3\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ref
possible_keys: a
          key: a
      key_len: 5
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

以上介紹的四種連線方式(system/const/eq_ref/ref)都屬於效能比較高的連線方式,它們的where子句必須是等值比較運算子(=或者<=>)。

fulltext
在MySQL5.5及以下版本只有MyISAM儲存引擎支援全文索引。這個連線型別說明的事情很簡單,查詢使用了全文索引,僅此而已。

ref_or_null
ref_or_null連線型別和上文提及的ref差不多,只是顧名思義,有個or null的選項。

mysql> create table t (a int, key(a));
Query OK, 0 rows affected (0.25 sec)

mysql> explain select * from t where a = 3 or a is null\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ref_or_null
possible_keys: a
          key: a
      key_len: 5
          ref: const
         rows: 2
     filtered: 100.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.01 sec)

實際情況,幾乎遇不到這種型別,因為通常大部分公司的MySQL規範會強制要求表中的欄位為not null。

index_merge
關於索引合併,《高效能MySQL》的6.5.3節只是簡單地一筆帶過。
所謂索引合併就是對於查詢條件比較複雜的情況,MySQL會將條件拆分,使用不同的索引,再將結果進行交、並、去重等操作。

mysql> create table t(a int,b int,key(a),key(b));
Query OK, 0 rows affected (0.03 sec)

mysql> insert into t select 1,2;
Query OK, 1 row affected (0.05 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 1 row affected (0.03 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 2 rows affected (0.03 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 4 rows affected (0.03 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 8 rows affected (0.03 sec)
Records: 8  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 16 rows affected (0.04 sec)
Records: 16  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 32 rows affected (0.03 sec)
Records: 32  Duplicates: 0  Warnings: 0

mysql> insert into t select * from t;
Query OK, 64 rows affected (0.05 sec)
Records: 64  Duplicates: 0  Warnings: 0

mysql> explain select * from t  where a<1 or b>3\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
         type: index_merge
possible_keys: a,b
          key: a,b
      key_len: 5,5
          ref: NULL
         rows: 2
        Extra: Using sort_union(a,b); Using where
1 row in set (0.03 sec)

在上例中,在建表後,需要人為地再插入一些資料以避免在小資料量的情況下,MySQL直接掃表方式來完成查詢。在實際應用中,即便優化器選項中開啟index_merge,也未必會使用到,如果確信索引合併效率比較高的話,可以用index hint來指引MySQL使用index_merge。
索引合併的不足在於:

  1. 它需要讀取多個索引,這就增加了磁碟的I/O,效率不如讀取單個索引。
  2. 對於複雜的AND/OR,很多時候沒辦法正確優化,或者即便可以用索引合併,MySQL在執行的時候也未必會用
  3. 索引合併需要對部分結果進行交、並、去重,這本身也是有一定開銷的。

unique_subquery
與eq_ref不同的地方在於,當至多隻會有一條結果的select出現在where中的in條件時,執行計劃的type會顯示為unique_subquery。在高版本MySQL(5.6或以上)由於對in子查詢有了很多優化,比較難看見這種型別。瞭解即可。

index_subquery
與unique_subquery不同點在於:unique_subquery保證了in列表中的值不會是重複的。而index_subquery有點類似於ref,它會利用索引對in子句中的查詢結果去重。

range
這是一種很常見的連線方式,對於where條件中出現索引列 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN與常數進行比較的運算,都有可能會出現range連線方式。在這種情況下ref列會輸出NULL。

mysql> create table t ( a int,b int, key(a));
Query OK, 0 rows affected (0.26 sec)

mysql> explain select * from t where a >1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: range
possible_keys: a
          key: a
      key_len: 5
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

index
index很容易給人造成誤解,認為是一種很高效的查詢。實際上index可以認為是用索引樹來掃表而已。它和掃表要讀的記錄數是一樣的,只是因為(二級)索引樹只含有索引列,比資料檔案(對於InnoDB來說是聚集索引)要小很多,所以會比all效率要高一些。
通常在如下兩種情況下可能會出現index連線方式

  • 某個索引的列覆蓋了查詢條件,即可以使用索引覆蓋避免訪問聚集索引(InnoDB),這時extra列會顯示"using index"。
  • 可以利用索引的順序來遍歷記錄,這種情況下extra列不會顯示"using index"。
mysql> create table t (a int, b int, key(a));
Query OK, 0 rows affected (0.26 sec)

mysql> explain select a from t order by a desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: index
possible_keys: NULL
          key: a
      key_len: 5
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.01 sec)

mysql> explain select * from t order by a desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using filesort
1 row in set, 1 warning (0.00 sec)

這個例子展示了,當我們需要按a順序查詢t表中的a欄位時,MySQL會掃描索引。但是當我們的查詢為*時,這時由於即便訪問索引也需要再次去資料檔案(InnoDB來說就是聚集索引)中讀取相應的行,MySQL為了減少這樣的I/O索性直接用聚集索引掃表,在記憶體中排好序輸出。

all
這就是讓人望而生畏的所謂的“掃表”了。掃表出現的原因通常是由於既有索引不能適應查詢SQL,也包括如上面index中提及的為了減少I/O,MySQL可能讀聚集索引掃表再進行排序。對於OLTP應用,掃表還是儘量要避免的,儘可能保證type是range及以上為好。這裡就不列出掃表的示例了。

3.4 possible_keys

這只是列出MySQL在選擇執行計劃時的索引候選。對於SQL調優,此列資訊作用不是太大。

3.5 key

key列展示的是在執行查詢時實際會使用的索引,這個列輸出的資訊非常重要。它的輸出可能是NULL/PRIMARY/或者自定索引名。在非索引合併的情況下,這個列輸出的資訊只會至多包含一個索引,對於索引合併,則會是索引合併用到的索引。

3.6 key_len

key_len展示了key中對應項在實際使用時用到了幾個位元組。這同樣是一個很重要的資訊。例如對於使用多重索引的情況,可以根據key_len來推斷查詢SQL使用多重索引幾個字首索引列。值得注意的是MySQL對於可為NULL的欄位需要額外的一個位元組儲存,因此key_len在這種情況下會比列型別本身的值多1。

3.7 ref

ref展示的用於與key中顯示的索引進行比較的常數/列。
如果是常數的話ref列會顯示const。對於這個列比較需要關注的是出現func的情況,這表示與索引進行比較的項是經過某種函式運算的。

3.8 rows

如前文已經提及,rows表示MySQL在查詢時必須檢查的行數,但是對於InnoDB表,這個值是預估的,未必精確。

3.9 extra

extra是一個非常具有參考價值的列。官方doc也有專門的小節來講解extra列中的值。
經驗有限,在實際工作中也不是所有的都遇到過,下面記錄一些遇到過的extra資訊,之後隨著工作遇到新的繼續補充。

Using filesort
這是在SQL調優實戰中很容易發現的獵物,出現這種情況,通常是由於查詢SQL的order by沒有合適的索引可以用。雖然名字是叫filesort然而實際上未必是在檔案中排序,可能是記憶體中也可能是在磁碟中(取決於排序緩衝區的大小)。對於出現Using filesort通常需要考慮優化掉不必要的order by或者新增索引。但是在實際情況中,也可能即使order by的列有對應索引,仍然會出現filesort。舉個例子來說對於select * from t where ... order by col。如果where條件中的篩選性比較低,MySQL是有可能為了避免讀取二級索引和聚集索引造成的I/O開銷,而傾向於只使用聚集索引順序訪問過濾where條件中的記錄,然後再進行filesort的。在實際工作中,曾經發現公司專案組的分頁框架就有類似的問題,對於分頁框架,往往需要查出原SQL可以在資料庫表中能篩選出來的總記錄數,比較簡單的方式就是在原SQL外面套上一個select count(0) from,將原來的SQL包為子查詢。這樣的話由於原SQL往往帶著order by語句,很可能會出現為了求一個total count而出現掃表+排序的情況。解決方式可以是在分頁攔截器中修改原SQL,將原來的語句最外層改寫為select count(0)形式。這樣MySQL容易優化掉不必要的order by。

Using temporary
與filesort一樣,這也是非常值得關注的資訊。這代表查詢中MySQL建立了臨時表,僅僅靠explain,通常無法斷定臨時表是在記憶體中建立的還是在磁碟中建立的。官方文件上提及常見的情況是group by與order by用了不同的索引。但是實際上並不是說extra裡沒顯示Using temporary就代表執行過程中沒有建立臨時表。
以下情況都是通常會建立臨時表的情況。

  • from語句帶了子查詢,MySQL把這個叫做派生derived,實際上也是臨時表
  • count(distinct col)並且無法使用索引時,會建立臨時表。
  • union/union all會用臨時表來合併結果。
  • 無法使用索引的排序

Using where
在《高效能MySQL》的6.2.2有提及對於使用者編寫的帶有where語句的SQL,MySQL有三種方式處理,從好到壞依次是。在儲存引擎層用索引來過濾where條件中不匹配的記錄;在MySQL伺服器層用索引覆蓋(extra列會出現Using index)返回記錄;從資料表中返回資料後,過濾where中不匹配的條件(extra會出現Using where)這也是MySQL伺服器層完成的。

Using join buffer (Block Nested Loop), Using join buffer (Batched Key Access)
關於連線的這兩種方式,可以參考官方文件中的描述以及演算法原型
對於多表連線查詢,如果被驅動表(下一個待join的表)在連線條件上沒有高效的索引(連線型別為all/index/range)的話,通常會使用BNL演算法來進行表之間的join。Batched Key Access是MySQL5.6開始出現的一種連線演算法。對於出現連線緩衝的extra資訊,可以檢查下MySQL選擇的連線順序,以及被連線表上的索引情況。一般來說優化器都足以選擇最優的連線順序,如果需要人為指定的話,儘量遵循以下幾點

  • 臨時表和普通表join 這種情況用臨時表作驅動表
  • 臨時表和臨時表join 用小表作驅動表
  • 普通表和普通表join 看索引和表大小,都有索引或者都沒索引,小表作驅動表。其餘情況的話儘量保證被驅動表上連線欄位有索引。

Impossible WHERE noticed after reading const tables
回顧一下上面提及過的system/const連線方式,對於查詢中的where條件如果是可以唯一確定至多一條記錄的話,MySQL是可以在查詢一開始就讀取記錄並將結果看作常量。因此如下的情況,MySQL是可以非常迅速推斷出結果不存在的。

mysql> create table t (a int primary key, b int);
Query OK, 0 rows affected (0.28 sec)

mysql> insert into t select 1,2;
Query OK, 1 row affected (0.05 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into t select 2,3;
Query OK, 1 row affected (0.04 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into t select 3,4;
Query OK, 1 row affected (0.05 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> explain select * from t where a=1 and b=3\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: NULL
   partitions: NULL
         type: NULL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: NULL
        Extra: Impossible WHERE noticed after reading const tables
1 row in set, 1 warning (0.00 sec)

mysql> show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select '1' AS `a`,'2' AS `b` from `test`.`t` where 0
1 row in set (0.00 sec)

Const row not found
對於SQL調優實戰,比較少遇到。出現這種情況,排查一下篩選條件值是否錯了,或者表資料是否少了即可。

No tables used
對於select 1 from dual或者其他不帶表的查詢,extra資訊中會顯示此列。