項目開發中,性能是我們比較關注的問題,特別是數據庫的性能;作爲一個開發,經常和SQL語句打交道,想要寫出合格的SQL語句,我們需要了解SQL語句在數據庫中是如何掃描表、如何使用索引的;
MySQL提供explain/desc命令輸出執行計劃,我們通過執行計劃優化SQL語句。
下面我們以MySQL5.7爲例瞭解一下執行計劃:
注:文中涉及到的表結構、sql語句只是爲了理解explain/desc執行計劃,有不合理之處勿噴
explain/desc 用法
只需要在我們的查詢語句前加 explain/desc即可
準備數據表
1 -- 創建user表
2 create table user(
3 id int,
4 name varchar(20),
5 role_id int,
6 primary key(id)
7 )engine=innodb default charset=utf8;
8 -- 創建role表
9 create table role(
10 id int,
11 name varchar(20),
12 primary key(id)
13 )engine=innodb default charset=utf8;
查詢,執行計劃
explain select * from user;
執行計劃輸出有id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、extra
,這些內容有什麼意義,下面簡單介紹一下
explain/desc 輸出詳解
一、id ,select
查詢序列號
1 id相同,從上往下一次執行;
-- 左關聯
explain select * from user a left join user b on a.id=b.id;
-- 右關聯
explain select * from user a right join user b on a.id=b.id;
通過left join
和 right join
驗證;id一樣(注意執行計劃的table列),left join
先掃描a表,再掃描b表;right join
先掃描b表,再掃描a表
2 id不同,id越大優先級越高,越先被執行
desc select * from user where role_id=(select id from role where name='開發');
我們編寫查詢角色爲開發的用戶;可以知道先查詢角色name爲開發角色id,查詢序列號爲2;再根據角色id查詢用戶,查詢序列號爲1;
二、select_type
,查詢語句類型
(1) SIMPLE
(簡單SELECT
,不使用UNION
或子查詢等)
explain select * from user;
(2)PRIMARY
(查詢中若包含任何複雜的子部分,最外層的select
被標記爲PRIMARY
)
desc select * from user where role_id=(select id from role where name='開發');
(3) UNION
(UNION
中的第二個或後面的SELECT
語句)
desc select * from user where name='Java' union select * from user where role_id=1;
(4) DEPENDENT
UNION
(UNION
中的第二個或後面的SELECT
語句,取決於外面的查詢)
desc select * from user a
where id in (
select b.id from user b where b.id=a.id union
select c.id from role c where c.id=a.role_id
);
(5) UNION RESULT
(UNION
的結果)
desc select * from user where name='Java' union select * from user where role_id=1;
(6)SUBQUERY
(子查詢中的第一個SELECT
)
1 desc select * from user where role_id=(select id from role where name='開發');
(7) DEPENDENT SUBQUERY
(子查詢中的第一個SELECT
,取決於外面的查詢)
desc select * from user where role_id = ( select id from role where id=user.id );
(8) DERIVED(派生/衍生表的SELECT, FROM子句的子查詢)
1 desc select * from ( select * from user where name='Java' union select * from user where role_id=1 ) a;
(9) MATERIALIZED(物化子查詢) 在SQL執行過程中,第一次需要子查詢結果時執行子查詢並將子查詢的結果保存爲臨時表 ,後續對子查詢結果集的訪問將直接通過臨時表獲得。
(10) UNCACHEABLE SUBQUERY(一個子查詢的結果不能被緩存,必須重新評估外鏈接的第一行)
(11) UNCACHEABLE UNION(UNION查詢的結果不能被緩存)
三、table
,查詢涉及的表或衍生表
table分別user、role表
四、partitions
查詢涉及到的分區
創建分區表,
1 -- 創建分區表,
2 -- 按照id分區,id<100 p0分區,其他p1分區
3 create table user_partitions (id int auto_increment,
4 name varchar(12),primary key(id))
5 partition by range(id)(
6 partition p0 values less than(100),
7 partition p1 values less than maxvalue
8 );
desc select * from user_partitions where id>200;
查詢id大於200(200>100,p1分區)的記錄,查看執行計劃,partitions是p1,符合我們的分區規則
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-S3TWJSQp-1591606490044)(~/imgs/1257218-20190521193453611-1758853509.png)]
五、type
提供了判斷查詢是否高效的重要依據依據
通過 type
字段, 我們判斷此次查詢是全表掃描還是索引掃描等,下面簡單介紹一下常用的type;
(1)system: 表中只有一條數據,相當於系統表; 這個類型是特殊的 const
類型;
(2)const: 主鍵或者唯一索引的常量查詢,表格最多隻有1行記錄符合查詢,通常const使用到主鍵或者唯一索引進行定值查詢。
主鍵
1 -- 創建user表
2 create table user(id int primary key, name varchar(20), role_id int );
3 -- 插入一條記錄
4 insert into user values (1, 'a', 1 );
5 -- 按id查詢
6 desc select * from user where id=1;
7 -- 按role_id查詢
8 desc select * from user where role_id=1;
分別查看按id和按role_id查詢的執行計劃;發現按主鍵id查詢,執行計劃type爲const
將主鍵設置爲id和role_id
1 -- 刪除主鍵
2 alter table user drop primary key;
3 -- 設置主鍵id,role_id
4 alter table user add primary key(id,role_id);
5 -- 按照部分主鍵查詢
6 desc select * from user where id=1;
7 -- 按照部分主鍵查詢
8 desc select * from user where role_id=1;
9 -- 按照全部主鍵查詢
10 desc select * from user where id=1 and role_id=1;
發現只有按照全部主鍵查詢,執行計劃type爲const
唯一索引
1 -- 刪除主鍵
2 alter table user drop primary key;
3 -- 設置主鍵
4 alter table user add primary key(id);
5 -- 設置role_id爲唯一索引
6 alter table user add unique key uk_role(role_id);
7 -- 按照唯一索引查詢
8 desc select * from user where role_id=1;
發現按role_id唯一索引查詢;執行計劃type爲const
普通索引
1 -- 將role_id設置成普通索引
2 -- 刪除唯一索引
3 alter table user drop index uk_role;
4 -- 設置普通索引
5 alter table user add index index_role(role_id);
6 -- 按照普通索引查詢
7 desc select * from user where role_id=1;
發現按role_id普通索引查詢;執行計劃type爲ref
const用於主鍵或唯一索引查詢;將PRIMARY KEY
或 UNIQUE
索引的所有部分與常量值進行比較時使用;與索引類型有關。
(3)eq_ref: 除了system和const類型之外,效率最高的連接類型;唯一索引掃描,對於每個索引鍵,表中只有一條記錄與之對應;常用於主鍵或唯一索引掃描
準備數據
1 -- 創建teacher表
2 create table teacher( id int primary key, name varchar(20), tc_id int );
3 -- 插入3條數據
4 insert into teacher values (1,'a',1),(2,'b',2),(3,'c',3);
5 -- 創建teacher_card表
6 create table teacher_card( id int primary key, remark varchar(20) );
7 -- 插入2條數據
8 insert into teacher_card values (1,'aa'),(2,'bb');
9 -- 關聯查詢,執行計劃
10 desc select * from teacher t join teacher_card tc on t.tc_id=tc.id where t.name='a';
執行計劃
根據上面的知識;可知id相同,由上至下依次執行,分析結果可知:
先查詢t表就是teacher表中name字段爲a的記錄,由於name字段沒有索引,所以全表掃描(type:ALL),一共有3條記錄,掃描了3行(rows:3),1條符合條件(filtered:33.33 1/3);
再查詢tc即teacher_card表使用主鍵和之前的t.tc_id關聯;由於是關聯查詢,並且是通過唯一索引(主鍵)進行查詢,僅能返回1或0條記錄,所以type爲eq_ref。
1 -- 刪除teacher_card主鍵
2 alter table teacher_card drop primary key;
3 -- 這是teacher_card.id爲唯一索引
4 alter table teacher_card add unique key ui_id(id);
5 -- 關聯查詢,執行計劃
6 desc select * from teacher t join teacher_card tc on t.tc_id=tc.id where t.name='a';
分析結果,將teacher_card的id設置爲唯一索引,type爲eq_ref;滿足僅能返回1或0條記錄。
1 -- 刪除teacher_card唯一索引
2 alter table teacher_card drop index ui_id;
3 -- 設置teacher_card.id爲普通索引
4 alter table teacher_card add index index_id(id);
5 -- 關聯查詢,執行計劃
6 desc select * from teacher t join teacher_card tc on t.tc_id=tc.id where t.name='a';
分析結果,將teacher_card的id設置爲普通索引,type爲ref;不滿足僅能返回1或0條記錄。
equ_ref用於唯一索引查詢,對每個索引鍵,表中只有一條或零條記錄與之匹配。
(4)ref:此類型通常出現在多表的 join 查詢, 針對於非唯一或非主鍵索引, 或者是使用了最左前綴規則索引的查詢(換句話說,連接不能基於鍵值選擇單行,可能是多行)。
1 -- teacher.tc_id無索引,執行計劃
2 desc select * from teacher t join teacher_card tc on t.tc_id=tc.id where tc.remark='aa';
3 -- 設置teacher.tc_id爲普通索引
4 alter table teacher add index index_tcid(tc_id);
5 -- teacher.tc_id有索引,執行計劃
6 desc select * from teacher t join teacher_card tc on t.tc_id=tc.id where tc.remark='aa';
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-RqoFwR9l-1591606490056)(~/imgs/1257218-20190523200222716-425466551.png)]
先查詢tc表就是teacher_card表中remark字段爲aa的記錄,由於remark字段沒有索引,所以全表掃描(type:ALL),一共有2條記錄,掃描了2行(rows:2),1條符合條件(filtered:50,1/2);
tc_id無索引 再查詢t即teacher表使用tc_id和之前的tc.id關聯;由於是關聯查詢,不是索引,全表掃描,所以type爲ALL。
tc_id有索引 再查詢t即teacher表使用tc_id和之前的tc.id關聯;由於是關聯查詢,索引掃描,能返回0或1或多條記錄,所以type爲ref。
(5)range: 表示使用索引範圍查詢, 通過索引字段範圍獲取表中部分數據記錄. 這個類型通常出現在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中。
1 desc select * from teacher where id>2;
2 desc select * from teacher where id in (1,2,3);
(6)index
: 掃描索引樹
如果索引是複合索引,並且複合索引列滿足select所需的所有數據,則僅掃描索引樹。在這種情況下,Extra
爲 Using index
。僅索引掃描通常比ALL索引的大小通常小於表數據更快 。
索引列不滿足select所需的所有數據,此時需要回表掃描;按索引順序查找數據行。 Uses index
沒有出現在 Extra
列中。
1 -- 查看teacher表索引
2 show index from teacher;
3 -- 查詢tc_id,執行計劃
4 desc select tc_id from teacher;
5 -- 按tc_id索引分組,執行計劃
6 desc select name from teacher group by tc_id;
查詢tc_id,掃描索引樹,type爲index,Extra
爲 Using index;
按tc_id分組,全表掃描,以按索引順序查找數據行。
(7)ALL
: 全表掃描,沒有任何索引可以使用時。這是最差的情況,應該避免。
1 -- 查看teacher表索引
2 show index from teacher;
3 desc select * from teacher where name='a';
由於name字段不存在索引,type:ALL全表掃描;可通過對name字段設置索引進行優化。
六、possible_keys:指示MySQL可以從中選擇查找此表中的行的索引。
七、key:MySQL查詢實際使用到的索引。
1 -- 創建course表
2 create table course(id int primary key,name varchar(20),t_id int,key index_name(name),key index_tid(t_id));
3 -- 插入數據
4 insert into course values (1,'Java',1), (2,'Python',2);
5 -- 查詢1
6 desc select * from course where name='Java' or t_id=1;
7 -- 查詢2
8 desc select * from course where name='Java';
查看執行計劃
查詢1,查詢name爲Java或t_id爲1的記錄;可能用到的索引possible_keys爲index_name,index_tid;實際用到的索引key爲NULL
查詢2,查詢name爲Java;可能用到的索引possible_keys爲index_name;實際用到的索引key爲index_name
八、key_len:表示索引中使用的字節數(只計算利用索引作爲index key的索引長度,不包括用於group by/order by的索引長度)
- 一般地,key_len 等於索引列類型字節長度,例如int類型爲4 bytes,bigint爲8 bytes;
- 如果是字符串類型,還需要同時考慮字符集因素,例如utf8字符集1個字符佔3個字節,gbk字符集1個字符佔2個字節
- 若該列類型定義時允許NULL,其key_len還需要再加 1 bytes
- 若該列類型爲變長類型,例如 VARCHAR(TEXT\BLOB不允許整列創建索引,如果創建部分索引也被視爲動態列類型),其key_len還需要再加 2 bytes
字符集會影響索引長度、數據的存儲空間,爲列選擇合適的字符集;變長字段需要額外的2個字節,固定長度字段不需要額外的字節。而null都需要1個字節的額外空間,所以以前有個說法:索引字段最好不要爲NULL,因爲NULL讓統計更加複雜,並且需要額外一個字節的存儲空間。
1 -- key_len的長度計算公式:
2
3 -- varchar(len)變長字段且允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(變長字段)
4 -- varchar(len)變長字段且不允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)+2(變長字段)
5
6 -- char(len)固定字段且允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)
7 -- char(len)固定字段且不允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)
下面驗證一下結論:
1 -- 創建user_info表
2 create table user_info( id int comment '主鍵',
3 name varchar(10) character set utf8 not null comment '姓名',
4 alias_name varchar(10) character set utf8 comment '姓名',
5 role_id int comment '角色id',
6 remark varchar(10) character set gbk not null comment '備註',
7 primary key(id),
8 key index_name(name),
9 key index_alias(alias_name),
10 key index_role(role_id),
11 key index_remark(remark)
12 )engine=innodb;
13 -- 插入數據
14 insert into user_info values (1,'a','aa',1,'aaa');
15 -- 按主鍵查詢
16 desc select * from user_info where id=1;
17 -- 按索引role_id查詢
18 desc select * from user_info where role_id=1;
按照主鍵id查詢possible_keys爲primary,實際用到的索引key爲primary,key_len爲4;
按照索引role_id查詢possible_keys爲index_role,實際用到的索引key爲index_role,key_len爲5;
分析結果:按照role_id比按照id(均爲int類型)的key_len大5-4=1,因爲role_id可以爲null,需要一個標誌位;
1 -- 按照name查詢 varchar(10) not null utf8 一個字符佔3個字節 10*3+2(變長)=32
2 desc select * from user_info where name='a';
3 -- 按照alias_name查詢 varchar(10) utf8 一個字符佔3個字節 10*3+2(變長)+1(null標誌位)=33
4 desc select * from user_info where alias_name='aa';
按照name查詢possible_keys爲index_name,實際用到的索引key爲index_name,key_len爲32=10*3+2(變長);
按照alias_name查詢possible_keys爲index_alias,實際用到的索引key爲index_alias,key_len爲33=10*3+2(變長)+1(null標誌位);
分析結果:name與remark均爲變長且字符集一致,remark可以爲null,33-32=1多佔一個標誌位;
1 -- 按照name查詢 varchar(10) not null utf8 一個字符佔3個字節 10*3+2(變長)=32
2 desc select * from user_info where name='a';
3 -- 按照remark查詢 varchar(10) not null gbk 一個字符佔2個字節 10*2+2(變長)=22
4 desc select * from user_info where remark='aaa';
按照name查詢possible_keys爲index_name,實際用到的索引key爲index_name,key_len爲32=10*3(utf8一個字符3個字節)+2(變長);
按照remark查詢possible_keys爲index_remark,實際用到的索引key爲index_remark,key_len爲22=10*2(gbk一個字符2個字節)+2(變長);
分析結果:name與remark均爲變長但字符集不一致,分別爲utf8與gbk;符合公式;
1 -- 將name修改爲char(10) 定長 character set utf8 not null
2 alter table user_info modify name char(10) character set utf8 not null;
3 -- 按照name查詢 varchar(10) not null utf8 一個字符佔3個字節 10*3=30
4 desc select * from user_info where name='a';
按照name查詢possible_keys爲index_name,實際用到的索引key爲index_name,key_len爲30;
因爲將name修改爲char(10) 定長 character set utf8 not null,10*3=30;符合公式
九、ref:顯示該表的索引字段關聯了哪張表的哪個字段
1 desc select * from user,role where user.role_id=role.id;
通過執行計劃可知,role表執行計劃ref爲study.user.role_id;說明role.id關聯user.role_id;
十、rows:根據表統計信息及選用情況,大致估算出找到所需的記錄或所需讀取的行數,數值越小越好
十一、filtered:返回結果的行數佔讀取行數的百分比,值越大越好
-- 查看teacher數據
select * from teacher;
-- 查看teacher_card數據
select * from teacher_card;
-- 查詢語句
select * from teacher t join teacher_card tc on t.tc_id=tc.id where t.name='a';
-- 執行計劃
desc select * from teacher t join teacher_card tc on t.tc_id=tc.id where t.name='a';
根據上面的知識;可知id相同,由上至下依次執行,分析結果可知:
先查詢t表就是teacher表中name字段爲a的記錄,由於name字段沒有索引,所以全表掃描(type:ALL),一共有3條記錄,掃描了3行(rows:3),1條符合條件(filtered:33.33 1/3);
再查詢tc即teacher_card表使用主鍵和之前的t.tc_id關聯;掃描索引*(type:ref)*,返回1條記錄,最終返回1條記錄,*(filtered:100 1/1)*。
十二、extra:包含不適合在其他列中顯示但十分重要的額外信息。常見的值如下
use filesort:MySQL會對數據使用非索引列進行排序,而不是按照索引順序進行讀取;若出現改值,應優化索引
1 -- 查看user索引
2 show index from user;
3 -- 查詢name並排序
4 desc select name from user order by name;
5 -- 爲name列設置索引,優化
6 alter table user add index index_name(name);
7 -- 查詢name並排序
8 desc select name from user order by name;
use temporary:使用臨時表保存中間結果,比如,MySQL在對查詢結果排序時使用臨時表,常見於order by和group by*;若出現改值,應優化索引*
use index:表示select操作使用了索引覆蓋,避免回表訪問數據行,效率不錯
use where:where子句用於限制哪一行
1 -- 創建student表
2 create table student(
3 id int,
4 first_name varchar(10),
5 last_name varchar(10),
6 primary key(id),
7 key index_first(first_name)
8 )engine=innodb default charset=utf8;
9 -- 插入數據
10 insert into student values (1,'a','b');
11 -- 按照first_name查找
12 desc select first_name,last_name from student where first_name='a';
13 -- 設置first_name,last_name複合索引
14 alter table student drop index index_first;
15 alter table student add index index_name(first_name,last_name);
16 -- 按照first_name查找
17 desc select first_name,last_name from student where first_name='a';
分析結果:
當設置first_name爲普通索引(單列索引),按照first_name查詢;type:ref、possible_keys:indes_first、key:indes_first、extra:null,用到索引;
當設置first_name,last_name爲複合索引(聯合索引),按照first_name查詢;type:ref、possible_keys:indes_name、key:indes_name、extra:Using index;type:ref用到索引,因爲是複合索引不需要回表掃描,extra:Using index索引覆蓋;注意此時key_len爲33=103(utf8)+2(變長)+1(null標誌位),用到了複合索引的一部分即first_name
當設置first_name,last_name爲複合索引(聯合索引),按照last_name查詢;type:index、possible_keys:null、key:indes_name、extra:Using where,Using index;type:index而不是ref,掃描索引樹,複合索引的最左原則;此時key_len爲66=103(utf8)+2(變長)+1(null)+10*3(utf8)+2(變長)+1(null標誌位);Using where應where子句進行限制
小結:
根據MySQL執行計劃的輸出,分析索引使用情況、掃描的行數可以預估查詢效率;進而可以重構SQL語句、調整索引,提升查詢效率。
本文只是簡單介紹一下MySQL執行計劃,想全面深入瞭解MySQL,可優先閱讀MySQL官方手冊。