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。
索引合並的不足在於:
- 它需要讀取多個索引,這就增加了磁盤的I/O,效率不如讀取單個索引。
- 對於復雜的AND/OR,很多時候沒辦法正確優化,或者即便可以用索引合並,MySQL在執行的時候也未必會用
- 索引合並需要對部分結果進行交、並、去重,這本身也是有一定開銷的。
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信息中會顯示此列。