工作中需要借鑒MySQL對于select的具體實現,在網上搜了很久,幾乎都是介紹原理的,對于實現細節都沒有介紹,無奈之下只得自己對著源碼gdb。結合以前對于sql解析的了解,對mysql select的具體實現有了大致的了解,總結一下。
如果要gdb單步調試,需要在編譯MySQl時加上debug選項,參見這篇博客.編譯好以后就可以用gdb啟動了。如果希望mysql運行時有日志輸出,可以指定輸出文件的路徑和日志類型:--debug=d,info,error,query,enter,general,where:O,/tmp/mysqld.trace
日志對MySQl內部邏輯的了解還是挺有用的。
MySQl在設計時,采用了這樣的思路:針對主要應用場景選擇一個或幾個性能優異的核心算法作為引擎,然后努力將一些非主要應用場景作為該算法的特例或變種植入到引擎當中。具體而言,MySQL的select查詢中,核心功能就是JOIN查詢,因此在設計時,核心實現JOIN功能,對于其它功能,都通過轉換為JOIN來實現。
比如select id, name from student;
,MySQL在執行時,也會轉換為JOIN來操作。
用gdb單步跟蹤后可以看出MySQL的執行過程大致如下:
- 收到請求后分配線程處理;
- sql解析,MySQL解析完sql以后,會生成很多item類。item類是sql解析和執行中最重要的類之一,對于它的介紹可以參見這里。
- 執行sql,可以看到
JOIN::exec
,MySQL是將任何select都轉換為JOIN來處理的。
以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;
為例來說明上面的步驟3的具體過程。
首先,MySQL在執行sql之前,會對sql進行優化處理,具體是在JOIN::optimise
函數中完成。MySQL針對JOIN的優化做的非常好,因此才會將其他操作都轉換為性能實現的非常好的JOIN操作。對于上面的sql,MySQL在執行時,會將join的key也轉換為一個where條件:A.id=B.id
來執行,那么經過處理后,上面的sql就有了3個where條件:
-
A.age > 10
; -
A.id = B.id
; -
B.score > 60
;
預處理完以后開始執行,即JOIN::exec
函數,首先會調用send_fields
函數,將最終結果的信息返回,然后調用do_select
。MySQL的join是采用nested loop join,可以參見這篇博客。在do_select
函數中,通過調用sub_select
函數來具體實現join功能。
在上面的例子中,需要完成2個join:先join表A,再join表B(這里請注意,不是涉及幾個表,就需要join幾個表,MySQL的join優化還是挺強大的,具體解釋見后)。在MySQL進行sql解析時,會生成一個需要join的表的list,后面會挨個對該list的表進行join操作。
繼續gdb,在sub_select
函數中,可以看到這樣一行代碼:(*join_tab->read_first_record)(join_tab)
這個就是讀取表A的第一行結果,可以看join_tab
里面的信息有表A的名字。接下來就是很關鍵的一個函數:evaluate_join_record
,這個函數主要做2件事:
- 將當前已經拿到的信息進行where條件計算,判斷是否需要繼續往下走;
- 遞歸JOIN;
還是以上面的sql為例,首先執行第一個join,此時會遍歷表A的每一行結果,每遍歷一個結果,會進行where條件的判斷。這里需要注意:當前的where條件判斷只會判斷已經讀出來的列,由于此時只讀出來表A的數據,因此現在只能對第一個where條件,即A.age > 10
進行判斷,如果滿足,則遞歸調用join:sql_select.cc: 11037 rc=(*join_tab->next_select)(join, join_tab+1, 0);
,這里的next_select函數就是sub_select
,MySQL就是這樣來實現遞歸操作的。如果不滿足,則不會遞歸join,而是繼續到下一行數據,從而達到剪枝的目的。
繼續跟下去,此時通過上面的next_select
遞歸的又調用到sub_select
上,同樣會走上面的邏輯,即先read_first_record
,然后evaluate_join_record
,這里由于表A和表B的數據都有了,于是可以對上面后面2個where條件:A.id = B.id
和B.score > 60
進行判斷了。到此,所有的where條件都已經判斷完畢,如果當前行對3個where條件都滿足,就可以將結果輸出。
以上就是select實現的大體過程,主要有2點,一個是join是采用遞歸實現的,另一個是每讀一個表的數據,會將當前的where條件進行計算,剪枝。還有一個細節沒有提到:MySQL是如何進行where條件判斷的?或者說,MySQL是如何進行表達式計算的?
答案就是前面提到的item類。當MySQL在解析時,會將sql解析為很多item,同時也會建立各個item之間的關系。對于表達式,會生成一棵語法樹。比如表達式:B.score > 60
,此時會生成3個item:B.score
、>
和60
,其中B.score
和60
分別是>
的左右孩子,這樣,求表達式的值時,就是求>
的val_int()
,然后就會遞歸的調用左右子樹的val_int()
,再做比較判斷即可。
還有一個問題:如何求B.score
的val_int()
?對于此問題的答案我沒有具體看過,根據之前一個同事的sql實現方式,我是這樣推測的:B.score
是數據表中的真實值,因此它的值肯定是通過去表中獲取。在item類中,有一個函數:fix_field
,它是用于告訴外界,去哪里獲取此item的值,往往在sql執行的預處理階段調用。于是在預處理時,告訴該item去某個固定buffer讀取結果,同時,每當從表中讀出一行數據時,將該數據保存在該buffer中,這樣就可以將兩者關聯起來。這個部分純屬個人推測,感興趣的同學可以自己根據源碼看看。
再回到之前提到的一點,如果我們將sql稍微改一下:select A.id, B.score from student A left join subject B on A.id=B.id where B.score > 60;
,即去掉第一個where條件,此時會發生什么?
答案是,MySQL會做一個優化,將sql轉換為select B.id, B.score from subject B where B.score > 60
,這樣就不需要A同B join的邏輯了。實際上最開始我在gdb時就用的這條sql,結果死活看不到遞歸調用sub_select
的場景,還以為原理不對,后來才發現是MySQL優化搗的亂。