自己實現一個SQL解析引擎
功能:將用戶輸入的SQL語句序列轉換爲一個可執行的操作序列,並返回查詢的結果集。
SQL的解析引擎包括查詢編譯與查詢優化和查詢的運行,主要包括3個步驟:
- 查詢分析:
- 制定邏輯查詢計劃(優化相關)
- 制定物理查詢計劃(優化相關)
- 查詢分析: 將SQL語句表示成某種有用的語法樹.
- 制定邏輯查詢計劃: 把語法樹轉換成一個關係代數表達式或者類似的結構,這個結構通常稱作邏輯計劃。
- 制定物理查詢計劃:把邏輯計劃轉換成物理查詢計劃,要求指定操作執行的順序,每一步使用的算法,操作之間的傳遞方式等。
查詢分析各模塊主要函數間的調用關係:
圖1.SQL引擎間模塊的調用關係
FLEX簡介
flex是一個詞法分析工具,其輸入爲後綴爲.l的文件,輸出爲.c的文件. 示例是一個類似Unix的單詞統計程序wc
。
.l文件通常分爲3部分:
definition
部分爲定義部分,包括引入頭文件,變量聲明,函數聲明,註釋等,這部分會被原樣拷貝到輸出的.c文件中。
rules
部分定義詞法規則,使用正則表達式定義詞法,後面大括號內則是掃描到對應詞法時的動作代碼。
code
部分爲C語言的代碼。yylex
爲flex的函數,使用yylex
開始掃描。
%option
指定flex掃描時的一些特性。yywrap
通常在多文件掃描時定義使用。常用的一些選項有
noyywrap
不使用yywrap函數
yylineno
使用行號
case-insensitive
正則表達式規則大小寫無關
flex文件的編譯
Bison簡介
Bison
作爲一個語法分析器,輸入爲一個.y的文件,輸出爲一個.h文件和一個.c文件。通常Bison需要使用Flex作爲協同的詞法分析器來獲取記號流。Flex識別正則表達式來獲取記號,Bison則分析這些記號基於邏輯規則進行組合。
計算器的示例:calc.y
Bision文件編譯
通常,Bison默認是不可重入的,如果希望在yyparse
結束後保留解析的語法樹,可以採用兩種方式,一種是增加一個全局變量,另一種則是設置一個額外參數,其中ParseResult可以是用戶自己定義的結構體。
%parse-param {ParseResult *result}
在規則代碼中可以引用該參數:
調用yyparse時則爲:
ParseResult p;
yyparse(&p);
SQL解析引擎中的數據結構
語法樹結構
在實現的時候可以把語法樹和邏輯計劃都看成是樹結構和列表結構,而物理計劃更像像是鏈式結構。樹結構要注意區分葉子節點(也叫終止符節點)和非葉子節點(非終止符節點)。同時葉子節點和非葉子節點都可能有多種類型。
語法樹的節點:包含兩個部分,節點的類型的枚舉值kind,表示節點值的聯合體u,聯合體中包含了各個節點所需的字段。
在語法樹中,分析樹的葉子節點爲數字,字符串,屬性等,其他爲內部節點。因此有些數據庫的實現中將語法樹的節點定義爲如下的ParseNode結構。
邏輯計劃結構
邏輯計劃的內部節點是算子,葉子節點是關係.
邏輯計劃節點的類型PLANNODEKIND的枚舉值如下:
物理計劃結構
物理邏輯計劃中關係掃描運算符爲葉子節點,其他運算符爲內部節點。擁有3個迭代器函數open,close,get_next_row。其定義如下:
物理查詢計劃的節點類型PHYOPNODEKIND枚舉如下:
節點內存池
可以看到分析樹,邏輯計劃樹和物理查詢樹都是以指針爲主的結構體,如果每次都動態從申請的話,會比較耗時。需要使用內存池的方式,一次性申請多個節點內存,供以後調用。下面是一種簡單的方式,每次創建節點時都使用newnode函數即可。程序結束時再釋放內存池即可。
查詢分析
查詢分析需要對查詢語句進行詞法分析和語法分析,構建語法樹。詞法分析是指識別SQL語句中的有意義的邏輯單元,如關鍵字(SELECT,INSERT等),數字,函數名等。語法分析則是根據語法規則將識別出來的詞組合成有意義的語句。 詞法分析工具LEX,語法分析工具爲Yacc,在GNU的開源軟件中對應的是Flex和Bison,通常都是搭配使用。
詞法和語法分析
SQL引擎的詞法分析和語法分析採用Flex和Bison生成,parse_sql爲生成語法樹的入口,調用bison的yyparse完成。源文件可以這樣表示
文件 | 意義 |
---|---|
parse_node.h parse_node.cpp | 定義語法樹節點結構和方法,入口函數爲parse_sql |
print_node.cpp | 打印節點信息 |
psql.y | 定義語法結構,由Bison語法書寫 |
psql.l | 定義詞法結構,由Flex語法書寫 |
SQL查詢語句語法規則
熟悉Bison和Flex的用法之後,我們就可以利用Flex獲取記號,Bison設計SQL查詢語法規則。一個SQL查詢的語句序列由多個語句組成,以分號隔開,單條的語句又有DML,DDL,功能語句之分。
stmt_list : stmt ‘;’
| stmt_list stmt ‘;’
;
stmt: ddl
| dml
| unility
| nothing
;
dml: select_stmt
| insert_stmt
| delete_stmt
| update_stmt
| replace_stmt
;
以DELETE 單表語法爲例
用Bison可以表示爲:
然後在把opt_where
,opt_groupby
,table_ident
等一直遞歸下去,直到不能在細分爲止。
SQL語句分爲DDL語句和DML語句和utility語句,其中只有DML語句需要制定執行計劃,其他的語句轉入功能模塊執行。
制定邏輯計劃
執行順序
語法樹轉爲邏輯計劃時各算子存在先後順序。以select語句爲例,執行的順序爲:
FROM > WHERE > GROUP BY> HAVING > SELECT > DISTINCT > UNION > ORDER BY >
LIMIT
。
沒有優化的邏輯計劃應按照上述順序逐步生成或者逆向生成。轉爲邏輯計劃算子則對應爲:
JOIN –> FILTER -> GROUP -> FILTER(HAVING) -> PROJECTION -> DIST -> UNION
-> SORT -> LIMIT
。
邏輯計劃的優化
邏輯計劃的優化需要更細一步的粒度,將FILTER對應的表達式拆分成多個原子表達式。如WHERE t1.a = t2.a AND t2.b = '1990'
可以拆分成兩個表達式:
1)t1.a = t2.a
2)t2.b = '1990'
不考慮謂詞LIKE,IN的情況下,原子表達式實際上就是一個比較關係表達式,其節點爲列名,數字,字符串,可以將原子表達式定義爲
CompOpType爲“>”, ”<” ,”=”等各種比較操作符的枚舉值。
如果表達式符合 attr comp value 或者 value comp attr,則可以將該原子表達式下推到對應的葉子節點之上,增加一個Filter。
如果是attr = value類型,且attr是關係的索引的話,則可以採用索引掃描IndexScan。
當計算三個或多個關係的並交時,先對最小的關係進行組合。
還有其他的優化方法可以進一步發掘。內存數據庫與存儲在磁盤上的數據庫的代價估計不一樣。根據處理查詢時CPU和內存佔用的代價,主要考慮以下一些因素:
- 查詢讀取的記錄數;
- 結果是否排序(這可能會導致使用臨時表);
- 是否需要訪問索引和原表。
制定物理計劃
物理查詢計劃主要是完成一些算法選擇的工作。如關係掃描運算符包括:
TableScan(R)
:按任意順序讀入所以存放在R中的元組。
SortScan(R,L)
:按順序讀入R的元組,並以列L的屬性進行排列
IndexScan(R,C)
: 按照索引C讀入R的元組。
根據不同的情況會選擇不同的掃描方式。其他運算符包括投影運算Projection
,選擇運算Filter
,連接運算包括嵌套連接運算NestLoopJoin
,散列連接HashJoin
,排序運算Sort
等。
算法的一般策略包括基於排序的,基於散列的,或者基於索引的。
流水化操作與物化
由於查詢的結果集可能會很大,超出緩衝區,同時爲了能夠提高查詢的速度,各運算符都會支持流水化操作。流水化操作要求各運算符都有支持迭代操作,它們之間通過GetNext調用來節點執行的實際順序。迭代器函數包括open,getnext,close3個函數。
設NestLoopJoin
的兩個運算符參數爲R,S,NestLoopJoin
的迭代器函數如下:
如果TableScan,IndexScan,NestLoopJoin
3個運算符都支持迭代器函數。則圖5中的連接NestLoopJoin(t1,t2’)
可表示爲:
phy = Projection(Filter(NestLoopJoin(TableScan(t1),IndexScan(t2’))));
執行物理計劃時:
這種方式下,物理計劃一次返回一行,執行的順序由運算符的函數調用序列來確定。程序只需要1個緩衝區就可以向用戶返回結果集。
也有些情況需要等待所有結果返回才進行下一步運算的,比如Sort , Dist
運算,需要將整個結果集排好序後才能返回,這種情況稱作物化,物化操作通常是在open函數中完成的。
一個完整的例子
接下來以一個例子爲例表示各部分的結構,SQL命令:
SELECT t1.a,t2.b FROM t1,t2 WHERE t1.a = t2.a AND t2.b = '1990';
其對應的分析樹爲:
圖2. SQL例句對應的分析樹
分析樹的葉子節點爲數字,字符串,屬性等,其他爲內部節點。
將圖2的分析樹轉化爲邏輯計劃樹,如圖3所示。
圖3. 圖2分析樹對應的邏輯計劃
邏輯計劃是關係代數的一種體現,關係代數擁有種基本運算符:投影 (π),選擇 (σ),自然連接 (⋈),聚集運算(G)等算子。因此邏輯計劃也擁有這些類型的節點。
邏輯計劃的內部節點是算子,葉子節點是關係,子樹是子表達式。各算子中最耗時的爲連接運算,因此SQL查詢優化的很大一部分工作是減小連接的大小。如圖3對應的邏輯計劃可優化爲圖4所示的邏輯計劃。
圖4. 圖3優化後的邏輯計劃
完成邏輯計劃的優化後,在將邏輯計劃轉化爲物理查詢計劃。圖4的邏輯計劃對應的物理查詢計劃如下:
圖5. 圖4對應的物理查詢計劃
物理查詢計劃針對邏輯計劃中的每一個算子擁有對應的1個或多個運算符,生成物理查詢計劃是基於不同的策略選擇合適的運算符進行運算。其中,關係掃描運算符爲葉子節點,其他運算符爲內部節點。
後記
開源的數據庫代碼中可以下載OceanBase
或者RedBase
。OceanBase
是淘寶的開源數據庫,RedBase是斯坦福大學數據庫系統實現課程的一個開源項目。後面這兩個項目都是較近開始的項目,代碼量較少,結構較清晰,相對簡單易讀,在github上都能找到。但是OceanBase目前SQL解析部分也沒有全部完成,只有DML部分完成;RedBase設計更簡單,不過沒有設計邏輯計劃。
本文中就是參考了RedBase的方式進行解析。
參考文獻:
《數據庫系統實現》
《flex與bison》
歡迎光臨我的網站----蝴蝶忽然的博客園----人既無名的專欄。
如果閱讀本文過程中有任何問題,請聯繫作者,轉載請註明出處!