面試官說:工作這么久了科展,應(yīng)該知道sql執(zhí)行計劃吧争涌,講講Sql的執(zhí)行計劃吧损俭!
看了看面試官手臂上紋的大花臂和一串看不懂的韓文桦锄,吞了吞口水扎附,暗示自己鎮(zhèn)定點(diǎn),整理了一下思緒緩緩的對面試官說:我不會
面試官:结耀。留夜。。图甜。碍粥,回去等通知吧
我:%^&%$!@#
一、前言
當(dāng)我們工作到了一定的年限之后黑毅,一些應(yīng)該掌握的知識點(diǎn)嚼摩,我們是必須需要去了解的,比如今天面試官問的SQL執(zhí)行計劃
當(dāng)我們執(zhí)行一條SQL的時候矿瘦,可以直接對應(yīng)的結(jié)果枕面,但是你并不曉得,它會經(jīng)歷多深遠(yuǎn)黑暗的隧道缚去,通過連接器潮秘、查詢緩存、分析器易结、優(yōu)化器枕荞、執(zhí)行器重重篩選,才有可能展示到我們面前搞动,有時候當(dāng)你等待N長時間躏精,但是展現(xiàn)的卻是 timeout,這個時候想砸電腦的心都有了鹦肿,不過當(dāng)你看了今天的SQL執(zhí)行計劃后矗烛,你再也不用砸電腦了,看懂了這篇文章你就會知道這都不是事箩溃,讓我們一起來揭曉這里面的奧妙
在實(shí)際的應(yīng)用場景中瞭吃,為了知道優(yōu)化SQL語句的執(zhí)行碌识,需要查看SQL語句的具體執(zhí)行過程,以加快SQL語句的執(zhí)行效率虱而。
通常會使用explain+SQL語句來模擬優(yōu)化器執(zhí)行SQL查詢語句,從而知道m(xù)ysql是如何處理sql語句的开泽。
官網(wǎng)地址: https://dev.mysql.com/doc/refman/5.5/en/explain-output.html
首先我們來下面的一條sql語句牡拇,其中會有id、select_type 穆律、table 等等
這些列惠呼,這些就是我們執(zhí)行計劃中所包含的信息,我們要弄明白的就是這些列是用來干嘛的峦耘,以及每個列可能存在多少個值剔蹋。
explain select * from emp;
二、執(zhí)行計劃中包含的信息
列(Column) | 含義(Meaning) |
---|---|
id | The SELECT identifier(每個select子句的標(biāo)識id) |
select_type | The SELECT type(select語句的類型) |
table | The table for the output row(當(dāng)前表名) |
partitions | The matching partitions (顯示查詢將訪問的分區(qū)辅髓,如果你的查詢是基于分區(qū)表) |
type | The join type(當(dāng)前表內(nèi)訪問方式) |
possible_keys | The possible indexes to choose(可能使用到的索引) |
key | The index actually chosen(經(jīng)過優(yōu)化器評估最終使用的索引) |
key_len | The length of the chosen key (使用到的索引長度) |
ref | The columns compared to the index(引用到的上一個表的列) |
rows | Estimate of rows to be examined (要得到最終記錄索要掃描經(jīng)過的記錄數(shù)) |
filtered | Percentage of rows filtered by table condition(存儲引擎返回的數(shù)據(jù)在server層過濾后泣崩,剩下滿足查詢的記錄數(shù)量的比例) |
extra | Additional information (額外的信息說明) |
貼心的小農(nóng)已經(jīng)把sql復(fù)制出來了,只需要放到數(shù)據(jù)庫中執(zhí)行即可洛口,方便簡單快捷
——牧小農(nóng)
建表語句:
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`DEPTNO` int NOT NULL,
`DNAME` varchar(14) DEFAULT NULL,
`LOC` varchar(13) DEFAULT NULL,
PRIMARY KEY (`DEPTNO`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `dept` VALUES ('10', 'ACCOUNTING', 'NEW YORK');
INSERT INTO `dept` VALUES ('20', 'RESEARCH', 'DALLAS');
INSERT INTO `dept` VALUES ('30', 'SALES', 'CHICAGO');
INSERT INTO `dept` VALUES ('40', 'OPERATIONS', 'BOSTON');
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp` (
`EMPNO` int NOT NULL,
`ENAME` varchar(10) DEFAULT NULL,
`JOB` varchar(9) DEFAULT NULL,
`MGR` int DEFAULT NULL,
`HIREDATE` date DEFAULT NULL,
`SAL` double(7,2) DEFAULT NULL,
`COMM` double(7,2) DEFAULT NULL,
`DEPTNO` int DEFAULT NULL,
PRIMARY KEY (`EMPNO`),
KEY `idx_job` (`JOB`),
KEY `jdx_mgr` (`MGR`),
KEY `jdx_3` (`DEPTNO`),
KEY `idx_3` (`DEPTNO`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `emp` VALUES ('7369', 'SMITH', 'CLERK', '7902', '1980-12-17', '800.00', null, '20');
INSERT INTO `emp` VALUES ('7499', 'ALLEN', 'SALESMAN', '7698', '1981-02-20', '1600.00', '300.00', '30');
INSERT INTO `emp` VALUES ('7521', 'WARD', 'SALESMAN', '7698', '1981-02-22', '1250.00', '500.00', '30');
INSERT INTO `emp` VALUES ('7566', 'JONES', 'MANAGER', '7839', '1981-02-02', '2975.00', null, '20');
INSERT INTO `emp` VALUES ('7654', 'MARTIN', 'SALESMAN', '7698', '1981-09-28', '1250.00', '1400.00', '30');
INSERT INTO `emp` VALUES ('7698', 'BLAKE', 'MANAGER', '7839', '1981-01-05', '2850.00', null, '30');
INSERT INTO `emp` VALUES ('7782', 'CLARK', 'MANAGER', '7839', '1981-09-06', '2450.00', null, '10');
INSERT INTO `emp` VALUES ('7839', 'KING', 'PRESIDENT', null, '1981-11-17', '5000.00', null, '10');
INSERT INTO `emp` VALUES ('7844', 'TURNER', 'SALESMAN', '7698', '1981-09-08', '1500.00', '0.00', '30');
INSERT INTO `emp` VALUES ('7900', 'JAMES', 'CLERK', '7698', '1981-12-03', '950.00', null, '30');
INSERT INTO `emp` VALUES ('7902', 'FORD', 'ANALYST', '7566', '1981-12-03', '3000.00', null, '20');
INSERT INTO `emp` VALUES ('7934', 'MILLER', 'CLERK', '7782', '1982-01-23', '1300.00', null, '10');
DROP TABLE IF EXISTS `emp2`;
CREATE TABLE `emp2` (
`id` int NOT NULL AUTO_INCREMENT,
`empno` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `emp2` VALUES ('1', '111');
INSERT INTO `emp2` VALUES ('2', '222');
DROP TABLE IF EXISTS `salgrade`;
CREATE TABLE `salgrade` (
`GRADE` int NOT NULL,
`LOSAL` double DEFAULT NULL,
`HISAL` double DEFAULT NULL,
PRIMARY KEY (`GRADE`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `salgrade` VALUES ('1', '700', '1200');
INSERT INTO `salgrade` VALUES ('2', '1201', '1400');
INSERT INTO `salgrade` VALUES ('3', '1401', '2000');
INSERT INTO `salgrade` VALUES ('4', '2001', '3000');
INSERT INTO `salgrade` VALUES ('5', '3001', '9999');
DROP TABLE IF EXISTS `t_job`;
CREATE TABLE `t_job` (
`id` int NOT NULL AUTO_INCREMENT,
`job` varchar(9) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `j` (`job`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.1 id
select查詢的序列號矫付,包含一組數(shù)字,表示查詢中執(zhí)行select子句或者操作表的順序
id號分為三種情況:
? 1第焰、如果id相同买优,那么執(zhí)行順序從上到下
-- 左關(guān)聯(lián)
explain select * from emp e left join dept d on e.deptno = d.deptno;
-- 右關(guān)聯(lián)
explain select * from emp e right join dept d on e.deptno = d.deptno;
通過left join 和 right join 驗證;id一樣(注意執(zhí)行計劃的table列)挺举,left join 先掃描e表杀赢,再掃描d表;right join 先掃描d表湘纵,再掃描e表
? 2脂崔、如果id不同,如果是子查詢瞻佛,id的序號會遞增脱篙,id值越大優(yōu)先級越高,越先被執(zhí)行
explain select * from emp e where e.deptno = (select d.deptno from dept d where d.dname = 'SALES');
在下面的表中回先查詢 id 為 2的數(shù)據(jù) 也就是我們的 d表(注意:我們可以看到d表中select_type為 SUBQUERY 也就是子查詢的 意思)伤柄,然后根據(jù)d表中的deptno去查詢 e表中的數(shù)據(jù)
? 3绊困、id相同和不同的,同時存在:相同的可以認(rèn)為是一組适刀,從上往下順序執(zhí)行秤朗,在所有組中,id值越大笔喉,優(yōu)先級越高取视,越先執(zhí)行
explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal where e.deptno = (select d.deptno from dept d where d.dname = 'SALES');
在這里先從id為2的執(zhí)行硝皂,如果id是一樣的,就按照順序執(zhí)行
2.2 select_type
主要用來分辨查詢的類型作谭,是普通查詢還是聯(lián)合查詢還是子查詢
select_type 值 | 含義(Meaning) |
---|---|
SIMPLE | 簡單的查詢不包含 UNION 和 subqueries |
PRIMARY | 查詢中若包含任何復(fù)雜的子查詢稽物,最外層查詢則被標(biāo)記為Primary |
UNION | 若第二個select出現(xiàn)在union之后,則被標(biāo)記為union |
DEPENDENT UNION | 跟union類似折欠,此處的depentent表示union或union all聯(lián)合而成的結(jié)果會受外部表影響 |
UNION RESULT | 從union表獲取結(jié)果的select |
SUBQUERY | 在select或者where列表中包含子查詢 |
DEPENDENT SUBQUERY | subquery的子查詢要受到外部表查詢的影響 |
DERIVED | from子句中出現(xiàn)的子查詢贝或,也叫做派生類 |
UNCACHEABLE SUBQUERY | 表示使用子查詢的結(jié)果不能被緩存 |
UNCACHEABLE UNION | 表示union的查詢結(jié)果不能被緩存 |
--simple:簡單的查詢,不包含子查詢和union
explain select * from emp;
--primary:查詢中若包含任何復(fù)雜的子查詢锐秦,最外層查詢則被標(biāo)記為Primary
explain select * from emp e where e.deptno = (select d.deptno from dept d where d.dname = 'SALES');
--union:若第二個select出現(xiàn)在union之后咪奖,則被標(biāo)記為union
explain select * from emp where deptno = 10 union select * from emp where sal >2000;
--dependent union:跟union類似,此處的depentent表示union或union all聯(lián)合而成的結(jié)果會受外部表影響
explain select * from emp e where e.empno in ( select empno from emp where deptno = 10 union select empno from emp where sal >2000);
--union result:從union表獲取結(jié)果的select
explain select * from emp where deptno = 10 union select * from emp where sal >2000;
--subquery:在select或者where列表中包含子查詢
explain select * from emp where sal > (select avg(sal) from emp) ;
--dependent subquery:subquery的子查詢要受到外部表查詢的影響
explain select * from emp e where e.deptno = (select distinct deptno from dept where deptno = e.deptno);
--DERIVED: from子句中出現(xiàn)的子查詢酱床,也叫做派生類羊赵,
explain select * from (select ename staname,mgr from emp where ename = 'W' union select ename,mgr from emp where ename = 'E') a;
-- UNCACHEABLE SUBQUERY:表示使用子查詢的結(jié)果不能被緩存
explain select * from emp where empno = (select empno from emp where deptno=@@sort_buffer_size);
--uncacheable union:表示union的查詢結(jié)果不能被緩存
explain select * from emp where exists (select 1 from dept where emp.deptno = dept.deptno union select 1 from dept where deptno = 10);
2.3 table
對應(yīng)行正在訪問哪一個表,表名或者別名扇谣,可能是臨時表或者union合并結(jié)果集
1昧捷、如果是具體的表名,則表明從實(shí)際的物理表中獲取數(shù)據(jù)揍堕,也可以是表的別名
explain select * from emp where sal > (select avg(sal) from emp) ;
? 2料身、表名是derivedN的形式,表示使用了id為N的查詢產(chǎn)生的衍生表
explain select * from (select ename staname,mgr from emp where ename = 'WARD' union select ename staname,mgr from emp where ename = 'SMITH') a;
derived2 : 表明我們需要從衍生表2中取數(shù)據(jù)
? 3衩茸、當(dāng)有union result的時候芹血,表名是union n1,n2等的形式,n1,n2表示參與union的id
explain select * from emp where deptno = 10 union select * from emp where sal >2000;
2.4 type
type顯示的是訪問類型楞慈,訪問類型表示我是以何種方式去訪問我們的數(shù)據(jù)幔烛,最容易想的是全表掃描,直接暴力的遍歷一張表去尋找需要的數(shù)據(jù)囊蓝,效率非常低下饿悬,訪問的類型有很多,效率從最好到最壞依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般情況下聚霜,得保證查詢至少達(dá)到range級別狡恬,最好能達(dá)到ref
--all:全表掃描,一般情況下出現(xiàn)這樣的sql語句而且數(shù)據(jù)量比較大的話那么就需要進(jìn)行優(yōu)化蝎宇。
explain select * from emp;
--index:全索引掃描這個比all的效率要好弟劲,主要有兩種情況,一種是當(dāng)前的查詢時覆蓋索引姥芥,即我們需要的數(shù)據(jù)在索引中就可以索取兔乞,或者是使用了索引進(jìn)行排序,這樣就避免數(shù)據(jù)的重排序
explain select empno from emp;
--range:表示利用索引查詢的時候限制了范圍,在指定范圍內(nèi)進(jìn)行查詢庸追,這樣避免了index的全索引掃描霍骄,適用的操作符: =, <>, >, >=, <, <=, IS NULL, BETWEEN, LIKE, or IN()
explain select * from emp where empno between 7000 and 7500;
--index_subquery:利用索引來關(guān)聯(lián)子查詢,不再掃描全表
explain select * from emp where emp.job in (select job from t_job);
--unique_subquery:該連接類型類似與index_subquery,使用的是唯一索引
explain select * from emp e where e.deptno in (select distinct deptno from dept);
--ref_or_null:對于某個字段即需要關(guān)聯(lián)條件淡溯,也需要null值的情況下读整,查詢優(yōu)化器會選擇這種訪問方式
explain select * from emp e where e.mgr is null or e.mgr=7369;
--ref:使用了非唯一性索引進(jìn)行數(shù)據(jù)的查找
create index idx_3 on emp(deptno);
explain select * from emp e,dept d where e.deptno =d.deptno;
--eq_ref :使用唯一性索引進(jìn)行數(shù)據(jù)查找
explain select * from emp,emp2 where emp.empno = emp2.empno;
--const:這個表至多有一個匹配行,
explain select * from emp where empno = 7369;
--system:表只有一行記錄(等于系統(tǒng)表)咱娶,這是const類型的特例绘沉,平時不會出現(xiàn)
2.5 possible_keys
? 顯示可能應(yīng)用在這張表中的索引,一個或多個豺总,查詢涉及到的字段上若存在索引,則該索引將被列出择懂,但不一定被查詢實(shí)際使用
explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;
2.6 key
? 實(shí)際使用的索引喻喳,如果為null,則沒有使用索引困曙,查詢中若使用了覆蓋索引表伦,則該索引和查詢的select字段重疊。
explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;
2.7 key_len
表示索引中使用的字節(jié)數(shù)慷丽,可以通過key_len計算查詢中使用的索引長度蹦哼,在不損失精度的情況下長度越短越好。
1要糊、一般地纲熏,key_len 等于索引列類型字節(jié)長度,例如int類型為4 bytes锄俄,bigint為8 bytes局劲;
2、如果是字符串類型奶赠,還需要同時考慮字符集因素鱼填,例如utf8字符集1個字符占3個字節(jié),gbk字符集1個字符占2個字節(jié)
3毅戈、若該列類型定義時允許NULL苹丸,其key_len還需要再加 1 bytes
4、若該列類型為變長類型苇经,例如 VARCHAR(TEXT\BLOB不允許整列創(chuàng)建索引赘理,如果創(chuàng)建部分索引也被視為動態(tài)列類型),其key_len還需要再加 2 bytes
字符集會影響索引長度塑陵、數(shù)據(jù)的存儲空間感憾,為列選擇合適的字符集;變長字段需要額外的2個字節(jié),固定長度字段不需要額外的字節(jié)阻桅。而null都需要1個字節(jié)的額外空間凉倚,所以以前有個說法:索引字段最好不要為NULL,因為NULL讓統(tǒng)計更加復(fù)雜嫂沉,并且需要額外一個字節(jié)的存儲空間稽寒。
-- key_len的長度計算公式:
-- varchar(len)變長字段且允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(變長字段)
-- varchar(len)變長字段且不允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)+2(變長字段)
-- char(len)固定字段且允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)
-- char(len)固定字段且不允許NULL : len*(Character Set:utf8=3,gbk=2,latin1=1)
explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;
2.8 ref
顯示索引的哪一列被使用了,如果可能的話趟章,是一個常數(shù)
explain select * from emp,dept where emp.deptno = dept.deptno and emp.deptno = 10;
2.9 rows
根據(jù)表的統(tǒng)計信息及索引使用情況杏糙,大致估算出找出所需記錄需要讀取的行數(shù),此參數(shù)很重要蚓土,直接反應(yīng)的sql找了多少數(shù)據(jù)宏侍,在完成目的的情況下越少越好
explain select * from emp;
2.10 extra
包含額外的信息。
--using filesort:說明mysql無法利用索引進(jìn)行排序蜀漆,只能利用排序算法進(jìn)行排序谅河,會消耗額外的位置
explain select * from emp order by sal;
--using temporary:建立臨時表來保存中間結(jié)果,查詢完成之后把臨時表刪除
explain select ename,count(*) from emp where deptno = 10 group by ename;
--using index:這個表示當(dāng)前的查詢時覆蓋索引的确丢,直接從索引中讀取數(shù)據(jù)绷耍,而不用訪問數(shù)據(jù)表。如果同時出現(xiàn)using where 表名索引被用來執(zhí)行索引鍵值的查找鲜侥,如果沒有褂始,表面索引被用來讀取數(shù)據(jù),而不是真的查找
explain select deptno,count(*) from emp group by deptno limit 10;
--using where:使用where進(jìn)行條件過濾
explain select * from emp2 where empno = 1;
三 總結(jié)
到這里執(zhí)行計劃就講完了描函,sql的執(zhí)行計劃并不是很難崎苗,主要是記住每個列代表的意思和如何進(jìn)行優(yōu)化,這個是需要大量的訓(xùn)練和實(shí)操實(shí)現(xiàn)的舀寓, 有興趣的小伙伴可以自行去試試益缠,還是很有趣的,本文只是簡單介紹一下MySQL執(zhí)行計劃基公,想全面深入了解MySQL,可優(yōu)先閱讀MySQL官方手冊幅慌,大家加油~