懂了MySql編譯器吃什么都香,開發,業務得心應手


今天也沒啥講的,就跟大家一起探索一下MySQL編譯器。
數據庫系統能夠接受SQL語句,並返回數據查詢的結果,或者對數據庫中的數據進行修改,可以說幾乎每個程序員都使用過它。
而MySQL又是目前使用最廣泛的數據庫。所以,解析一下MySQL編譯並執行SQL語句的過程,一方面能幫助你加深對數據庫領域的編譯技術的理解;另一方面,由於SQL是一種最成功的DSL(特定領域語言),所以理解了MySQL編譯器的內部運作機制,也能加深你對所有使用數據操作類DSL的理解,比如文檔數據庫的查詢語言。另外,解讀SQL與它的運行時的關系,也有助於你在自己的領域成功地使用DSL技術。
那么,數據庫系統是如何使用編譯技術的呢?接下來,我就會花兩講的時間,帶你進入到MySQL的內部,做一次全面的探秘。
今天這一講,我先帶你了解一下如何跟蹤MySQL的運行,了解它處理一個SQL語句的過程,以及MySQL在詞法分析和語法分析方面的實現機制。
好,讓我們開始吧!

編譯並調試MySQL’

按照慣例,你要下載MySQL的源代碼。我下載的是8.0版本的分支。
源代碼里的主要目錄及其作用如下,我們需要分析的代碼基本都在sql目錄下,它包含了編譯器和服務端的核心組件。

1

MySQL的源代碼主要是.cc結尾的,也就是說,MySQL主要是用C++編寫的。另外,也有少量幾個代碼文件是用C語言編寫的。
為了跟蹤MySQL的執行過程,你要用Debug模式編譯MySQL,具體步驟可以參考這篇開發者文檔。
如果你用單線程編譯,大約需要1個小時。編譯好以后,先初始化出一個數據庫來:

./mysqld --initialize --user=mysql

這個過程會為root@localhost用戶,生成一個缺省的密碼。
接着,運行MySQL服務器:

./mysqld &

之后,通過客戶端連接數據庫服務器,這時我們就可以執行SQL了:

./mysql -uroot -p   #連接mysql server

最后,我們把GDB調試工具附加到mysqld進程上,就可以對它進行調試了。

gdb -p `pidof mysqld`  #pidof是一個工具,用於獲取進程的id,你可以安裝一下

提示:這一講中,我是采用了一個CentOS 8的虛擬機來編譯和調試MySQL。我也試過在macOS下編譯,並用LLDB進行調試,也一樣方便。
注意,你在調試程序的時候,有兩個設置斷點的好地方:
● dispatch_command:在sql/sql_parse.cc文件里。在接受客戶端請求的時候(比如一個SQL語句),會在這里集中處理。
● my_message_sql:在sql/mysqld.cc文件里。當系統需要輸出錯誤信息的時候,會在這里集中處理。
這個時候,我們在MySQL的客戶端輸入一個查詢命令,就可以從雇員表里查詢姓和名了。在這個例子中,我采用的數據庫是MySQL的一個示例數據庫employees,你可以根據它的文檔來生成示例數據庫。

mysql> select first_name, last_name from employees;    #從mysql庫的user表中查詢信息

這個命令被mysqld接收到以后,就會觸發斷點,並停止執行。這個時候,客戶端也會老老實實地停在那里,等候從服務端傳回數據。即使你在后端跟蹤代碼的過程會花很長的時間,客戶端也不會超時,一直在安靜地等待。給我的感覺就是,MySQL對於調試程序還是很友好的。
在GDB中輸入bt命令,會打印出調用棧,這樣你就能了解一個SQL語句,在MySQL中執行的完整過程。為了方便你理解和復習,這里我整理成了一個表格:

1

我也把MySQL執行SQL語句時的一些重要程序入口記錄了下來,這也需要你重點關注。它反映了執行SQL過程中的一些重要的處理階段,包括語法分析、處理上下文、引用消解、優化和執行。你在這些地方都可以設置斷點。

1

好了,現在你就已經做好准備,能夠分析MySQL的內部實現機制了。不過,由於MySQL執行的是SQL語言,它跟我們前面分析的高級語言有所不同。所以,我們先稍微回顧一下SQL語言的特點。

SQL語言:數據庫領域的DSL

SQL是結構化查詢語言(Structural Query Language)的英文縮寫。舉個例子,這是一個很簡單的SQL語句:

select emp_no, first_name, last_name from employees;

其實在大部分情況下,SQL都是這樣一個一個來做語句執行的。這些語句又分為DML(數據操縱語言)和DDL(數據定義語言)兩類。前者是對數據的查詢、修改和刪除等操作,而后者是用來定義數據庫和表的結構(又叫模式)。
我們平常最多使用的是DML。而DML中,執行起來最復雜的是select語句。所以,在本講,我都是用select語句來給你舉例子。
那么,SQL跟我們前面分析的高級語言相比有什么不同呢?
第一個特點:SQL是聲明式(Declarative)的。這是什么意思呢?其實就是說,SQL語句能夠表達它的計算邏輯,但它不需要描述控制流。
高級語言一般都有控制流,也就是詳細規定了實現一個功能的流程:先調用什么功能,再調用什么功能,比如if語句、循環語句等等。這種方式叫做命令式(imperative)編程。
更深入一點,聲明式編程說的是“要什么”,它不關心實現的過程;而命令式編程強調的是“如何做”。前者更接近人類社會的領域問題,而后者更接近計算機實現。
第二個特點:SQL是一種特定領域語言(DSL,Domain Specific Language),專門針對關系數據庫這個領域的。SQL中的各個元素能夠映射成關系代數中的操作術語,比如選擇、投影、連接、笛卡爾積、交集、並集等操作。它采用的是表、字段、連接等要素,而不需要使用常見的高級語言的變量、類、函數等要素。
所以,SQL就給其他DSL的設計提供了一個很好的參考:
● 采用聲明式,更加貼近領域需求。比如,你可以設計一個報表的DSL,這個DSL只需要描述報表的特征,而不需要描述其實現過程。
● 采用特定領域的模型、術語,甚至是數學理論。比如,針對人工智能領域,你完全就可以用張量計算(力學概念)的術語來定義DSL。
好了,現在我們分析了SQL的特點,從而也讓你了解了DSL的一些共性特點。那么接下來,順着MySQL運行的脈絡,我們先來了解一下MySQL是如何做詞法分析和語法分析的。

詞法和語法分析

詞法分析的代碼是在sql/sql_lex.cc中,入口是MYSQLlex()函數。在sql/lex.h中,有一個symbols[]數組,它定義了各類關鍵字、操作符。
MySQL的詞法分析器也是手寫的,這給算法提供了一定的靈活性。比如,SQL語句中,Token的解析是跟當前使用的字符集有關的。使用不同的字符集,詞法分析器所占用的字節數是不一樣的,判斷合法字符的依據也是不同的。而字符集信息,取決於當前的系統的配置。詞法分析器可以根據這些配置信息,正確地解析標識符和字符串。
MySQL的語法分析器是用bison工具生成的,bison是一個語法分析器生成工具,它是GNU版本的yacc。bison支持的語法分析算法是LALR算法,而LALR是LR算法家族中的一員,它能夠支持大部分常見的語法規則。bison的規則文件是sql/sql_yacc.yy,經過編譯后會生成sql/sql_yacc.cc文件。
sql_yacc.yy中,用你熟悉的EBNF格式定義了MySQL的語法規則。我節選了與select語句有關的規則,如下所示,從中你可以體會一下,SQL語句的語法是怎樣被一層一層定義出來的:

select_stmt:
          query_expression
        | ...
        | select_stmt_with_into
        ;
query_expression:
          query_expression_body opt_order_clause opt_limit_clause
        | with_clause query_expression_body opt_order_clause opt_limit_clause
        | ...
        ;
query_expression_body:
          query_primary
        | query_expression_body UNION_SYM union_option query_primary
        | ...
        ;
query_primary:
          query_specification
        | table_value_constructor
        | explicit_table
        ;
query_specification:
          ...
        | SELECT_SYM             /*select關鍵字*/
          select_options         /*distinct等選項*/
          select_item_list       /*select項列表*/
          opt_from_clause        /*可選:from子句*/
          opt_where_clause       /*可選:where子句*/
          opt_group_clause       /*可選:group子句*/
          opt_having_clause      /*可選:having子句*/
          opt_window_clause      /*可選:window子句*/
        ;
...

其中,query_expression就是一個最基礎的select語句,它包含了SELECT關鍵字、字段列表、from子句、where子句等。
你可以看一下select_options、opt_from_clause和其他幾個以opt開頭的規則,它們都是SQL語句的組成部分。opt是可選的意思,也就是它的產生式可能產生ε。

opt_from_clause:
          /* Empty. */ 
        | from_clause
        ;

另外,你還可以看一下表達式部分的語法。在MySQL編譯器當中,對於二元運算,你可以大膽地寫成左遞歸的文法。因為它的語法分析的算法用的是LALR,這個算法能夠自動處理左遞歸。
一般研究表達式的時候,我們總是會關注編譯器是如何處理結合性和優先級的。那么,bison是如何處理的呢?
原來,bison里面有專門的規則,可以規定運算符的優先級和結合性。在sql_yacc.yy中,你會看到如下所示的規則片段:

1

你可以看一下bit_expr的產生式,它其實完全把加減乘數等運算符並列就行了。

bit_expr : 
         ...
         | bit_expr '+' bit_expr %prec '+'
         | bit_expr '-' bit_expr %prec '-'
         | bit_expr '*' bit_expr %prec '*'
         | bit_expr '/' bit_expr %prec '/'
         ...
         | simple_expr

如果你只是用到加減乘除的運算,那就可以不用在產生式的后面加%prec這個標記。但由於加減乘除這幾個還可以用在其他地方,比如“-a”可以用來表示把a取負值;減號可以用在一元表達式當中,這會比用在二元表達式中有更高的優先級。也就是說,為了區分同一個Token在不同上下文中的優先級,我們可以用%prec,來說明該優先級是上下文依賴的。
好了,在了解了詞法分析器和語法分析器以后,我們接着來跟蹤一下MySQL的執行,看看編譯器所生成的解析樹和AST是什么樣子的。
在sql_class.cc的sql_parser()方法中,編譯器執行完解析程序之后,會返回解析樹的根節點root,在GDB中通過p命令,可以逐步打印出整個解析樹。你會看到,它的根節點是一個PT_select_stmt指針(見圖3)。
解析樹的節點是在語法規則中規定的,這是一些C++的代碼,它們會嵌入到語法規則中去。
下面展示的這個語法規則就表明,編譯器在解析完query_expression規則以后,要創建一個PT_query_expression的節點,其構造函數的參數分別是三個子規則所形成的節點。對於query_expression_body和query_primary這兩個規則,它們會直接把子節點返回,因為它們都只有一個子節點。這樣就會簡化解析樹,讓它更像一棵AST。關於AST和解析樹(也叫CST)的區別,我在解析Python的編譯器中講過了,你可以回憶一下。

query_expression:
          query_expression_body
          opt_order_clause
          opt_limit_clause
          {
            $$ = NEW_PTN PT_query_expression($1, $2, $3); /*創建節點*/
          }
        | ...
query_expression_body:
          query_primary
          {
            $$ = $1;   /*直接返回query_primary的節點*/
          }
        | ...
        
query_primary:
          query_specification
          {
            $$= $1;  /*直接返回query_specification的節點*/
          }
        | ...

最后,對於“select first_name, last_name from employees”這樣一個簡單的SQL語句,它所形成的解析樹如下:

1

而對於“select 2 + 3”這樣一個做表達式計算的SQL語句,所形成的解析樹如下。你會看到,它跟普通的高級語言的表達式的AST很相似:

1

圖4中的PT_query_expression等類,就是解析樹的節點,它們都是Parse_tree_node的子類(PT是Parse Tree的縮寫)。這些類主要定義在sql/parse_tree_nodes.h和parse_tree_items.h文件中。
其中,Item代表了與“值”有關的節點,它的子類能夠用於表示字段、常量和表達式等。你可以通過Item的val_int()、val_str()等方法獲取它的值。

1

由於SQL是一個個單獨的語句,所以select、insert、update等語句,它們都各自有不同的根節點,都是Parse_tree_root的子類。

1

好了,現在你就已經了解了SQL的解析過程和它所生成的AST了。前面我說過,MySQL采用的是LALR算法,因此我們可以借助MySQL編譯器,來加深一下對LR算法家族的理解。

重溫LR算法

你在閱讀yacc.yy文件的時候,在注釋里,你會發現如何跟蹤語法分析器的執行過程的一些信息。
你可以用下面的命令,帶上“-debug”參數,來啟動MySQL服務器:

mysqld --debug="d,parser_debug"

然后,你可以通過客戶端執行一個簡單的SQL語句:“select 2+3*5”。在終端,會輸出語法分析的過程。這里我截取了一部分界面,通過這些輸出信息,你能看出LR算法執行過程中的移進、規約過程,以及工作區內和預讀的信息。

1

我來給你簡單地復現一下這個解析過程。

第1步,編譯器處於狀態0,並且預讀了一個select關鍵字。你已經知道,LR算法是基於一個DFA的。在這里的輸出信息中,你能看到某些狀態的編號達到了一千多,所以這個DFA還是比較大的。

第2步,把select關鍵字移進工作區,並進入狀態42。這個時候,編譯器已經知道后面跟着的一定是一個select語句了,也就是會使用下面的語法規則:

query_specification:
          ...
        | SELECT_SYM             /*select關鍵字*/
          select_options         /*distinct等選項*/
          select_item_list       /*select項列表*/
          opt_from_clause        /*可選:from子句*/
          opt_where_clause       /*可選:where子句*/
          opt_group_clause       /*可選:group子句*/
          opt_having_clause      /*可選:having子句*/
          opt_window_clause      /*可選:window子句*/
        ;

為了給你一個直觀的印象,這里我畫了DFA的局部示意圖(做了一定的簡化),如下所示。你可以看到,在狀態42,點符號位於“select”關鍵字之后、select_options之前。select_options代表了“distinct”這樣的一些關鍵字,但也有可能為空。

1

第3步,因為預讀到的Token是一個數字(NUM),這說明select_options產生式一定生成了一個ε,因為NUM是在select_options的Follow集合中。

這就是LALR算法的特點,它不僅會依據預讀的信息來做判斷,還要依據Follow集合中的元素。所以編譯器做了一個規約,也就是讓select_options為空。
也就是,編譯器依據“select_options->ε”做了一次規約,並進入了新的狀態920。注意,狀態42和920從DFA的角度來看,它們是同一個大狀態。而DFA中包含了多個小狀態,分別代表了不同的規約情況。

1

你還需要注意,這個時候,老的狀態都被壓到了棧里,所以棧里會有0和42兩個狀態。棧里的這些狀態,其實記錄了推導的過程,讓我們知道下一步要怎樣繼續去做推導。

1

第4步,移進NUM。這時又進入一個新狀態720。

1

而舊的狀態也會入棧,記錄下推導路徑:

1

第5~8步,依次依據NUM_literal->NUM、literal->NUM_literal、simple_expr->literal、bit_expr->simple_expr這四條產生式做規約。這時候,編譯器預讀的Token是+號,所以你會看到,圖中的紅點停在+號前。

1

第9~10步,移進+號和NUM。這個時候,狀態又重新回到了720。這跟第4步進入的狀態是一樣的。

1

而棧里的目前有5個狀態,記錄了完整的推導路徑。

1

到這里,其實你就已經了解了LR算法做移進和規約的思路了。不過你還可以繼續往下研究。由於棧里保留了完整的推導路徑,因此MySQL編譯器最后會依次規約回來,把棧里的元素清空,並且形成一棵完整的AST。

課程小結

這一講,我帶你初步探索了MySQL編譯SQL語句的過程。你需要記住幾個關鍵點:
● 掌握如何用GDB來跟蹤MySQL的執行的方法。你要特別注意的是,我給你梳理的那些關鍵的程序入口,它是你理解MySQL運行過程的地圖。
● SQL語言是面向關系數據庫的一種DSL,它是聲明式的,並采用了領域特定的模型和術語,可以為你設計自己的DSL提供啟發。
● MySQL的語法分析器是采用bison工具生成的。這至少說明,語法分析器生成工具是很有用的,連正式的數據庫系統都在使用它,所以你也可以大膽地使用它,來提高你的工作效率。我在最后的參考資料中給出了bison的手冊,希望你能自己閱讀一下,做一些簡單的練習,掌握bison這個工具。
● 最后,你一定要知道LR算法的運行原理,知其所以然,這也會更加有助於你理解和用好工具。
我依然把本講的內容給你整理成了一張知識地圖,供你參考和復習回顧:

1

參考資料

  1. MySQL的內行手冊(MySQL Internals Manual)能提供一些重要的信息。但我發現文檔內容經常跟源代碼的版本不同步,比如介紹源代碼的目錄結構的信息就過時了,你要注意這點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM