工作中需要借鑒MySQL對(duì)于select的具體實(shí)現(xiàn),在網(wǎng)上搜了很久,幾乎都是介紹原理的多艇,對(duì)于實(shí)現(xiàn)細(xì)節(jié)都沒(méi)有介紹,無(wú)奈之下只得自己對(duì)著源碼gdb像吻。結(jié)合以前對(duì)于sql解析的了解峻黍,對(duì)mysql select的具體實(shí)現(xiàn)有了大致的了解,總結(jié)一下拨匆。
如果要gdb單步調(diào)試姆涩,需要在編譯MySQl時(shí)加上debug選項(xiàng),參見(jiàn)這篇博客.編譯好以后就可以用gdb啟動(dòng)了惭每。如果希望mysql運(yùn)行時(shí)有日志輸出骨饿,可以指定輸出文件的路徑和日志類型:--debug=d,info,error,query,enter,general,where:O,/tmp/mysqld.trace
日志對(duì)MySQl內(nèi)部邏輯的了解還是挺有用的。
MySQl在設(shè)計(jì)時(shí),采用了這樣的思路:針對(duì)主要應(yīng)用場(chǎng)景選擇一個(gè)或幾個(gè)性能優(yōu)異的核心算法作為引擎,然后努力將一些非主要應(yīng)用場(chǎng)景作為該算法的特例或變種植入到引擎當(dāng)中。具體而言,MySQL的select查詢中贷祈,核心功能就是JOIN查詢,因此在設(shè)計(jì)時(shí),核心實(shí)現(xiàn)JOIN功能,對(duì)于其它功能,都通過(guò)轉(zhuǎn)換為JOIN來(lái)實(shí)現(xiàn)储藐。
比如select id, name from student;
俱济,MySQL在執(zhí)行時(shí),也會(huì)轉(zhuǎn)換為JOIN來(lái)操作钙勃。
用gdb單步跟蹤后可以看出MySQL的執(zhí)行過(guò)程大致如下:
- 收到請(qǐng)求后分配線程處理蛛碌;
- sql解析,MySQL解析完sql以后辖源,會(huì)生成很多item類蔚携。item類是sql解析和執(zhí)行中最重要的類之一,對(duì)于它的介紹可以參見(jiàn)這里克饶。
- 執(zhí)行sql酝蜒,可以看到
JOIN::exec
,MySQL是將任何select都轉(zhuǎn)換為JOIN來(lái)處理的矾湃。
以sql:select A.id, B.score from student A left join subject B on A.id=B.id where A.age > 10 and B.score > 60;
為例來(lái)說(shuō)明上面的步驟3的具體過(guò)程亡脑。
首先,MySQL在執(zhí)行sql之前,會(huì)對(duì)sql進(jìn)行優(yōu)化處理霉咨,具體是在JOIN::optimise
函數(shù)中完成蛙紫。MySQL針對(duì)JOIN的優(yōu)化做的非常好,因此才會(huì)將其他操作都轉(zhuǎn)換為性能實(shí)現(xiàn)的非常好的JOIN操作途戒。對(duì)于上面的sql坑傅,MySQL在執(zhí)行時(shí),會(huì)將join的key也轉(zhuǎn)換為一個(gè)where條件:A.id=B.id
來(lái)執(zhí)行喷斋,那么經(jīng)過(guò)處理后唁毒,上面的sql就有了3個(gè)where條件:
-
A.age > 10
; -
A.id = B.id
继准; -
B.score > 60
枉证;
預(yù)處理完以后開(kāi)始執(zhí)行,即JOIN::exec
函數(shù)移必,首先會(huì)調(diào)用send_fields
函數(shù)室谚,將最終結(jié)果的信息返回,然后調(diào)用do_select
崔泵。MySQL的join是采用nested loop join秒赤,可以參見(jiàn)這篇博客。在do_select
函數(shù)中憎瘸,通過(guò)調(diào)用sub_select
函數(shù)來(lái)具體實(shí)現(xiàn)join功能入篮。
在上面的例子中,需要完成2個(gè)join:先join表A幌甘,再join表B(這里請(qǐng)注意潮售,不是涉及幾個(gè)表,就需要join幾個(gè)表锅风,MySQL的join優(yōu)化還是挺強(qiáng)大的酥诽,具體解釋見(jiàn)后)。在MySQL進(jìn)行sql解析時(shí)皱埠,會(huì)生成一個(gè)需要join的表的list肮帐,后面會(huì)挨個(gè)對(duì)該list的表進(jìn)行join操作。
繼續(xù)gdb边器,在sub_select
函數(shù)中训枢,可以看到這樣一行代碼:(*join_tab->read_first_record)(join_tab)
這個(gè)就是讀取表A的第一行結(jié)果,可以看join_tab
里面的信息有表A的名字忘巧。接下來(lái)就是很關(guān)鍵的一個(gè)函數(shù):evaluate_join_record
恒界,這個(gè)函數(shù)主要做2件事:
- 將當(dāng)前已經(jīng)拿到的信息進(jìn)行where條件計(jì)算,判斷是否需要繼續(xù)往下走砚嘴;
- 遞歸JOIN仗处;
還是以上面的sql為例眯勾,首先執(zhí)行第一個(gè)join,此時(shí)會(huì)遍歷表A的每一行結(jié)果婆誓,每遍歷一個(gè)結(jié)果吃环,會(huì)進(jìn)行where條件的判斷。這里需要注意:當(dāng)前的where條件判斷只會(huì)判斷已經(jīng)讀出來(lái)的列洋幻,由于此時(shí)只讀出來(lái)表A的數(shù)據(jù)郁轻,因此現(xiàn)在只能對(duì)第一個(gè)where條件,即A.age > 10
進(jìn)行判斷文留,如果滿足好唯,則遞歸調(diào)用join:sql_select.cc: 11037 rc=(*join_tab->next_select)(join, join_tab+1, 0);
,這里的next_select函數(shù)就是sub_select
燥翅,MySQL就是這樣來(lái)實(shí)現(xiàn)遞歸操作的骑篙。如果不滿足,則不會(huì)遞歸join森书,而是繼續(xù)到下一行數(shù)據(jù)靶端,從而達(dá)到剪枝的目的。
繼續(xù)跟下去凛膏,此時(shí)通過(guò)上面的next_select
遞歸的又調(diào)用到sub_select
上杨名,同樣會(huì)走上面的邏輯,即先read_first_record
猖毫,然后evaluate_join_record
台谍,這里由于表A和表B的數(shù)據(jù)都有了,于是可以對(duì)上面后面2個(gè)where條件:A.id = B.id
和B.score > 60
進(jìn)行判斷了吁断。到此趁蕊,所有的where條件都已經(jīng)判斷完畢,如果當(dāng)前行對(duì)3個(gè)where條件都滿足仔役,就可以將結(jié)果輸出掷伙。
以上就是select實(shí)現(xiàn)的大體過(guò)程,主要有2點(diǎn)骂因,一個(gè)是join是采用遞歸實(shí)現(xiàn)的,另一個(gè)是每讀一個(gè)表的數(shù)據(jù)赃泡,會(huì)將當(dāng)前的where條件進(jìn)行計(jì)算寒波,剪枝。還有一個(gè)細(xì)節(jié)沒(méi)有提到:MySQL是如何進(jìn)行where條件判斷的升熊?或者說(shuō)俄烁,MySQL是如何進(jìn)行表達(dá)式計(jì)算的?
答案就是前面提到的item類级野。當(dāng)MySQL在解析時(shí)页屠,會(huì)將sql解析為很多item,同時(shí)也會(huì)建立各個(gè)item之間的關(guān)系。對(duì)于表達(dá)式辰企,會(huì)生成一棵語(yǔ)法樹(shù)风纠。比如表達(dá)式:B.score > 60
,此時(shí)會(huì)生成3個(gè)item:B.score
牢贸、>
和60
竹观,其中B.score
和60
分別是>
的左右孩子,這樣潜索,求表達(dá)式的值時(shí)臭增,就是求>
的val_int()
,然后就會(huì)遞歸的調(diào)用左右子樹(shù)的val_int()
竹习,再做比較判斷即可誊抛。
還有一個(gè)問(wèn)題:如何求B.score
的val_int()
?對(duì)于此問(wèn)題的答案我沒(méi)有具體看過(guò)整陌,根據(jù)之前一個(gè)同事的sql實(shí)現(xiàn)方式拗窃,我是這樣推測(cè)的:B.score
是數(shù)據(jù)表中的真實(shí)值,因此它的值肯定是通過(guò)去表中獲取蔓榄。在item類中并炮,有一個(gè)函數(shù):fix_field
,它是用于告訴外界甥郑,去哪里獲取此item的值逃魄,往往在sql執(zhí)行的預(yù)處理階段調(diào)用。于是在預(yù)處理時(shí)澜搅,告訴該item去某個(gè)固定buffer讀取結(jié)果伍俘,同時(shí),每當(dāng)從表中讀出一行數(shù)據(jù)時(shí)勉躺,將該數(shù)據(jù)保存在該buffer中癌瘾,這樣就可以將兩者關(guān)聯(lián)起來(lái)。這個(gè)部分純屬個(gè)人推測(cè)饵溅,感興趣的同學(xué)可以自己根據(jù)源碼看看妨退。
再回到之前提到的一點(diǎn),如果我們將sql稍微改一下:select A.id, B.score from student A left join subject B on A.id=B.id where B.score > 60;
蜕企,即去掉第一個(gè)where條件咬荷,此時(shí)會(huì)發(fā)生什么?
答案是轻掩,MySQL會(huì)做一個(gè)優(yōu)化幸乒,將sql轉(zhuǎn)換為select B.id, B.score from subject B where B.score > 60
,這樣就不需要A同B join的邏輯了唇牧。實(shí)際上最開(kāi)始我在gdb時(shí)就用的這條sql罕扎,結(jié)果死活看不到遞歸調(diào)用sub_select
的場(chǎng)景聚唐,還以為原理不對(duì),后來(lái)才發(fā)現(xiàn)是MySQL優(yōu)化搗的亂腔召。