MySQL优化从执行计划开始(explain超详细)
前言
小伙伴一定遇到過這樣反饋:這頁面加載數(shù)據(jù)太慢啦,甚至有的超時了,用戶體驗極差,需要趕緊優(yōu)化;
反饋等同于投訴啊,多有幾次,估計領(lǐng)導(dǎo)要找你談話啦。
于是不得不停下手里頭的活,趕緊進(jìn)行排查,最終可能是程序處理的問題、也可能是并發(fā)量大導(dǎo)致排隊問題、也可能是SQL查詢性能導(dǎo)致等;而在很多時候,SQL查詢緩慢是最直接拖慢系統(tǒng)的罪魁禍?zhǔn)?#xff0c;同樣是實現(xiàn)一個功能,有的小伙伴毫秒級呈現(xiàn)效果,有的卻要好幾秒,而調(diào)優(yōu)需要的花費時間不容小覷,最終可能就體現(xiàn)到個人業(yè)務(wù)能力上和形象上:哇,真牛逼,分分鐘搞定;菜鳥,居然寫出這樣的SQL;
而對于SQL調(diào)優(yōu),搜索引擎一查,72般絕技絕對夠秀,于是照著開始實操,運氣好一下就解決啦,運氣差的時候怎么用都不行;所以更重要的是業(yè)務(wù)場景,要學(xué)會分析原因,最后才知道用什么方式解決;而這個系列就來聊聊數(shù)據(jù)庫優(yōu)化,聊聊原因,聊聊方法。
1. MySQL邏輯結(jié)構(gòu)先知
關(guān)于MySQL的邏輯結(jié)構(gòu),將其理解為四層,就像項目分層一樣,每一層處理不同的業(yè)務(wù)邏輯,先看圖后說話:
image-20210313155849571上圖概述:
客戶端:這里指連接MySQL各種形式,如.Net中使用的ADO連接、Java使用JDBC連接等;MySQL是客戶端和服務(wù)器模式,前提先建立連接,才能傳輸數(shù)據(jù),處理相關(guān)邏輯;
業(yè)務(wù)邏輯:在MySQL內(nèi)部有很多模塊組成,分別處理相關(guān)業(yè)務(wù)邏輯;
連接管理:負(fù)責(zé)連接認(rèn)證、連接數(shù)判斷、連接池處理等業(yè)務(wù)邏輯處理;
查詢緩存:當(dāng)一個SQL進(jìn)來時,如果開啟查詢緩存功能,MySQL會優(yōu)先去查詢緩存中檢查是否有數(shù)據(jù)匹配,如果匹配上,就不會再去解析對應(yīng)的SQL啦,但如果語句中有用戶自定義函數(shù)、存儲函數(shù)、用戶變量、臨時表、mysql庫中的系統(tǒng)表時,都不會走緩存;對于查詢緩存來說,在MySQL8.0已經(jīng)去除,官方回應(yīng)的是在一定場景上,查詢緩存會導(dǎo)致性能上的瓶頸。
解析器:對于一個SQL語句,MySql根據(jù)語法規(guī)則需要對其進(jìn)行解析,并生成一個內(nèi)部能識別的解析樹;
優(yōu)化器:負(fù)責(zé)對解析器得到的解析樹進(jìn)行優(yōu)化,MySQL會根據(jù)內(nèi)部算法找到一個MySQL認(rèn)為最優(yōu)的執(zhí)行計劃,后續(xù)就按照這個執(zhí)行計劃執(zhí)行。所以后續(xù)我們分析的就是MySQL針對SQL語句選擇出來的最優(yōu)執(zhí)行計劃,結(jié)合業(yè)務(wù),根據(jù)規(guī)則對SQL進(jìn)行優(yōu)化,從而讓SQL語句在MySQL內(nèi)部達(dá)到真正的最優(yōu)。
執(zhí)行器:得到執(zhí)行計劃之后,就會找到對應(yīng)的存儲引擎,根據(jù)執(zhí)行計劃給出的指令依次執(zhí)行。
存儲引擎:數(shù)據(jù)的存儲和提取最后是靠存儲引擎;MySQL內(nèi)部實現(xiàn)可插拔式的存儲引擎機(jī)制,不同的存儲引擎執(zhí)行不同的邏輯;
物理文件:數(shù)據(jù)存儲的最終位置,即磁盤上;協(xié)同存儲引擎對數(shù)據(jù)進(jìn)行讀寫操作。
關(guān)于MySql的邏輯結(jié)構(gòu),以上只是簡單描述,業(yè)務(wù)邏輯層的功能模塊遠(yuǎn)不止上面提到的,小伙伴有興趣可以專門研究一下,這里的目的就是為了體現(xiàn)SQL語句到服務(wù)器上時經(jīng)過的幾個關(guān)鍵步驟,方便后續(xù)優(yōu)化的理解。
2. SQL語句的中關(guān)鍵字執(zhí)行順序須知
在編寫一條查詢語句時,習(xí)慣性的從頭到尾開始敲出來,應(yīng)該都是從select 開始吧,但似乎沒太注意它們真正的執(zhí)行順序;既然要優(yōu)化,肯定需要得知道一條SQL語句大概的執(zhí)行流程,結(jié)合執(zhí)行計劃,目的就更加清晰啦;上一張一看就明白的圖:
image-20210313223002285關(guān)鍵字簡述:
FROM:確定數(shù)據(jù)來源,即指定表;
JOIN…ON:確定關(guān)聯(lián)表和關(guān)聯(lián)條件;
WHERE:指定過濾條件,過濾出滿足條件的數(shù)據(jù);
GROUP BY:按指定的字段對過濾后的數(shù)據(jù)進(jìn)行分組;
HAVING:對分組之后的數(shù)據(jù)指定過濾條件;
SELECT:查找想要的字段數(shù)據(jù);
DISTINCT:針對查找出來的數(shù)據(jù)進(jìn)行去重;
ORDER BY:對去重后的數(shù)據(jù)指定字段進(jìn)行排序;
LIMIT:對去重后的數(shù)據(jù)限制獲取到的條數(shù),即分頁;
好啦,大概了解MySQL的邏輯結(jié)構(gòu)和SQL查詢關(guān)鍵字執(zhí)行順序之后,接下來就可以好好說說執(zhí)行計劃啦。
3. 好好說說執(zhí)行計劃
通過上面的邏輯結(jié)構(gòu),當(dāng)一個SQL發(fā)送到MySQL執(zhí)行時,需要經(jīng)過內(nèi)部優(yōu)化器進(jìn)行優(yōu)化,而使用explain關(guān)鍵字可以模擬優(yōu)化器執(zhí)行SQL查詢語句,從而知道MySQL是如何處理SQL的,即SQL的執(zhí)行計劃;根據(jù)explain提供的執(zhí)行計劃信息分析SQL語句,然后進(jìn)行相關(guān)優(yōu)化操作。接下來的示例演示用到五張表:USER(用戶表)、MENU(菜單表)、ROLE(角色表)、USER_ROLE(用戶角色關(guān)系表)、ROLE_MENU(角色菜單關(guān)系表)、ADDR(用戶地址表,這里認(rèn)為和用戶一一對應(yīng))、FRIEND(朋友表,一對多關(guān)系),它們的關(guān)系這里就不詳細(xì)說了吧,小伙伴肯定都明白,這是管控菜單權(quán)限的五張基礎(chǔ)表和兩個基礎(chǔ)信息表;
演示用的版本是MySql5.5,各版本之間會有不同,所以小伙伴用的版本測試結(jié)果不一樣的時候,千萬別罵我渣哦;其實重要的是查看的思路,整體是大同小異。(求原諒……)
通過explain會輸出如下信息,很多小伙伴只關(guān)注紅框標(biāo)注部分(即索引),但其實是不夠的,接下來就一個一個好好說說。
image-20210314190432145id
這個id和咱們平時表結(jié)構(gòu)設(shè)計的主鍵ID不太一樣,這里的id代表了每一條SQL語句執(zhí)行計劃中表加載的順序,分為三種情況:
id相同的時候:這時是從上到下依次執(zhí)行;
EXPLAIN SELECT t.ID,t.USER_NAME,r.ROLE_NAME FROM USER t JOIN USER_ROLE tr ON t.ID = tr.USER_IDJOIN ROLE r ON tr.ROLE_ID = r.ID執(zhí)行如下語句,得如下結(jié)果:
image-20210315000741898如上圖所示,id一樣,從上到下依次執(zhí)行,所對應(yīng)表加載順序為t->tr->r(這里的表是別名);
id不同的時候:當(dāng)id不同的時,id越大的越先執(zhí)行;
EXPLAIN SELECT t.ID,t.MENU_NAME,t.MENU_URL FROM MENU tWHERE t.ID IN (SELECT MENU_ID FROM ROLE_MENU rm WHERE rm.ROLE_ID IN(SELECT ROLE_ID FROM USER_ROLE ur WHERE ur.USER_ID=1))子查詢會導(dǎo)致id遞增,結(jié)果如下:
image-20210315002147586如上圖所示,id遞增啦,所對應(yīng)表的加載順序為ur->rm->t(這里的表是別名);
id相同和不同同時存在時:id相同的認(rèn)為是同一組,還是從上往下加載;不一樣的情況還是越大越優(yōu)先執(zhí)行
?EXPLAIN?SELECT?t.ROLE_ID,m.ID,m.MENU_NAME,m.MENU_URL?FROM?(SELECT ROLE_ID FROM USER_ROLE WHERE USER_ID=3) t,ROLE_MENU rm,MENU mWHERE t.ROLE_ID=rm.ROLE_IDAND rm.MENU_ID=m.ID執(zhí)行結(jié)果如下:
image-20210315004001664如上圖所示,id有一樣的,也有不同的,則對應(yīng)表的加載順序為USER_ROLE->derived2 (衍生表)->rm->m;衍生表表名后面的2代表的是id,所以可以通過衍生表表名后面的id知道是哪一步產(chǎn)生的,即derived2衍生表是id為2的這一步產(chǎn)生的。
select_type
select_type 是表示每一步的查詢類型,方便分析人員很直接的看到當(dāng)前步驟執(zhí)行的是什么查詢,有多種類型,見下圖:
1> ?SIMPLE:簡單的SELECT查詢,不包含子查詢或UNION的那種;
EXPLAIN SELECT * FROM USER;輸出結(jié)果如下:
image-202103151245411982> ?PRIMARY:查詢語句中包含其他子查詢或UNION操作,那最外層的SELECT就被標(biāo)記為該類型;
image-20210315124706414如上圖所示,查詢中包含子查詢,最外層查詢被標(biāo)記為PRIMARY;
3> ?SUBQUERY:在SELECT或WHERE中包含的子查詢會被標(biāo)記為該類型;
見PRIMARY圖,當(dāng)存在子查詢時,會將子查詢標(biāo)記為SUBQUERY
4> ?MATERIALIZED:被物化的子查詢,即針對對應(yīng)的子查詢將其物化為一個臨時表;
EXPLAIN?SELECT?t.ID,t.MENU_NAME,t.MENU_URL?FROM?MENU?tWHERE t.ID IN (SELECT MENU_ID FROM ROLE_MENU rm WHERE rm.ROLE_ID IN(SELECT ROLE_ID FROM USER_ROLE ur WHERE ur.USER_ID=1));測試物化用的是MySQL8.0,和5.*版本有所不同,輸出結(jié)果如下:
image-20210315125116200如上圖所示,將子查詢物化為一個臨時表subquery2,這個功能是可以通過設(shè)置優(yōu)化器對應(yīng)的開關(guān)的。
5> ?DERIVED:在FROM之后的子查詢會被標(biāo)記為該類型,同樣會把結(jié)果放在一個臨時表中;
EXPLAIN SELECT tm.MENU_NAME,rm.ROLE_ID FROM (SELECT * FROM MENU WHERE ID >3 ) tm ,ROLE_MENU rm WHERE tm.ID=rm.MENU_ID AND rm.ROLE_ID=1輸出結(jié)果:
image-20210315205026760如圖所示,FROM后面跟的子查詢就被標(biāo)記為DERIVED,對應(yīng)步驟產(chǎn)生的衍生表為derived2。高版本好像對其進(jìn)行了優(yōu)化,8.0版本這種形式認(rèn)為是簡單查詢。
6> ?UNION:UNION操作中,查詢中處于內(nèi)層的SELECT;
EXPLAIN SELECT * FROM USER_ROLE T1 WHERE T1.USER_ID=1UNIONSELECT * FROM USER_ROLE T2 WHERE T2.USER_ID=2輸出結(jié)果如下:
image-20210315133138810如上圖所示,將第二個SELECT標(biāo)注為UNION ,即對應(yīng)加載的表為T2。
7> ?UNIOIN RESULT:UNION操作的結(jié)果,對應(yīng)的id為空,代表的是一個結(jié)果集;
見UNIOIN圖,UNIOIN RESULT代表的是UNION之后的結(jié)果,對應(yīng)id為空。
table
table代表對應(yīng)步驟加載的是哪張表,中間會出現(xiàn)一些臨時表,比如subquery2、derived2等這種,最后的數(shù)字代表產(chǎn)生該表對應(yīng)步驟的id。
type
代表訪問類型,MySQL內(nèi)部將其分為多類型,常用的類型從好到差的順序展示如下:
system->const->eq_ef->ref->fulltext->ref_or_null->index_merge->unique_subquery->index_subquery->range->index->ALL;
而在實際開發(fā)場景中,比較常見的幾種類型如下:const->eq_ref->ref->range->index->ALL(順序從好到差),通常優(yōu)化至少在range級別或以上,比如ref算是比較不錯的啦;
上面說到的從好到差指的是查詢性能。
1>const:表示通過索引一次就找到數(shù)據(jù),用于比較primary key或者unique索引,很快就能找到對應(yīng)的數(shù)據(jù);
image-202103152133488122>eq_ref:唯一性索引掃描,對于每個索引鍵,表中只有一條記錄與之匹配,常用于主鍵或唯一索引掃描;
image-202103152152463613>ref:非唯一索引掃描,返回匹配的所有行,如建立一個朋友維護(hù)表,維護(hù)用戶對應(yīng)的朋友,而在用戶ID建立非唯一索引;
image-202103152205445064>range:使用一個索引檢索指定范圍的行,一般在where語句中會出現(xiàn)between、<、>、in等范圍查詢;
image-202103152218388255>index:全索引掃描,只遍歷索引樹;
image-202103152223120906>ALL:全表掃描,找到匹配行。與index比較,ALL需要掃描磁盤數(shù)據(jù),index值需要遍歷索引樹。
image-20210315222815614possible_keys
顯示可能被用到的索引,但在實際查詢中不一定能用到;查詢涉及到字段,如果存在索引,會被列出,但如果使用的是覆蓋索引,只會在key中列出;
image-20210315223744943key
實際使用到的索引,如果為NULL代表沒有使用到索引;這也是平時小伙伴判斷是否用上索引的關(guān)鍵。
key_len
key_len表示索引使用的字節(jié)數(shù),根據(jù)這個值可以判斷索引的使用情況,特別是在組合索引的時候,判斷該索引有多少部分被使用到,非常重要;key_len是根據(jù)表定義計算而得。這里測試在USER表中對USER_NAME創(chuàng)建一個非唯一索引,如下:
image-20210316001057900這里key_len是這么計算的,前提是指定的字符串集是utf8,可變長 且允許為空,計算過程如下:
128(設(shè)置的可變長度)*3(utf8占3字節(jié))+1(允許為空標(biāo)識占一個字節(jié))+2(長度信息占兩個字節(jié))=387;
key_len針對不同類型字段的計算規(guī)則不一樣,這里用USER(用戶表)簡單計算為例:
字段Key_len說明 ID(int,不為空) 4 int為4個字節(jié),不為空 USER_NAME(varchar(128),utf8,可為空) 128*3+1+2=387 可變?yōu)?28,utf8每個占3字節(jié),1個字節(jié)標(biāo)識可控,兩個字節(jié)標(biāo)識長度 不同類型占用的字節(jié)不一樣,字符集不一樣占用的字節(jié)也不一樣,允許為空的字段需要1個字節(jié)做標(biāo)識,可變長度的字段需要2個字節(jié)標(biāo)識長度。小伙伴照著這個思路就可以計算其他類型啦。
ref
顯示索引的哪些列被引用了,通常是對應(yīng)字段或const;
image-20210316003104264image-20210316003227737rows
根據(jù)表統(tǒng)計信息和索引的使用情況,大概估算出找到所需記錄數(shù)據(jù)所掃描的數(shù)據(jù)行數(shù);不是所需數(shù)據(jù)的行數(shù)。
Extra
這個字段里包含一些其他信息,但也是優(yōu)化SQL的重要參考,通常會出現(xiàn)以下幾種信息:
Using index:表示查詢語句中用到了覆蓋索引,不訪問表的數(shù)據(jù)行,查詢效率比較好。
image-20210316092530361如果用SELECT *進(jìn)行查詢,就不會有Using index,關(guān)于索引的介紹下篇好好說說。
Using filesort:代表MySQL會使用一個外部索引對數(shù)據(jù)進(jìn)行排序(文件排序),而不是使用表內(nèi)索引。這種情況在SQL查詢需要避免,最好不要在Extra中出現(xiàn)此類型:
image-20210316093336121通常會是使用ORDER BY語句導(dǎo)致,上圖中使用無索引的字段進(jìn)行排序會出現(xiàn),同樣如果使用有索引的字段,但用法不對也會出現(xiàn),比如使用組合索引不規(guī)范時。
Using temporary:產(chǎn)生臨時表保存中間結(jié)果,這種SQL是不允許的,遇見數(shù)據(jù)量大的場景,基本就跑不動啦;
image-20210316094004777這種類型常常因為ORDER BY 和 GROUP BY導(dǎo)致,所以在進(jìn)行數(shù)據(jù)排序和分組查詢時,要注意索引的合理利用。
Using where:使用where過濾數(shù)據(jù),小伙伴試一把。
Using join buffer:表示使用到了表連接緩存;當(dāng)表數(shù)據(jù)量大,可能導(dǎo)致buffer過大,查詢效率比較低,這種情況注意在表連接字段上正確使用索引。
image-20210316101458370如果表連接查詢慢時,在連接字段上加個索引試試,藥到病除;
impossible where:代表where后面的條件永遠(yuǎn)為false,匹配不到數(shù)據(jù);
image-20210316095219041用到的表及數(shù)據(jù)從Gitgub中獲取:https://github.com/zyq025/SQL_Optimize
總結(jié)
看完這篇文章之后,小伙伴再去找些SQL看看對應(yīng)的執(zhí)行計劃,是不是看懂啦,對于優(yōu)化意義非凡;但是這還不夠,接下來還要聊聊索引,聊聊索引失效情況,聊聊除了EXPALIN其他優(yōu)化方式等,最后日常的開發(fā)優(yōu)化應(yīng)該都能搞定,遠(yuǎn)離低效SQL,是不是又有更多時間學(xué)習(xí)啦。
一個被程序搞丑的帥小伙,關(guān)注"Code綜藝圈",跟我一起學(xué)~~~
圖片總結(jié)
以上是生活随笔為你收集整理的MySQL优化从执行计划开始(explain超详细)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【翻译】WPF 中附加行为的介绍 Int
- 下一篇: 应用程序自动更新组件GeneralUpd