《為什么數據庫中要使用B+tree索引,而不用hash索引?MySQL中的B+tree索引介紹》
看完以上這篇文章,明白B+tree索引結構,對explain解析更有幫助。
MySQL官網doc文檔: https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
一、建立 t_user 表並建立3個B+tree索引
先在mysql中建個表:
crete table t_user (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`age` int(4) NOT NULL DEFAULT 20,
`birthday_date_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`remark` varchar(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`version` int(4) NOT NULL DEFAULT 0,
PRIMARY UNIQUE INDEX `idx_name`(`username`) USING BTREE,
`idx_age_remark`(`age`, `remark`) USING BTREE,
INDEX idx_create_time(create_time) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = DYNAMIC;
這個DDL創建了 4 個B+tree索引,我都用黃色背景標出來了。用同樣的DDL語句再復制一個t_user2表。
現在開始使用explain對各種查詢進行分析:
二、Explain解析SQL
重點:explain只認select關鍵字,每出現一次select,就生成一個id。所以如果有子查詢或者union之類的,會生成多個id。
| select_type | table | Extra | |||
| 值 | 解釋 | 值 | 解釋 | No tables used | 當SQL中沒有from子句時 |
| SIMPLE | 簡單的select 查詢,不使用 union 及子查詢 | system | 當表只有一行記錄(等於系統表),並且存儲引擎像MyISAM在統計表數據時是精確的,就會出現system。 像innodb這個存儲引擎在運行中就不會精確統計表數據(這里當然不是指select count(*)這種)。 這是const類型的特列,平時不會出現,這個也可以忽略不計。 |
Impossible WHERE | 當SQL中的where 條件永遠為false時 |
| PRIMARY | 最外層的 select 查詢 | const | 根據主鍵或者唯一二級索引列與常數進行等值匹配時 | no matching row in const table | 當查詢列表處有MIN或者MAX聚集函數,但是並沒有符合WHERE子句中的搜索條件的記錄時,將會提示該額外信息 |
| UNION | UNION 中的第二個或隨后的 select 查詢,不 依賴於外部查詢的結果集 | eq_ref | 主鍵索引(primary key)或者非空唯一索引(unique not null)等值掃描 | Using index | 當使用到覆蓋索引時 |
| UNION RESULT | UNION 結果集,臨時表。物化表,是結果緩存內存,臨時表。 | ref | 當使用普通二級索引(不包含唯一索引)與常量進行等值匹配時 | Using index condition | 索引條件下推 |
| SUBQUERY | 子查詢中的第一個 select 查詢,不依賴於外 部查詢的結果集。 | ref_or_null | 有時候我們不僅想找出某個二級索引列的值等於某個常數的記錄,還想把該列的值為NULL的記錄也找出來 | Using where | 需要使用where后面的條件一行行過濾。比如全表掃描,比如回表后又用了where其他條件過濾 |
| DEPENDENT UNION | UNION 中的第二個或隨后的 select 查詢,依賴於外部查詢的結果集 | index_merge | 一般情況下對於某個表的查詢只能使用到一個索引,在某些場景下可以使用索引合並的方式來執行查詢:比如主鍵列范圍查詢,或者其他二級索引等值匹配。 | Using join buffer | 在連接查詢執行過程中,當被驅動表不能有效的利用索引加快訪問速度,MySQL一般會為其分配一塊名叫join buffer的內存塊來加快查詢速度 |
| DEPENDENT SUBQUERY | 子查詢中的第一個 select 查詢,依賴於外部查詢的結果集 | unique_subquery | 當使用in到子查詢時,而且in的對象是一個主鍵時 | Not exists | 當我們使用左(外)連接時,如果WHERE子句中包含要求被驅動表的某個列等於NULL值的搜索條件,而且那個列又是不允許存儲NULL值的,那么在該表的執行計划的Extra列就會提示Not exists額外信息 |
| DERIVED | 用於 from 子句里有子查詢的情況。 MySQL 會 遞歸執行這些子查詢, 把結果放在臨時表里。 這個是對派生表的物化。 | index_subquery | 當使用in到子查詢時,而且in的對象是二級索引時 | ||
| MATERIALIZED | 物化子查詢。這個是對子查詢的物化。 | range | 當使用了 = < > BETWEEN 等比較符在索引字段上時,注意mysql會把in, like也當成一種range掃描范圍的 | ||
| UNCACHEABLE SUBQUERY | 結果集不能被緩存的子查詢,必須重新為外層查詢的每一行進行評估,出現極少。 | index | 如果用到了覆蓋索引中的第二列或以上去select第一列,不需要回表,就是index | ||
| UNCACHEABLE UNION | UNION 中的第二個或隨后的select 查詢,屬於不可緩存的子查詢,出現極少。 | ALL | 全表掃描(full table scan) | ||
| 總結:通過某個小查詢的select_type屬性,就知道了這個小查詢在整個大查詢中扮演了一個什么角色。 | 總結: 執行計划的一條記錄就代表着MySQL對某個表的執行查詢時的訪問方法/訪問類型,其中的type列就表明了這個訪問方法/訪問類型是個什么東西,是較為重要的一個指標,結果值從最好到最壞依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 一般來說,得保證查詢至少達到range級別,最好能達到ref。 回表:回表是個典型的隨機IO,能不回表就不回表,盡量使用主鍵索引、二級覆蓋索引查詢。 |
||||
三、EXPLAIN的select_type列例子
3.1 select_type = SIMPLE
簡單的select 查詢,不使用 union 及子查詢。 EXPLAIN select * from t_user;

3.2 select_type = PRIMARY 或 UNION 或 UNION RESULT
EXPLAIN select * from t_user union select * from t_user;

3.3 select_type = SUBQUERY
EXPLAIN select * from t_user where id in ( select id from t_user2 ) or age = 40;

3.4 select_type = DEPENDENT SUBQUERY 或 DEPENDENT UNION
EXPLAIN warning 查看的例子也包含其中。 EXPLAIN select * from t_user where id in ( select id from t_user where id = 800 union select id from t_user where id = 900 );

3.5 select_type = DERIVED
EXPLAIN select * from ( select id, count(*) as c from t_user2 group by id ) t_derived where c > 1;

3.6 select_type = MATERIALIZED
EXPLAIN select * from t_user where age in ( select age from t_user2 );

這里的<subquery2>是子查詢的結果集臨時表名,外面的select查詢和這個子查詢結果集物化表,做了一個關聯查詢,所以兩個id都是1.
3.7 select_type = UNCACHEABLE SUBQUERY
這種情況主要是指用了什么變量,MySQL無法緩存該SQL,出現場景極少。
set @myid = 1043; EXPLAIN select * from t_user where id = ( select id from t_user2 where id = @myid );

四、EXPLAIN的table列例子
4.1 table = system
這里沒有例子。。。
4.2 table = const
根據主鍵或者唯一二級索引列與常數進行等值匹配時。 EXPLAIN select * from t_user where id = 1888;

4.3 table = eq_ref
這個t2是驅動表,t1是被驅動表。就像是訂單表(驅動表)和訂單詳情表(被驅動表),一般訂單表里有一條記錄,而訂單詳情表有N條記錄。
如果對這個被驅動表的訪問方式,是通過等於id主鍵或二級唯一索引這種等值比較,則是eq_ref
EXPLAIN select * from t_user t1 inner join t_user2 t2 on t1.id = t2.id;

4.4 table = ref
EXPLAIN select * from t_user t1 where age = 50; 通過普通的二級索引查詢。當使用普通二級索引(不包含唯一索引)與常量進行等值匹配時

4.5 table = ref_or_null
有時候我們不僅想找出某個二級索引列的值等於某個常數的記錄,還想把該列的值為NULL的記錄也找出來。
EXPLAIN select * from t_user t1 WHERE username = 'c7c70eca-903b-44b7-9a76-b3286bfa1d3a' or username is null;

4.6 table = index_merge
EXPLAIN select * from t_user t1 WHERE username = 'c7c70eca-903b-44b7-9a76-b3286bfa1d3a' or age = 10; 使用索引合並的方式查詢,但是一般情況下只會使用到一個二級索引

4.7 table = unique_subquery
EXPLAIN select * from t_user t1 WHERE t1.ID IN (SELECT t2.ID FROM t_user2 t2 WHERE t1.create_time = t2.create_time) or username = 'c7c70eca-903b-44b7-9a76-b3286bfa1d3a';
當使用in到子查詢時,而且in的對象是一個主鍵時,就會出現unique_subquery。這個例子中使用 show warnings/G 可以看到mysql把in優化成了exists

4.8 table = index_subquery
EXPLAIN select * from t_user t1 WHERE t1.username IN (SELECT t2.username FROM t_user2 t2 WHERE t1.create_time = t2.create_time) or username = 'c7c70eca-903b-44b7-9a76-b3286bfa1d3a';
當使用in到子查詢時,而且in的對象是一個二級索引時,就會出現index_subquery

4.9 table = range
EXPLAIN select * from t_user where age BETWEEN 15 and 18; 當使用了 = < > BETWEEN 等比較符在索引字段上時,注意mysql會把in, like也當成一種range掃描范圍的
EXPLAIN select * from t_user where username like 'a%';


4.10 table = index
EXPLAIN select age from t_user where remark = 15; 因為有個聯合索引 idx_age_remark(age,remark) 所以這里用了remark列,然后只查詢了age列,這兩個列都是這個聯合二級索引里的列,就成了索引覆蓋,不需要回表,所以這個查詢就是index,如果倒過來where age然后select remark的話就是ref 級別了。

4.11 table = ALL
EXPLAIN select * from t_user; 不帶任何條件,就是全表掃描

五、EXPLAIN的possible_keys 與 key
possible_keys是可能用到的索引,key是實際用到的索引。 EXPLAIN select username from t_user where create_time = '2021-06-02 08:19:14' and username like 'bbb%';

在下面這個例子中,可能用到的索引沒有,但實際用到了索引idx_age_remark,因為我們存在一個二級聯合索引 idx_age_remark(age, reamrk)。
EXPLAIN select age from t_user where remark = 10;

六、EXPLAIN的key_len
key_len表示用到的索引長度值。 EXPLAIN select * from t_user where id = 10; 這里用到id主鍵,由於主鍵時 int(11) 所以是int是定長4個字節。

但是在varchar列中,需要看該列的字符集長度,是不定長的。
比如這里用的utf8mb4是4個字節,這里Length是40,就是 40 * 4 = 160,再加上varchar需要是否為空的標記和長度信息,共占用2個字節,所以這里是162字節:
EXPLAIN select age from t_user where username like 'c7%';

username列在db里的設計截圖,是varchar(40)

數據類型本身占字節長度:
int(11) 4個字節。即使你寫成int(1)也沒用,仍然是占用4個字節
tinyint(3) 1個字節。即使你寫成tinyint(10)也沒用,仍然是占用一個字節
timestamp 4個字節,定長。
datetime 8個字節,定長。
gbk編碼為: 1個字符2個字節
utf8編碼為:1個字符3個字節
utf8mb4編碼為: 1個字符4個字節
七、EXPLAIN的ref
當使用索引列等值匹配的條件去執行查詢時,也就是在訪問方法是const、eg_ref、ref、ref_or_null、unique_sutbquery、index_subopery其中之一時,ref列展示的就是與索引列作等值匹配的是誰。
EXPLAIN select * from t_user t1 WHERE id = 400; 這個例子演示的是等值匹配。

ref 這一列的值是告訴你SQL中的被比較對象,是和哪個db庫的哪個表的哪個列進行比較。如下例中,就是對mytest庫的t2表的id列進行比較。
EXPLAIN select * from t_user t1 INNER JOIN t_user2 t2 on t1.id = t2.id; 這個例子演示的是連接產生的等值匹配。

EXPLAIN select * from t_user t1 INNER JOIN t_user2 t2 on t1.username = UPPER(t2.username) 這個例子證明,有時候等值匹配的也可以是個函數 func

八、EXPLAIN的rows
EXPLAIN select * from t_user where age = 70; 如果查詢優化器決定使用全表掃描的方式對某個表執行查詢時,執行計划的rows列就代表預計需要掃描的行數,如果使用索引來執行查詢時,執行計划的rows列就代表預計掃描的索引記錄行數。

EXPLAIN select * from t_user where username > 'a'; 這個例子中,表總數才一萬條,但是EXPLAIN預計需要掃描9815條,所以還不如全表掃描,所以 type = ALL

EXPLAIN select * from t_user where username > 'z'; 這個例子中,EXPLAIN預計需要掃描一條記錄就能找到,所以走了 type = range,而不是全表掃描

九、EXPLAIN的filtered
EXPLAIN select * from t_user t1 INNER JOIN t_user2 t2 on t1.id = t2.id WHERE t1.age > 80; 這個例子中,t1表結果集預計會是總數的 20.11%,而 t2表由於是需要查找的記錄全部都是確定值,所以過濾出來的數據是 100%

十、EXPLAIN的Extra
Extra的種類有幾十個之多,這里只介紹幾種常用的。
No tables used 當SQL中沒有from子句時:EXPLAIN select 1;

Impossible WHERE 當SQL中的where 條件永遠為false時:EXPLAIN select * from t_user where 1 != 1;

no matching row in const table 當查詢列表處有MIN或者MAX聚集函數,但是並沒有符合WHERE子句中的搜索條件的記錄時,將會提示該額外信息。也就是在該例子中,username = abc的記錄不存在.
EXPLAIN select min(age) from t_user where username = 'abc';

Using index 當使用到覆蓋索引時 EXPLAIN select remark from t_user where age = 9;

Using index condition 索引條件下推。即當 username > 'z' 找到一條記錄時,不急着回表,而是看看是不是還符合 username like '%z' ,如果符合,才回表,這就叫索引條件下推。
EXPLAIN select * from t_user where username > 'z' and username like '%a';

Using where 全表掃描。因為只能依靠where后面的條件一行行過濾 EXPLAIN select * from t_user where birthday_date_time = '1991-11-27 21:58:04';

Using where 已經使用了 age = 4 這個二級索引找到確切的數據,再回表根據 username > 'c' 這個where條件來過濾數據,這里重點是使用了where條件過濾了
EXPLAIN select * from t_user where age = 4 and username > 'c';

Using join buffer 在連接查詢執行過程中,當被驅動表不能有效的利用索引加快訪問速度,MySQL一般會為其分配一塊名叫join buffer的內存塊來加快查詢速度
EXPLAIN select * from t_user t1 INNER JOIN t_user2 t2 on t1.birthday_date_time = t2.birthday_date_time;

Not exists 當我們使用左(外)連接時,如果WHERE子句中包含要求被驅動表的某個列等於NULL值的搜索條件,而且那個列又是不允許存儲NULL值的,那么在該表的執行計划的Extra列就會提示Not exists額外信息
EXPLAIN select * from t_user t1 LEFT JOIN t_user2 t2 on t1.username = t2.username where t2.id is null;

Using intersect(...) 交集合並、Using union(...) 並集合並 和 Using sort_union(...) 並集有序合並
如果執行計划的Extra列出現了Using intersect(...)提示,說明准備使用Intersect索引合並的方式執行查詢,括號中的...表示需要進行索引合並的索引名稱;如果出現了Using union(...)提示,說明准備使用Union索引合並的方式執行查詢;出現了Using sort_union(...)提示,說明准備使用Sort-Union索引合並的方式執行查詢。
Zero limit 當我們的LIMIT子句的參數為0時,表示壓根兒不打算從表中讀出任何記錄,將會提示該額外信息。比如 limit 0;
Using filesort 當出現這個,說明SQL執行慢,比如 select * from t_user order by update_time asc limit 10; 這個排序就沒用到索引列。再比如 select * from t_user order by username limit 10; 像這種情況下對結果集中的記錄進行排序是可以使用到索引的。
Using temporary 臨時表。在許多查詢的執行過程中,MySQL可能會借助臨時表來完成一些功能,比如去重、排序之類的。
EXPLAIN select DISTINCT remark from t_user;

Using temporary與Using filesort同時出現,是因為mysql在處理group by中默認加上了order by,因為只有排序,才能分組。但是因為默認加了order by會導致消耗性能,所以可以自己加上order by null; 這樣手動處理后可以省去Using filesort這一步。
EXPLAIN select birthday_date_time,count(*) from t_user GROUP BY birthday_date_time;

十一、EXPLAIN的 json格式,查看更具體的cost耗時信息
想查看explain的里更具體的cost耗時信息,參考《mysql5.7版本explain解析,使用format=json查看cost耗時詳細信息》
其他知識
1)UNION關鍵字連接
union關鍵字會創建一張臨時表,但是union all就不會創建一張臨時表,因為:union all 的結果集不用去重!!!
EXPLAIN select id,username from t_user where id < 3 union select id,username from t_user where id > 10 and id < 12;

end.
