功能:將用戶輸入的SQL語句序列轉換爲一個可運行的操做序列,並返回查詢的結果集。
SQL的解析引擎包含查詢編譯與查詢優化和查詢的執行,主要包含3個步驟:node
flex是一個詞法分析工具,其輸入爲後綴爲.l的文件,輸出爲.c的文件. 演示樣例是一個相似Unix的單詞統計程序wc
。git
%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部分:github
%{ definition %} %% rules %% code
definition
部分爲定義部分,包含引入頭文件,變量聲明,函數聲明,凝視等,這部分會被原樣複製到輸出的.c文件裏。
rules
部分定義詞法規則,使用正則表達式定義詞法,後面大括號內則是掃描到相應詞法時的動做代碼。
code
部分爲C語言的代碼。yylex
爲flex的函數,使用yylex
開始掃描。
%option
指定flex掃描時的一些特性。yywrap
一般在多文件掃描時定義使用。常用的一些選項有
noyywrap
不使用yywrap函數
yylineno
使用行號
case-insensitive
正則表達式規則大寫和小寫無關算法
flex文件的編譯sql
flex –o wc.c wc.l
cc wc.c –o wc
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}
在規則代碼中可以引用該參數:app
stmt_list: stmt ';' { $$ = $1; result->result_tree = $$; } | stmt_list stmt ';' { $$ = (($2 != NULL)? $2 : $1); result->result_tree = $$;}
調用yyparse時則爲:
ParseResult p;
yyparse(&p);
ide
在實現的時候可以把語法樹和邏輯計劃都當作是樹結構和列表結構,而物理計劃更像像是鏈式結構。樹結構要注意區分葉子節點(也叫終止符節點)和非葉子節點(非終止符節點)。同一時候葉子節點和非葉子節點均可能有多種類型。函數
語法樹的節點:包括兩個部分,節點的類型的枚舉值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語法書寫 |
熟悉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》
歡迎光臨個人站點----蝴蝶突然的博客園----人既無名的專欄。
假設閱讀本文過程當中有不論什麼問題,請聯繫做者,轉載請註明出處!