自己實現一個SQL解析引擎
功能:將用戶輸入的SQL語句序列轉換為一個可運行的操作序列,並返回查詢的結果集。
SQL的解析引擎包含查詢編譯與查詢優化和查詢的執行,主要包含3個步驟:
- 查詢分析:
- 制定邏輯查詢計划(優化相關)
- 制定物理查詢計划(優化相關)
- 查詢分析: 將SQL語句表示成某種實用的語法樹.
- 制定邏輯查詢計划: 把語法樹轉換成一個關系代數表達式或者類似的結構,這個結構通常稱作邏輯計划。
- 制定物理查詢計划:把邏輯計划轉換成物理查詢計划,要求指定操作運行的順序,每一步使用的算法,操作之間的傳遞方式等。
查詢分析各模塊主要函數間的調用關系:
圖1.SQL引擎間模塊的調用關系
FLEX簡單介紹
flex是一個詞法分析工具,其輸入為后綴為.l的文件,輸出為.c的文件. 演示樣例是一個類似Unix的單詞統計程序wc
。
%option noyywrap
%{ int chars = 0; int words = 0; int lines = 0; %}
%% [_a-zA-Z][_a-zA-Z0-9]+ { words++; chars += strlen(yytext); } \n { chars++ ; lines++; } . { chars++; } %%
int main()
{
yylex();
printf("%8d %8d %8d\n",lines,words,chars);
return 0;
}
.l文件通常分為3部分:
%{ definition %}
%% rules %%
code
definition
部分為定義部分,包含引入頭文件,變量聲明,函數聲明,凝視等,這部分會被原樣復制到輸出的.c文件里。
rules
部分定義詞法規則,使用正則表達式定義詞法,后面大括號內則是掃描到相應詞法時的動作代碼。
code
部分為C語言的代碼。yylex
為flex的函數,使用yylex
開始掃描。
%option
指定flex掃描時的一些特性。yywrap
通常在多文件掃描時定義使用。經常使用的一些選項有
noyywrap
不使用yywrap函數
yylineno
使用行號
case-insensitive
正則表達式規則大寫和小寫無關
flex文件的編譯
flex –o wc.c wc.l
cc wc.c –o wc
Bison簡單介紹
Bison
作為一個語法分析器,輸入為一個.y的文件,輸出為一個.h文件和一個.c文件。通常Bison須要使用Flex作為協同的詞法分析器來獲取記號流。Flex識別正則表達式來獲取記號,Bison則分析這些記號基於邏輯規則進行組合。
計算器的演示樣例:calc.y
%{
#include <stdio.h>
%}
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL
%%
calclist:
| calclist exp EOL {printf("=%d \n> ",$2);}
| calclist EOL {printf("> ");}
;
exp: factor
| exp ADD factor {$$ = $1 + $3;}
| exp SUB factor {$$ = $1 - $3;}
;
factor:term
| factor MUL term {$$ = $1 * $3;}
| factor DIV term {$$ = $1 / $3;}
;
term:NUMBER
| ABS term ABS { $$ = ($2 >= 0 ? $2 : -$2);}
| OP exp CP { $$ = $2;}
;
%%
int main(int argc,char *argv[])
{
printf("> ");
yyparse();
return 0;
}
void yyerror(char *s)
{
fprintf(stderr,"error:%s:\n",s);
}
Flex與Bison共享記號,值通過yylval在Flex與Bison間傳遞。相應的.l文件為
%option noyywrap
%{
#include "fb1-5.tab.h"
#include <string.h>
%}
%%
"+" { return ADD;}
"-" { return SUB;}
"*" { return MUL;} "/" { return DIV;} "|" { return ABS;} "(" { return OP;} ")" { return CP;} [0-9]+ { yylval = atoi(yytext); return NUMBER; } \n { return EOL; } "//".* [ \t] {} "q" {exit(0);} . { yyerror("invalid char: %c\n;",*yytext); } %%
Bision文件編譯
bison -d cacl.y
flex cacl.l
cc -o cacl cacl.tab.c lex.yy.c
通常,Bison默認是不可重入的,假設希望在yyparse
結束后保留解析的語法樹,能夠採用兩種方式,一種是添加一個全局變量,還有一種則是設置一個額外參數,當中ParseResult能夠是用戶自定義的結構體。
%parse-param {ParseResult *result}
在規則代碼中能夠引用該參數:
stmt_list: stmt ';' { $$ = $1; result->result_tree = $$; }
| stmt_list stmt ';' { $$ = (($2 != NULL)? $2 : $1); result->result_tree = $$;}
調用yyparse時則為:
ParseResult p;
yyparse(&p);
SQL解析引擎中的數據結構
語法樹結構
在實現的時候能夠把語法樹和邏輯計划都看成是樹結構和列表結構,而物理計划更像像是鏈式結構。樹結構要注意區分葉子節點(也叫終止符節點)和非葉子節點(非終止符節點)。同一時候葉子節點和非葉子節點都可能有多種類型。
語法樹的節點:包括兩個部分,節點的類型的枚舉值kind,表示節點值的聯合體u,聯合體中包括了各個節點所需的字段。
typedef struct node{
NODEKIND kind;
union{
//...
/* query node */
struct{
int distinct_opt;
struct node *limit;
struct node *select_list;
struct node *tbl_list;
struct node *where_clause;
struct node *group_clause;
struct node *having_clause;
struct node *order_clause;
} SELECT;
/* delete node */
struct{
struct node *limit;
struct node *table;
struct node *where_clause;
struct node *group_clause;
} DELETE;
/* relation node */
struct{
char * db_name;
char * tbl_name;
char * alias_name;
} TABLE;
//其它結構體
}u;
}NODE ;
NODEKIND枚舉了全部可能出現的節點類型.其定義為
typedef enum NODEKIND{
N_MIN,
/* const node*/
N_INT, //int or long
N_FLOAT, //float
N_STRING, //string
N_BOOL, //true or false or unknown
N_NULL, //null
/* var node*/
N_COLUMN, // colunm name
//其它類型
/*stmt node*/
N_SELECT,
N_INSERT,
N_REPLACE,
N_DELETE,
N_UPDATE,
//其它類型
N_MAX
} NODEKIND;
在語法樹中,分析樹的葉子節點為數字,字符串,屬性等,其它為內部節點。因此有些數據庫的實現中將語法樹的節點定義為例如以下的ParseNode結構。
typedef struct _ParseNode
{
ObItemType type_;//節點的類型,如T_STRING,T_SELECT等
/* 終止符節點,具有實際的值 */
int64_t value_;
const char* str_value_;
/* 非終止符節點,擁有多個孩子 */
int32_t num_child_;//子節點的個數
struct _ParseNode** children_;//子節點指針鏈
} ParseNode;
邏輯計划結構
邏輯計划的內部節點是算子,葉子節點是關系.
typedef struct plannode{
PLANNODEKIND kind;
union{
/*stmt node*/
struct {
struct plannode *plan;
}SELECT;
/*op node*/
struct {
struct plannode *rel;
struct plannode *filters; //list of filter
}SCAN;
struct {
struct plannode *rel;
NODE *expr_filter; //list of compare expr
}FILTER;
struct {
struct plannode *rel;
NODE *select_list;
}PROJECTION;
struct {
struct plannode *left;
struct plannode *right;
}JOIN;
/*leaf node*/
struct {
NODE *table;
}FILESCAN;
//其它類型節點
}u;
}PLANNODE;
邏輯計划節點的類型PLANNODEKIND的枚舉值例如以下:
typedef enum PLANNODEKIND{
/*stmt node tags*/
PLAN_SELECT,
PLAN_INSERT,
PLAN_DELETE,
PLAN_UPDATE,
PLAN_REPLACE,
/*op node tags*/
PLAN_FILESCAN, /* Relation 關系,葉子節點 */
PLAN_SCAN,
PLAN_FILTER, /* Selection 選擇 */
PLAN_PROJ, /* Projection 投影*/
PLAN_JOIN, /* Join 連接 ,指等值連接*/
PLAN_DIST, /* Duplicate elimination( Distinct) 消除反復*/
PLAN_GROUP, /* Grouping 分組(包括了聚集)*/
PLAN_SORT, /* Sorting 排序*/
PLAN_LIMIT,
/*support node tags*/
PLAN_LIST
}PLANNODEKIND;
物理計划結構
物理邏輯計划中關系掃描運算符為葉子節點,其它運算符為內部節點。擁有3個迭代器函數open,close,get_next_row。其定義例如以下:
typedef int (*IntFun)(PhyOperator *);
typedef int (*RowFun)(Row &row,PhyOperator *);
struct phyoperator{
PHYOPNODEKIND kind;
IntFun open;
IntFun close;
RowFun get_next_row;//迭代函數
union{
struct {
struct phyoperator *inner;
struct phyoperator *outter;
Row one_row;
}NESTLOOPJOIN;
struct {
struct phyoperator *inner;
struct phyoperator *outter;
}HASHJOIN;
struct {
struct phyoperator *inner;
}TABLESCAN;
struct {
struct phyoperator *inner;
NODE * expr_filters;
}INDEXSCAN;
//其它類型的節點
}u;
}PhyOperator;
物理查詢計划的節點類型PHYOPNODEKIND枚舉例如以下:
typedef enum PHYOPNODEKIND{
/*stmt node tags*/
PHY_SELECT,
PHY_INSERT,
PHY_DELETE,
PHY_UPDATE,
PHY_REPLACE,
/*phyoperator node tags*/
PHY_TABLESCAN,
PHY_INDEXSCAN,
PHY_FILESCAN,
PHY_NESTLOOPJOIN,
PHY_HASHJOIN,
PHY_FILTER,
PHY_SORT,
PHY_DIST,
PHY_GROUP,
PHY_PROJECTION,
PHY_LIMIT
}PHYOPNODEKIND;
節點內存池
能夠看到分析樹,邏輯計划樹和物理查詢樹都是以指針為主的結構體,假設每次都動態從申請的話,會比較耗時。須要使用內存池的方式,一次性申請多個節點內存,供以后調用。以下是一種簡單的方式,每次創建節點時都使用newnode函數就可以。程序結束時再釋放內存池就可以。
static NODE *nodepool = NULL;
static int MAXNODE = 256;
static int nodeptr = 0;
NODE *newnode(NODEKIND kind)
{
//首次使用時申請MAXNODE個節點
if(nodepool == NULL){
nodepool = (NODE *)malloc(sizeof(NODE)*MAXNODE);
assert(nodepool);
}
assert(nodeptr <= MAXNODE);
//當節點個數等於MAXNODE時realloc擴展為原來的兩倍節點
if (nodeptr == MAXNODE){
MAXNODE *= 2;
NODE *newpool =
(NODE *)realloc(nodepool,sizeof(NODE)*MAXNODE) ;
assert(newpool);
nodepool = newpool;
}
NODE *n = nodepool + nodeptr;
n->kind = kind ;
++nodeptr;
return n;
}
查詢分析
查詢分析須要對查詢語句進行詞法分析和語法分析,構建語法樹。詞法分析是指識別SQL語句中的有意義的邏輯單元,如keyword(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 單表語法為例
DELETE [IGNORE] [FIRST|LAST row_count]
FROM tbl_name
[WHERE where_definition]
[ORDER BY ...]
用Bison能夠表示為:
delete_stmt:DELETE opt_ignore opt_first FROM table_ident opt_where opt_groupby
{
$$ = delete_node(N_DELETE,$3,$5,$6,$7);
}
;
opt_ignore:/*empty*/
| IGNORE
;
opt_first: /* empty */{ $$ = NULL;}
| FIRST INTNUM { $$ = limit_node(N_LIMIT,0,$2);}
| LAST INTNUM { $$ = limit_node(N_LIMIT,1,$2);}
;
然后在把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的情況下,原子表達式實際上就是一個比較關系表達式,其節點為列名,數字,字符串,能夠將原子表達式定義為
struct CompExpr
{
NODE * attr_or_value;
NODE * attr_or_value;
CompOpType kind;
};
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
的迭代器函數例如以下:
void NestLoopJoin::Open()
{
R.Open();
S.Open();
r =R.GetNext();
}
void NestLoopJoin::GetNext(tuple &t)
{
Row r,s;
S.GetNext(s);
if(s.empty()){
S.Close();
R.GetNext(r);
if(r.empty())
return;
S.Open();
S.GetNext(s);
}
t = join(r,s)
}
void NestLoopJoin::Close()
{
R.Close();
S.Close();
}
假設TableScan,IndexScan,NestLoopJoin
3個運算符都支持迭代器函數。則圖5中的連接NestLoopJoin(t1,t2’)
可表示為:
phy = Projection(Filter(NestLoopJoin(TableScan(t1),IndexScan(t2’))));
運行物理計划時:
phy.Open();
while(!tuple.empty()){
phy.GetNext(tuple);
}
phy.Close();
這樣的方式下,物理計划一次返回一行,運行的順序由運算符的函數調用序列來確定。程序僅僅須要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》
歡迎光臨我的站點----蝴蝶忽然的博客園----人既無名的專欄。
假設閱讀本文過程中有不論什么問題,請聯系作者,轉載請注明出處!