Explain詳解與索引優化實踐

一、何爲explain執行計數據結構

使用explain關鍵字能夠模擬優化器執行SQL語句,從而知道MySQL是如何使用索引來處理你的SQL查詢語句以及鏈接表,能夠分析查詢語句或是結構的性能瓶頸,幫助咱們選擇更好的索引和寫出更優化的查詢語句。(說白了,就是優化SQL的工具函數

二、如何使用explain?工具

在你的SQL查詢語句前加上 explain 便可,如explain select * from table,MySQL會在查詢上設置一個標記,執行查詢時,會返回執行計劃的信息,而不是執行這條SQL(若是 from 中包含子查詢,仍會執行該子查詢,將結果放入臨時表)。性能

三、使用explain的例子優化

須要使用三張表,分別爲 actor 演員表,film 電影表,film_actor 電影-演員關聯表。spa

CREATE TABLE `actor` (
  `id` int(11) NOT NULL COMMENT '主鍵id',
  `name` varchar(45) DEFAULT NULL COMMENT '演員名稱',
  `update_time` datetime DEFAULT NULL COMMENT '修改時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;3d

insert into `actor` (`id`, `name`, `update_time`) values('1','a','2020-02-11 22:56:00');
insert into `actor` (`id`, `name`, `update_time`) values('2','b','2020-02-11 22:56:00');
insert into `actor` (`id`, `name`, `update_time`) values('3','c','2020-02-11 22:56:00');指針

CREATE TABLE `film` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
  `name` varchar(10) DEFAULT NULL COMMENT '電影名稱',
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;blog

insert into `film` (`id`, `name`) values('3','film0');
insert into `film` (`id`, `name`) values('1','film1');
insert into `film` (`id`, `name`) values('2','film2');排序

CREATE TABLE `film_actor` (
  `id` int(11) NOT NULL COMMENT '主鍵id',
  `film_id` int(11) NOT NULL COMMENT '電影id',
  `actor_id` int(11) NOT NULL COMMENT '演員id',
  `remark` varchar(255) DEFAULT NULL COMMENT '備註',
  PRIMARY KEY (`id`),
  KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into `film_actor` (`id`, `film_id`, `actor_id`, `remark`) values('1','1','1',NULL);
insert into `film_actor` (`id`, `film_id`, `actor_id`, `remark`) values('2','1','2',NULL);
insert into `film_actor` (`id`, `film_id`, `actor_id`, `remark`) values('3','2','1',NULL);

執行完以上SQL後,三張表數據對應以下:

下面展現explain中每一個列的信息:

(1)id列

id列的編號是select語句的序列號,有幾個 select 就有幾個id,而且id的序號是按 select 出現的順序而增加的(id越大,對應的select語句越先執行,若是id相等,則從上往下執行,id爲NULL最後執行)。

MySQL將select查詢分爲簡單查詢(SIMPLE)和複雜查詢(PRIMARY)。

複雜查詢分爲三類:簡單子查詢、派生表(from語句中的子查詢)、union查詢。

1)簡單子查詢

執行SQL語句:EXPLAIN SELECT (SELECT 1 FROM actor LIMIT 1) FROM film

2)from子句中的子查詢

執行SQL語句:EXPLAIN SELECT id FROM (SELECT id FROM film) AS der

分析:這個查詢執行時有個臨時表別名爲der,外部select查詢引用了這個臨時表。

3)union查詢

執行SQL語句:EXPLAIN SELECT 1 UNION ALL SELECT 1

分析:union結果老是放在一個匿名臨時表中,臨時表不在SQL中出現,所以它的id爲NULL。(不推薦使用union,性能不高)

(2)select_type列

這一列表示對應行是簡單仍是複雜查詢,若是是複雜查詢,又是上述三種複雜查詢中的哪種。

1)SIMPLE:簡單查詢。查詢不包含子查詢和union。

執行SQL語句:EXPLAIN SELECT * FROM film WHERE id=2

2)PRIMARY:複雜查詢中最外層的select。

3)SUBQUERY:包含在select中的子查詢(不在from子句中)。

4)DERIVED:包含在from子句中的子查詢。MySQL會將結果存放在一個臨時表中,也稱爲派生表(DERIVED的英文含義)。

執行SQL語句:EXPLAIN SELECT (SELECT 1 FROM actor WHERE id=1) FROM (SELECT * FROM film WHERE id=1) der

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

6)UNION RESULT:從union臨時表檢索結果的select。

執行SQL語句:EXPLAIN SELECT 1 UNION ALL SELECT 1

(3)table列

這一列表示explain的一行正在訪問哪一個表。

當from子句中有子查詢時,table列是<DERIVED N>格式,表示當前查詢依賴id=N的查詢,因而先執行id=N的查詢。

當有union時,UNION RESULT的table列的值爲<union 1,2>,1和2表示參與union的select行id。

(4)type列

(舒適提示:如下部分理論有可能解釋完仍是懵逼,不要緊,繼續往下看,有實踐例子)

這一列表示關聯類型或訪問類型,即MySQL決定如何查找表中的行,查找數據記錄的大概範圍。

SQL語句查詢效率從最優到最差依次爲:system > const > eq_ref > ref > range > index > ALL

通常來講,得保證查詢達到range級別,最好達到ref

NULL:MySQL可以在SQL語句執行以前(即優化階段)分析分解查詢語句,在執行階段用不着再訪問表或索引。例如:在索引列中選取最小值,能夠單獨查找索引來完成,不須要在執行時訪問表,出現的頻率不高。

const,system:MySQL可以對查詢的某部分進行優化並將其轉化成一個常量(能夠看show warnings的結果)。用於主鍵索引或惟一索引的全部列與常數比較時,表最多有一個匹配行,讀取1次,速度比較快。system是const的特例,表裏只有一條記錄匹配時爲system。

執行SQL語句:EXPLAIN EXTENDED SELECT * FROM (SELECT * FROM film WHERE id=1) tmp

分析:上面的子查詢SELECT * FROM film WHERE id = 1語句where後面id使用的是主鍵索引查詢,主鍵是惟一的,因此查詢結果必定是隻有一條記錄,對於明確知道結果集只有一條記錄的查詢,它的type爲const類型,性能已經很是高了;而第一個select複雜查詢的表只有一條記錄,因此結果也確定只有一條記錄(第二個select子查詢以前表中多是多條記錄),這種特例它的type爲system類型,性能最高。

執行SQL語句:EXPLAIN EXTENDED SELECT * FROM (SELECT * FROM film WHERE id=1) tmp;  SHOW WARNINGS;

分析:用explain extended查看執行計劃會比explain多一列filtered,該列給出一個百分比的值,這個值和rows列一塊兒使用,能夠估計出那些將要和explain中的前一個表進行鏈接的行的數目,前一個表就是指explain的id列的值比當前表的id小的表。explain extended還能夠搭配show warnings一塊兒使用,它能夠給出一個優化建議,真正執行時是執行優化建議的那條SQL,可是若是是很複雜的SQL,它優化出來的結果可能都沒你原先的SQL性能高。

eq_ref:主鍵索引或惟一索引的全部部分被鏈接使用,最多隻會返回一條符合條件的記錄。這多是在const以外最好的鏈接類型了,簡單的select查詢不會出現這種type。

執行SQL語句:EXPLAIN SELECT * FROM film_actor LEFT JOIN film ON film_actor.film_id=film.id

分析:有兩條記錄,說明有2次查詢, id相等,則從上往下執行,說明第1條先執行查詢film_actor表,第2條左鏈接查詢film表。左鏈接film表並關聯film.id,因爲film.id是惟一索引,film表只能關聯一行記錄,因此第2條select的type爲eq_ref。

ref:相比eq_ref,不使用惟一索引,而是使用普通索引或者惟一索引的前綴部分,索引要和某個值相比較,可能會找到多條符合條件的記錄。

① 簡單select查詢,name是普通索引(非惟一索引)

執行SQL語句:EXPLAIN SELECT * FROM film WHERE NAME="film1"

② 關聯表查詢,idx_film_actor_id是film_id和actor_id的聯合索引,這裏使用了film_actor的索引左邊前綴部分 film_id。

執行SQL語句:EXPLAIN SELECT * FROM film LEFT JOIN film_actor ON film.id=film_actor.film_id

range:範圍掃描一般出如今in(),between,>,<,>=等操做中。使用一個索引來檢索給定範圍的行。

執行SQL語句:EXPLAIN SELECT * FROM actor WHERE id>1

index: 掃描全表索引,這一般會比ALL快一些。(index是從索引中讀取的,而ALL是從硬盤中讀取)

執行SQL語句:EXPLAIN SELECT * FROM film;(film表全部字段都加了索引)

ALL: 即全表掃描,意味着MySQL須要從頭至尾去查找所須要的行(不走索引)。一般狀況下這須要增長索引來優化了。

執行SQL語句:EXPLAIN SELECT * FROM actor;(actor表有一個字段沒加索引)

(5)possible_keys列

這一列顯示查詢可能使用哪些索引來查找。

explain時可能出現possible_key有列,而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可推斷出只使用了第一個列flim_id來執行索引查找。

執行SQL語句: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

(8)ref列

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

(9)rows列

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

(10)Extra列 

這一列展現的是額外信息。常見的重要值以下:

Using index: 查詢的列被索引覆蓋,而且where篩選條件是索引的前導列(相似聯合索引的最左前綴原則),是性能高的表現。通常是使用了覆蓋索引(即索引包含了全部查詢的字段)。對於InnoDB來講,若是是普通索引性能會有很多提升。

執行SQL語句:EXPLAIN SELECT film_id FROM film_actor WHERE film_id=1

Using where:查詢的列不徹底被索引覆蓋,where篩選條件非索引的前導列。(不走索引,性能較低)

執行SQL語句:EXPLAIN SELECT * FROM actor WHERE name='a'

 Using where; Using index:查詢的列被索引覆蓋,而且where篩選條件是索引列之一但不是索引的前導列,意味着沒法直接經過索引來查找符合條件的數據。

執行SQL語句:EXPLAIN SELECT film_id FROM film_actor WHERE actor_id=1

NULL:查詢的列未被索引覆蓋,而且where篩選條件是索引的前導列,意味着用到了索引,可是部分字段未被索引覆蓋,必須經過「回表」來實現,不是純粹地用到了索引,也不是徹底沒用到索引。

執行SQL語句:EXPLAIN SELECT * FROM film_actor WHERE film_id=1

Using index condition:MySQL 5.6版本開始加入的新特性,與Using where相似,查詢的列不徹底被索引覆蓋,where條件中是一個前導列的範圍。

執行SQL語句:EXPLAIN SELECT * FROM film_actor WHERE film_id>1

Using temporary:MySQL須要建立一張臨時表來處理查詢。出現這種狀況通常是要進行優化的,首先要想到用索引來優化。

① actor.name沒有索引,此時建立了一張臨時表來distinct。(distinct:去除查詢結果中的重複記錄)

執行SQL語句:EXPLAIN SELECT DISTINCT NAME FROM actor

② film.name創建了idx_name索引,此時查詢時extra是Using index,沒有用臨時表。

執行SQL語句:EXPLAIN SELECT DISTINCT NAME FROM film

Using filesort:MySQL會對結果使用一個外部索引排序,而不是按照索引次序從表裏讀取行。此時MySQL會根據鏈接類型瀏覽全部符合條件的記錄,並保存排序關鍵字和行指針,而後排序關鍵字並按順序檢索行信息。這種狀況下通常也是要考慮使用索引來優化。

① actor.name未建立索引,會瀏覽actor整個表,保存排序關鍵字name和對應的id,而後排序name並檢索行記錄。

執行SQL語句:EXPLAIN SELECT * FROM actor ORDER BY name

② film.name創建了idx_name索引,此時查詢時extra是Using index,由於索引底層數據結構已是排好序的。

執行SQL語句:EXPLAIN SELECT * FROM film ORDER BY name

四、索引優化最佳實踐

使用了 employees 員工表:

CREATE TABLE `employees` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
  `name` varchar(24) NOT NULL COMMENT '員工姓名',
  `age` int(11) NOT NULL DEFAULT '0' COMMENT '員工年齡',
  `position` varchar(20) NOT NULL COMMENT '員工職位',
  `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時間',
  PRIMARY KEY (`id`),
  KEY `idx_name_age_position` (`name`,`age`,`position`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

insert into `employees` (`id`, `name`, `age`, `position`, `hire_time`) values('1','LiLei','22','manager','2020-02-13 14:22:55');
insert into `employees` (`id`, `name`, `age`, `position`, `hire_time`) values('2','HanMeimei','23','dev','2020-02-13 14:22:57');
insert into `employees` (`id`, `name`, `age`, `position`, `hire_time`) values('3','Lucy','23','dev','2020-02-13 14:22:59');

(1)全值匹配

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name='LiLei'

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name='LiLei' AND age=22

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name='LiLei' AND age=22 AND position='manager'

(2)索引最左前綴原則 

若是索引了多列,要遵循最左前綴原則。指的是查詢從索引的最左前列開始而且不跳過索引中的列。

提問:爲何聯合索引要想命中索引必須採用最左前綴原則?(命中索引:便是否用到了索引)

如下索引優化規則不少均可以結合下面這張圖思考,聯合索引底層的索引數據結構圖(B+樹),索引的排序首先按10002排序,接着是Staff,最後纔是1996-08-03,若是不先拿第一個字段10002去比較,根本無法比較,致使沒法命中索引。

提問:如下SQL命中索引?

① EXPLAIN SELECT * FROM employees WHERE age = 22 AND position = 'manager';
② EXPLAIN SELECT * FROM employees WHERE position = 'manager';
③ EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';
④ EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' AND position = 'manager';

分析:

①中的where條件後面age=22不是索引的最左前列,後面就不用看了,沒有命中索引,②也是如此。

③中的name是索引idx_name_age_position的最左前列,命中索引。

④中的name命中索引,position沒有命中索引,由於跳過索引中的age列,中間斷了,age列仍是須要全表掃描。

(3)不要在索引列上作任何操做(如計算、函數、自動或手動類型轉換),不然會致使索引失效而轉向全表掃描

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE LEFT(name, 3)='LiLei'

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

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name='LiLei' AND age>22 AND position='manager'

分析:長度爲78,name爲74,age是int類型,因此爲4,即只有name和age命中索引,position沒有命中索引,由於它屬於age範圍條件右邊的索引列。

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

執行SQL語句:EXPLAIN SELECT name,age FROM employees WHERE name='LiLei'

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name='LiLei'

 (6)MySQL在使用不等於(!= 或者 <>)的時候沒法使用索引,會致使全表掃描

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name != 'LiLei'

(7)is null,is not null也沒法使用索引

執行SQL語句:

EXPLAIN SELECT * FROM employees WHERE name IS NULL

(8)like以通配符開頭('$abc'),MySQL索引會失效致使全表掃描

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name LIKE '%Lei'

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name LIKE 'Lei%'

提問:如何解決like '%字符串%' 索引沒有命中?

① 使用覆蓋索引,查詢字段必須是創建覆蓋索引字段

執行SQL語句:EXPLAIN SELECT name,age,position FROM employees WHERE name LIKE '%Lei%'

② 當覆蓋索引指向的字段是varchar(380)及以上的字段時,覆蓋索引會失效!

(9)字符串不加單引號,索引失效(內部會作一個字符串轉換函數)

執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name=1000

(10)少用or或in,用它查詢時,非主鍵字段的索引會失效,主鍵索引有時生效,有時不生效,跟數據量有關,具體還得看MySQL的查詢優化結果

 執行SQL語句:EXPLAIN SELECT * FROM employees WHERE name='LiLei' OR name='Hanmeimei'

總結

like KK% 至關於等於常量,%KK 和 %KK% 至關於範圍。

相關文章
相關標籤/搜索