1. 程式人生 > 其它 >Mysql的Explain使用及索引總結

Mysql的Explain使用及索引總結

Explain工具介紹

使用EXPLAIN關鍵字可以模擬優化器執行SQL語句,分析你的查詢語句或是結構的效能瓶頸
在 select 語句之前增加 explain 關鍵字,MySQL 會在查詢上設定一個標記,執行查詢會返
回執行計劃的資訊,而不是執行這條SQL
注意:如果 from 中包含子查詢,仍會執行該子查詢,將結果放入臨時表中。

actor建表語句

DROP TABLE IF EXISTS `actor`;
CREATE TABLE `actor` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of actor
-- ----------------------------
INSERT INTO `actor` VALUES ('1', 'a', '2020-01-11 19:57:26');
INSERT INTO `actor` VALUES ('2', 'b', '2020-01-11 19:57:38');
INSERT INTO `actor` VALUES ('3', 'c', '2020-01-11 19:57:57');

film建表語句

DROP TABLE IF EXISTS `film`;
CREATE TABLE `film` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of film
-- ----------------------------
INSERT INTO `film` VALUES ('1', 'film0');
INSERT INTO `film` VALUES ('2', 'film1');
INSERT INTO `film` VALUES ('3', 'film2');

film_actor建表語句

DROP TABLE IF EXISTS `file_actor`;
CREATE TABLE `file_actor` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `film_id` int(11) NOT NULL,
  `actor_id` int(11) NOT NULL,
  `remark` varchar(25) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of file_actor
-- ----------------------------
INSERT INTO `file_actor` VALUES ('1', '1', '1', null);
INSERT INTO `file_actor` VALUES ('2', '1', '2', null);
INSERT INTO `file_actor` VALUES ('3', '2', '1', null);

Explain展示的欄位

explain列

展示explain中的每個列的資訊。

1、id列

id列的編號是select的序號,有幾個select就有幾個id,並且id的順序是按select出現的順序增長的。

id值越大優先順序越高,id相同則從上往下執行,id為NULL最後執行。

2、select_type

select_type表示對應行是簡單還是複雜的查詢

  1. simple:簡單查詢。查詢不包含子查詢和union

如上圖

  1. primary:複雜查詢中最外層的select

3)subquery:包含在 select 中的子查詢(不在 from 子句中)
4)derived:包含在 from 子句中的子查詢。MySQL會將結果存放在一個臨時表中,也稱為
派生表(derived的英文含義)
用這個例子來了解 primary、subquery 和 derived 型別
mysql> set session optimizer_switch='derived_merge=off'; #關閉mysql5.7新特性對衍
生表的合併優化
mysql> explain select (select 1 from actor where id = 1) from (select * from film
where id = 1) der;

mysql> set session optimizer_switch='derived_merge=on'; #還原預設配置

5)union:在 union 中的第二個和隨後的 select

mysql> explain select 1 union all select 1;

3、table列

這一列表示 explain 的一行正在訪問哪個表。
當 from 子句中有子查詢時,table列是 格式,表示當前查詢依賴 id=N 的查
詢,於是先執行 id=N 的查詢。
當有 union 時,UNION RESULT 的 table 列的值為<union1,2>,1和2表示參與 union 的
select 行id。

4、type列

這一列表示關聯型別或訪問型別,即MySQL決定如何查詢表中的行,查詢資料行記錄的大概
範圍。
依次從最優到最差分別為:system > const > eq_ref > ref > range > index > ALL
一般來說,得保證查詢達到range級別,最好達到ref
NULL:mysql能夠在優化階段分解查詢語句,在執行階段用不著再訪問表或索引。例如:在
索引列中選取最小值,可以單獨查詢索引來完成,不需要在執行時訪問表
mysql> explain select min(id) from film;


const, system:mysql能對查詢的某部分進行優化並將其轉化成一個常量(可以看show
warnings 的結果)。用於 primary key 或 unique key 的所有列與常數比較時,所以表最多
有一個匹配行,讀取1次,速度比較快。system是const的特例,表裡只有一條元組匹配時為
system
mysql> explain extended select * from (select * from film where id = 1) tmp;


mysql> show warnings;


eq_ref:primary key 或 unique key 索引的所有部分被連線使用 ,最多隻會返回一條符合
條件的記錄。這可能是在 const 之外最好的聯接型別了,簡單的 select 查詢不會出現這種
type。
mysql> explain select * from film_actor left join film on film_actor.film_id = film.id;


ref:相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分字首,索引要
和某個值相比較,可能會找到多個符合條件的行。

  1. 簡單 select 查詢,name是普通索引(非唯一索引)
    mysql> explain select * from film where name = 'film1';

  2. 關聯表查詢,idx_film_actor_id是film_id和actor_id的聯合索引,這裡使用到了film_actor
    的左邊字首film_id部分。
    mysql> explain select film_id from film left join film_actor on film.id =
    film_actor.film_id;

    range:範圍掃描通常出現在 in(), between ,> ,<, >= 等操作中。使用一個索引來檢索給定
    範圍的行。
    mysql> explain select * from actor where id > 1;

    index:掃描全表索引,這通常比ALL快一些。
    mysql> explain select * from film;

    ALL:即全表掃描,意味著mysql需要從頭到尾去查詢所需要的行。通常情況下這需要增加索
    引來進行優化了
    mysql> explain select * from actor;

5、possible_keys列

這一列顯示查詢可能使用哪些索引來查詢。
explain 時可能出現 possible_keys 有列,而 key 顯示 NULL 的情況,這種情況是因為表中
資料不多,mysql認為索引對此查詢幫助不大,選擇了全表查詢。
如果該列是NULL,則沒有相關的索引。在這種情況下,可以通過檢查 where 子句看是否可
以創造一個適當的索引來提高查詢效能,然後用 explain 檢視效果。

6、key列

這一列顯示mysql實際採用哪個索引來優化對該表的訪問。
如果沒有使用索引,則該列是 NULL。如果想強制mysql使用或忽視possible_keys列中的索
引,在查詢中使用 force index、ignore index。

7、key_len列

這一列顯示了mysql在索引裡使用的位元組數,通過這個值可以算出具體使用了索引中的哪些
列。
舉例來說,film_actor的聯合索引 idx_film_actor_id 由 film_id 和 actor_id 兩個int列組成,
並且每個int是4位元組。通過結果中的key_len=4可推斷出查詢使用了第一個列:film_id列來執
行索引查詢。
mysql> explain select * from film_actor where film_id = 2;

key_len計算規則如下:

  • 字串

    char(n):n位元組長度

    varchar(n):2位元組儲存字串長度,如果是utf-8,則長度3n+2

  • 數值型別

    tinyint:1位元組
    smallint:2位元組
    int:4位元組
    bigint:8位元組

  • 時間型別

    date:3位元組
    timestamp:4位元組
    datetime:8位元組

  • 如果欄位允許為 NULL,需要1位元組記錄是否為 NULL

索引最大長度是768位元組,當字串過長時,mysql會做一個類似左字首索引的處理,將前半
部分的字元提取出來做索引。

8、ref列

這一列顯示了在key列記錄的索引中,表查詢值所用到的列或常量,常見的有:const(常
量),欄位名(例:film.id)

9、rows列

這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集裡的行數。

10、Extra列

這一列展示的是額外資訊。常見的重要值如下:
1)Using index:使用覆蓋索引
mysql> explain select film_id from film_actor where film_id = 1;

2)Using where:使用 where 語句來處理結果,查詢的列未被索引覆蓋

mysql> explain select * from actor where name = 'a';

3)Using index condition:查詢的列不完全被索引覆蓋,where條件中是一個前導列的範
圍;

mysql> explain select * from film_actor where film_id > 1;

4)Using temporary:mysql需要建立一張臨時表來處理查詢。出現這種情況一般是要進行
優化的,首先是想到用索引來優化。

1、actor.name沒有索引,此時建立了張臨時表來distinct
mysql> explain select distinct name from actor;

2、film.name建立了idx_name索引,此時查詢時extra是using index,沒有用臨時表
mysql> explain select distinct name from film;

5)Using filesort:將用外部排序而不是索引排序,資料較小時從記憶體排序,否則需要在磁碟
完成排序。這種情況下一般也是要考慮使用索引來優化的。

1、actor.name未建立索引,會瀏覽actor整個表,儲存排序關鍵字name和對應的id,然後排
序name並檢索行記錄
mysql> explain select * from actor order by name;

2、film.name建立了idx_name索引,此時查詢時extra是using index
mysql> explain select * from film order by name;

6)Select tables optimized away:使用某些聚合函式(比如 max、min)來訪問存在索引
的某個欄位是
mysql> explain select min(id) from film;

Using filesort檔案排序原理詳解

filesort檔案排序方式

單路排序:是一次性取出滿足條件行的所有欄位,然後在sort buffer中進行排序;用trace工具可
以看到sort_mode資訊裡顯示< sort_key, additional_fields >或者< sort_key,
packed_additional_fields >

雙路排序(又叫回表排序模式):是首先根據相應的條件取出相應的排序欄位和可以直接定位行
資料的行 ID,然後在 sort buffer 中進行排序,排序完後需要再次取回其它需要的欄位;用trace工具
可以看到sort_mode資訊裡顯示< sort_key, rowid >

MySQL 通過比較系統變數 max_length_for_sort_data(預設1024位元組) 的大小和需要查詢的欄位總大小來
判斷使用哪種排序模式。
如果 max_length_for_sort_data 比查詢欄位的總長度大,那麼使用 單路排序模式;
如果 max_length_for_sort_data 比查詢欄位的總長度小,那麼使用 雙路排序模式。

我們先看單路排序的詳細過程:

  1. 從索引name找到第一個滿足 name = ‘xx’ 條件的主鍵 id

  2. 根據主鍵 id 取出整行,取出所有欄位的值,存入 sort_buffer 中

  3. 從索引name找到下一個滿足 name = ‘xx’ 條件的主鍵 id

  4. 重複步驟 2、3 直到不滿足 name = ‘xx’

  5. 對 sort_buffer 中的資料按照欄位 position 進行排序

  6. 返回結果給客戶端

我們再看下雙路排序的詳細過程:

  1. 從索引 name 找到第一個滿足 name = ‘xx’ 的主鍵id
  2. 根據主鍵 id 取出整行,把排序欄位 position 和主鍵 id 這兩個欄位放到 sort buffer 中
  3. 從索引 name 取下一個滿足 name = ‘xx’ 記錄的主鍵 id
  4. 重複 3、4 直到不滿足 name = ‘xx’
  5. 對 sort_buffer 中的欄位 position 和主鍵 id 按照欄位 position 進行排序
  6. 遍歷排序好的 id 和欄位 position,按照 id 的值回到原表中取出 所有欄位的值返回給客戶端

其實對比兩個排序模式,單路排序會把所有需要查詢的欄位都放到 sort buffer 中,而雙路排序只會把主鍵
和需要排序的欄位放到 sort buffer 中進行排序,然後再通過主鍵回到原表查詢需要的欄位。
如果 MySQL 排序記憶體配置的比較小並且沒有條件繼續增加了,可以適當把 max_length_for_sort_data 配
置小點,讓優化器選擇使用雙路排序演算法,可以在sort_buffer 中一次排序更多的行,只是需要再根據主鍵
回到原表取資料。
如果 MySQL 排序記憶體有條件可以配置比較大,可以適當增大 max_length_for_sort_data 的值,讓優化器
優先選擇全欄位排序(單路排序),把需要的欄位放到 sort_buffer 中,這樣排序後就會直接從記憶體裡返回查
詢結果了。
所以,MySQL通過 max_length_for_sort_data 這個引數來控制排序,在不同場景使用不同的排序模式,
從而提升排序效率。
注意,如果全部使用sort_buffer記憶體排序一般情況下效率會高於磁碟檔案排序,但不能因為這個就隨便增
大sort_buffer(預設1M),mysql很多引數設定都是做過優化的,不要輕易調整。

Join關聯查詢優化

mysql的表關聯常見有兩種演算法

  • Nested-Loop Join 演算法
  • Block Nested-Loop Join 演算法

巢狀迴圈連線 Nested-Loop Join(NLJ) 演算法

一次一行迴圈地從第一張表(稱為驅動表)中讀取行,在這行資料中取到關聯欄位,根據關聯欄位在另一張表(被驅動
表)裡取出滿足條件的行,然後取出兩張表的結果合集。
mysql> EXPLAIN select*from t1 inner join t2 on t1.a= t2.a;

從執行計劃中可以看到這些資訊:
驅動表是 t2,被驅動表是 t1。先執行的就是驅動表(執行計劃結果的id如果一樣則按從上到下順序執行sql);優
化器一般會優先選擇小表做驅動表。所以使用 inner join 時,排在前面的表並不一定就是驅動表。
使用了 NLJ演算法。一般 join 語句中,如果執行計劃 Extra 中未出現 Using join buffer 則表示使用的 join 算
法是 NLJ。
上面sql的大致流程如下:

  1. 從表 t2 中讀取一行資料;
  2. 從第 1 步的資料中,取出關聯欄位 a,到表 t1 中查詢;
  3. 取出表 t1 中滿足條件的行,跟 t2 中獲取到的結果合併,作為結果返回給客戶端;
  4. 重複上面 3 步。

整個過程會讀取 t2 表的所有資料(掃描100行),然後遍歷這每行資料中欄位 a 的值,根據 t2 表中 a 的值索引掃描 t1 表
中的對應行(掃描100次 t1 表的索引,1次掃描可以認為最終只掃描 t1 表一行完整資料,也就是總共 t1 表也掃描了100
行)。因此整個過程掃描了 200 行。
如果被驅動表的關聯欄位沒索引,使用NLJ演算法效能會比較低(下面有詳細解釋),mysql會選擇Block Nested-Loop Join
演算法。

基於塊的巢狀迴圈連線 Block Nested-Loop Join( BNL )演算法

把驅動表的資料讀入到 join_buffer 中,然後掃描被驅動表,把被驅動表每一行取出來跟 join_buffer 中的資料做對比。
mysql>EXPLAIN select*from t1 inner join t2 on t1.b= t2.b;

Extra 中 的Using join buffer (Block Nested Loop)說明該關聯查詢使用的是 BNL 演算法。
上面sql的大致流程如下:

  1. 把 t2 的所有資料放入到 join_buffer 中
  2. 把表 t1 中每一行取出來,跟 join_buffer 中的資料做對比
  3. 返回滿足 join 條件的資料

整個過程對錶 t1 和 t2 都做了一次全表掃描,因此掃描的總行數為10000(表 t1 的資料總量) + 100(表 t2 的資料總量) =10100。並且 join_buffer 裡的資料是無序的,因此對錶 t1 中的每一行,都要做 100 次判斷,所以記憶體中的判斷次數是100 * 10000= 100 萬次。

被驅動表的關聯欄位沒索引為什麼要選擇使用 BNL 演算法而不使用 Nested-Loop Join 呢?

如果上面第二條sql使用 Nested-Loop Join,那麼掃描行數為 100 * 10000 = 100萬次,這個是磁碟掃描。
很顯然,用BNL磁碟掃描次數少很多,相比於磁碟掃描,BNL的記憶體計算會快得多。
因此MySQL對於被驅動表的關聯欄位沒索引的關聯查詢,一般都會使用 BNL 演算法。如果有索引一般選擇 NLJ 演算法,有
索引的情況下 NLJ 演算法比 BNL演算法效能更高

對於關聯sql的優化

關聯欄位加索引,讓mysql做join操作時儘量選擇NLJ演算法
小標驅動大表,寫多表連線sql時如果明確知道哪張表是小表可以用straight_join寫法固定連線驅動方式,省去
mysql優化器自己判斷的時間
straight_join解釋:straight_join功能同join類似,但能讓左邊的表來驅動右邊的表,能改表優化器對於聯表查詢的執
行順序。
比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表制定mysql選著 t2 表作為驅動表。
straight_join只適用於inner join,並不適用於left join,right join。(因為left join,right join已經代表指
定了表的執行順序)
儘可能讓優化器去判斷,因為大部分情況下mysql優化器是比人要聰明的。使用straight_join一定要慎重,因
為部分情況下人為指定的執行順序並不一定會比優化引擎要靠譜。
in和exsits優化
原則:小表驅動大表,即小的資料集驅動大的資料集
in:當B表的資料集小於A表的資料集時,in優於exists

select * from A where id in (select id from B)
 #等價於:
   for(select id from B){
  select * from A where A.id = B.id
  }

exis`ts:當A表的資料集小於B表的資料集時,exists優於in
  將主查詢A的資料,放到子查詢B中做條件驗證,根據驗證結果(true或false)來決定主查詢的資料是否保留

 select * from A where exists (select 1 from B where B.id = A.id)
 #等價於:
for(select * from A) {
  select * from B where B.id = A.id
}

A表與B表的ID欄位應建立索引

1、EXISTS (subquery)只返回TRUE或FALSE,因此子查詢中的SELECT * 也可以用SELECT 1替換,官方說法是實際執行時會
忽略SELECT清單,因此沒有區別
2、EXISTS子查詢的實際執行過程可能經過了優化而不是我們理解上的逐條對比
3、EXISTS子查詢往往也可以用JOIN來代替,何種最優需要具體問題具體分析

索引最佳實踐

1.全值匹配

2.最左字首法則(指的是查詢從索引的最左前列開始並且不跳過索引
中的列)

3.不在索引列上做任何操作(計算、函式、(自動or手動)型別轉換),會導致索引失效而轉
向全表掃描

4.儲存引擎不能使用索引中範圍條件右邊的列

5.儘量使用覆蓋索引(只訪問索引的查詢(索引列包含查詢列)),減少select *語句

6.mysql在使用不等於(!=或者<>)的時候無法使用索引會導致全表掃描

7.is null,is not null 也無法使用索引

8.like以萬用字元開頭('$abc...')mysql索引失效會變成全表掃描操作

9.字串不加單引號索引失效

10.少用or或in,用它查詢時,mysql不一定使用索引,mysql內部優化器會根據檢索比例、
表大小等多個因素整體評估是否使用索引,詳見範圍查詢優化

11.範圍查詢優化(縮小範圍)

12.查詢個數推薦使用count(*)

like KK%相當於=常量,%KK和%KK% 相當於範圍