作者:孤獨(dú)煙,資深后端工程師,業(yè)內(nèi)知名原創(chuàng)作者
一條查詢SQL執(zhí)行流程圖如下
本文改編自《高性能Mysql》,煙哥用小說(shuō)的形式來(lái)講這個(gè)內(nèi)容。
序章 自我介紹
我是一條sql,就是一條長(zhǎng)長(zhǎng)的字符串,不要問(wèn)我長(zhǎng)什么樣,因?yàn)槲冶容^傲嬌。
額~~不是我不說(shuō)啊,因?yàn)榧?xì)說(shuō)起來(lái),我可以細(xì)分為
DML
(Update、Insert、Delete),
DDL
(表結(jié)構(gòu)修改),
DCL
(權(quán)限操作),
DQL
(Select)操作,一個(gè)個(gè)去介紹,我怕大家嫌我煩!
嗯,大家沒(méi)什么意見(jiàn),我繼續(xù)往下自我介紹了~
由于種類太多,這里我只是一條查詢SQL,也就是一句DQL。
客戶端按照Mysql通信協(xié)議,把我發(fā)送到服務(wù)端。
當(dāng)我到達(dá)服務(wù)端后,我會(huì)在一個(gè)單獨(dú)的
線程
里進(jìn)行執(zhí)行。服務(wù)端要先…
萬(wàn)萬(wàn)沒(méi)想到,我又被打斷了~好吧,因?yàn)槲以谝粋€(gè)線程里執(zhí)行,總要有辦法能看到線程的執(zhí)行狀態(tài)吧。Mysql提供了下面的命令,給大家查看
SHOW [FULL] PROCESSLIST
出來(lái)的結(jié)果是長(zhǎng)下面這樣的
圖里
Command
這一列,反應(yīng)的就是這個(gè)線程當(dāng)前的執(zhí)行狀態(tài)啦。我在這個(gè)線程的執(zhí)行過(guò)程中,狀態(tài)是會(huì)變化很多次。
你看圖里,有一個(gè)
Sleep
,這是在告訴你線程正在等待客戶端發(fā)送新的請(qǐng)求。還有一個(gè)為
Query
,這代表線程正在執(zhí)行查詢或者正在將結(jié)果發(fā)送給客戶端。
至于其他的,還有
Locked
Sending data
等等,分別代表…
額,好吧,嘮嘮叨叨了一大堆,大家居然木有嫌我煩,嗯,至于其他狀態(tài)的含義大家可以去Mysql官網(wǎng)查詢哦。
嗯,回到剛才的話題。我到達(dá)服務(wù)端后,Mysql要判斷我的前6個(gè)字符是否為
select
。并且,語(yǔ)句中不帶有
SQL_NO_CACHE
關(guān)鍵字,如果符合條件,就進(jìn)入查詢緩存。
第一章 我和查詢緩存的那些事
說(shuō)到查詢緩存,它其實(shí)是一個(gè)哈希表,它將執(zhí)行過(guò)的語(yǔ)句及其結(jié)果會(huì)以 key-value 對(duì)的形式,被直接緩存在內(nèi)存中。
它的key是一個(gè)哈希值,是通過(guò)查詢SQL(也就是我)、當(dāng)前要查詢的數(shù)據(jù)庫(kù)、客戶端協(xié)議版本等,生成的一個(gè)哈希值,而它的value自然就是查詢結(jié)果啦。
當(dāng)然,如果我要繞過(guò)查詢緩存,也很簡(jiǎn)單。我可以像下面這么寫:
Select SQL_NO_CACHE * from table
也可以將參數(shù)query_cache_type設(shè)置成DEMAND來(lái)繞過(guò)查詢緩存。
可是,有一天查詢緩存悲傷的對(duì)我說(shuō):"你將來(lái)再也看不到我了,我已經(jīng)被歷史淘汰了,Mysql8.0版本開(kāi)始就沒(méi)有我了!"
聽(tīng)到這個(gè)消息后,我表面上故作堅(jiān)強(qiáng)的對(duì)查詢緩存說(shuō):"不要方,大家會(huì)想你的!"
然而,實(shí)際上心里想的是:"嘿嘿嘿,你個(gè)坑爹的,終于不存在了!"大家不要覺(jué)得我太邪惡,畢竟查詢緩存實(shí)在是太不好用了。接下來(lái)我們來(lái)說(shuō)說(shuō)解析器…
萬(wàn)萬(wàn)沒(méi)想到,本來(lái)想糊弄過(guò)去的。結(jié)果…好吧,回到正題,因?yàn)?/p>
- 只要有對(duì)一個(gè)表的更新,這個(gè)表上所有的查詢緩存都會(huì)被清空
- SQL任何字符上的不同,如空格,注釋,都會(huì)導(dǎo)致緩存不命中
因此,我能想到用查詢緩存的表,只有一種情況,那就是配置表。其他的業(yè)務(wù)表,根本是無(wú)法利用查詢緩存的特性,或許Mysql團(tuán)隊(duì)也是覺(jué)得查詢緩存的使用場(chǎng)景過(guò)于局限,就無(wú)情的將它剔除。
第二章 我和分析器的愛(ài)恨情仇
(本文將解析器和預(yù)處理器統(tǒng)一稱為分析器)
話說(shuō),我離開(kāi)查詢緩存后,進(jìn)入解析器。
解析器:"來(lái)來(lái)來(lái),我先對(duì)你進(jìn)行詞法分析,告訴我你長(zhǎng)啥樣?"
我是下面這樣的
select username from userinfo
解析器:"好,好,好。我有兩個(gè)階段,我先對(duì)你進(jìn)行詞法分析,我將你從左到右一個(gè)字符、一個(gè)字符地輸入,然后根據(jù)構(gòu)詞規(guī)則識(shí)別單詞。你將會(huì)生成4個(gè)Token,如下所示。"
解析器:"接下來(lái)呢,進(jìn)行語(yǔ)法解析,判斷你輸入的這個(gè) SQL 語(yǔ)句是否滿足 MySQL 語(yǔ)法。然后生成下面這樣一顆語(yǔ)法樹(shù)。"
我:"如果語(yǔ)法不對(duì)呢?"
解析器:"那你會(huì)收到一個(gè)提示如下!"
You have an error in your SQL syntax
解析器:"順利生成語(yǔ)法樹(shù)以后,我就將你送往預(yù)處理器!"
預(yù)處理器:"老弟,你來(lái)拉!"
我:"嗯!"
預(yù)處理器:"老弟,我來(lái)幫你看看你的列名對(duì)不對(duì),數(shù)據(jù)庫(kù)的這張表里是不是真的有這個(gè)列。再看看表名對(duì)不對(duì),如果不對(duì),你會(huì)看到下面的錯(cuò)誤!"
Unknown column xxx in ‘where clause’
預(yù)處理器:"最后我再給你送去做權(quán)限驗(yàn)證,如果你沒(méi)有操作這個(gè)表的權(quán)限,會(huì)報(bào)下面這個(gè)錯(cuò)誤!"
ERROR 1142 (42000): SELECT command denied to user 'root'@'localhost' for table 'xxx'
(這個(gè)地方,大家可能有疑問(wèn),因?yàn)橛行┪恼抡f(shuō)是執(zhí)行器做的權(quán)限驗(yàn)證,可以直接拉到本文底部看說(shuō)明)
最后,這顆語(yǔ)法樹(shù)會(huì)傳遞給優(yōu)化器。
第三章 我和優(yōu)化器的動(dòng)人過(guò)往
在告別了解析器后,我進(jìn)入了優(yōu)化器。
優(yōu)化器大哥:"告訴我,你長(zhǎng)什么樣啊?"
我說(shuō)道:"大哥不要捉急,我是長(zhǎng)這樣的~"(這里優(yōu)化的其實(shí)應(yīng)該是語(yǔ)法樹(shù),我只是為了便于說(shuō)明,才用SQL當(dāng)例子,實(shí)際上是針對(duì)語(yǔ)法樹(shù)進(jìn)行優(yōu)化)
select t1.*
from Table1 t1
inner join Table2 t2
on t1.CommonID = t2.CommonID
優(yōu)化器大哥:"我的任務(wù)就是幫你判斷一下怎么樣執(zhí)行更快,比如先查Table1再查Table2,還是先查Table2再查Table1呢?判斷完如何執(zhí)行以后,生成執(zhí)行計(jì)劃就好啦!"
我很不信任的說(shuō)道:“哼,你就不會(huì)判斷失誤么!”
優(yōu)化器大哥:"那就要對(duì)SQL進(jìn)行改寫啦,比如你帶了STRAIGHT_JOIN關(guān)鍵字,長(zhǎng)下面這樣"
select t1.*
from Table1 t1
STRAIGHT_JOIN Table2 t2
on t1.CommonID = t2.CommonID
"那我就知道強(qiáng)制先找Table1再關(guān)聯(lián)找Table2啦,類似的例子還有很多,我就不一一列舉了!"
(STRAIGHT_JOIN功能同join類似,但能讓左邊的表來(lái)驅(qū)動(dòng)右邊的表,能改表優(yōu)化器對(duì)于聯(lián)表查詢的執(zhí)行順序。)
我說(shuō)道:"哇塞,如何編寫一個(gè)高效的SQL,真是一門學(xué)問(wèn)??!"
于是,優(yōu)化器大哥將我變身為一個(gè)執(zhí)行計(jì)劃,然后交給執(zhí)行器啦~
第四章 我和執(zhí)行器的悲情經(jīng)歷
我:"執(zhí)行器大哥,你是用來(lái)做什么的?"
執(zhí)行器:"就是根據(jù)執(zhí)行計(jì)劃來(lái)進(jìn)行執(zhí)行查詢啦。我就根據(jù)你的指令,逐條調(diào)用底層存儲(chǔ)引擎,逐步執(zhí)行。"
MySQL定義了一系列抽象存儲(chǔ)引擎API,以支持插件式存儲(chǔ)引擎架構(gòu)。Mysql實(shí)現(xiàn)了一個(gè)抽象接口層,叫做 handler(sql/handler.h),其中定義了接口函數(shù),比如:ha_open, ha_index_end, ha_create等等,存儲(chǔ)引擎需要實(shí)現(xiàn)這些接口才能被系統(tǒng)使用。
末章 一些感慨
最后一個(gè)階段,Mysql會(huì)將查詢結(jié)果返回客戶端。
唯一需要說(shuō)明的是,如果是SELECT類型的SQL,Mysql會(huì)將查詢結(jié)果緩存起來(lái)。至于其他的SQL,就將該表涉及到的查詢緩存清空。
一些疑問(wèn)
這里關(guān)于權(quán)限驗(yàn)證究竟在哪個(gè)階段執(zhí)行,大家可能會(huì)有一些疑問(wèn)。
之前有一個(gè)大牛的文章說(shuō)是權(quán)限驗(yàn)證是在執(zhí)行階段,去執(zhí)行前驗(yàn)證權(quán)限,大家如果看過(guò)他的文章,可能會(huì)有疑問(wèn)。我也不是亂質(zhì)疑人家,畢竟我只是一個(gè)小咖。我在這里只是發(fā)表一下我自己的論點(diǎn),歡迎大家拍磚。
論點(diǎn)一:權(quán)限驗(yàn)證在執(zhí)行器中判斷從邏輯上說(shuō)不通
一條查詢SQL經(jīng)過(guò)查詢緩存、分析器、優(yōu)化器,執(zhí)行器。如果到最后一個(gè)階段執(zhí)行器中才發(fā)現(xiàn)權(quán)限不足、那不是前面一系列流程白做了,Mysql應(yīng)該不至于這么傻吧~
論點(diǎn)二:同《高性能Mysql》一書(shū)內(nèi)容不符
該書(shū)209頁(yè)有一句話如下圖所示
該書(shū)也指明權(quán)限驗(yàn)證是在預(yù)處理器中執(zhí)行。本文中將預(yù)處理和解析器統(tǒng)一劃分為分析器的范疇。
論點(diǎn)三:同源碼不符
我翻看了Mysql5.7.25這個(gè)版本的源碼,其在處理查詢這段的核心代碼如下
在sql_parse.cc文件中,有這么一段代碼如下
case SQLCOM_SELECT:
{
//省略
res= select_precheck(thd, lex, all_tables, first_table);
if (!res)
res= execute_sqlcom_select(thd, all_tables);
//省略
}
其中select_precheck是進(jìn)行權(quán)限校驗(yàn)。而優(yōu)化器和執(zhí)行器是在execute_sqlcom_select這個(gè)方法中。
當(dāng)然,大家有新的見(jiàn)解,歡迎留言。