自己實現一個SQL解析引擎 功能:將用戶輸入的SQL語句序列轉換爲一個可執行的操作序列,並返回查詢的結果集。 SQL的解析引擎包括查詢編譯與查詢優化和查詢的運行,主要包括3個步驟: 查詢分析

自己實現一個SQL解析引擎

功能:將用戶輸入的SQL語句序列轉換爲一個可執行的操作序列,並返回查詢的結果集。
SQL的解析引擎包括查詢編譯與查詢優化和查詢的運行,主要包括3個步驟:

  1. 查詢分析:
  2. 制定邏輯查詢計劃(優化相關)
  3. 制定物理查詢計劃(優化相關)
  • 查詢分析: 將SQL語句表示成某種有用的語法樹.
  • 制定邏輯查詢計劃: 把語法樹轉換成一個關係代數表達式或者類似的結構,這個結構通常稱作邏輯計劃。
  • 制定物理查詢計劃:把邏輯計劃轉換成物理查詢計劃,要求指定操作執行的順序,每一步使用的算法,操作之間的傳遞方式等。
    查詢分析各模塊主要函數間的調用關係: 

    圖1.SQL引擎間模塊的調用關係

FLEX簡介

flex是一個詞法分析工具,其輸入爲後綴爲.l的文件,輸出爲.c的文件. 示例是一個類似Unix的單詞統計程序wc

[cpp] view plain copy
  1. %option noyywrap  
  2. %{  
  3.     int chars = 0;  
  4.     int words = 0;  
  5.     int lines = 0;  
  6. %}  
  7.   
  8. %%  
  9.   
  10. [_a-zA-Z][_a-zA-Z0-9]+ { words++; chars += strlen(yytext); }  
  11. \n { chars++ ; lines++; }  
  12. .  { chars++; }  
  13.   
  14. %%  
  15.   
  16. int main()  
  17. {  
  18.        yylex();  
  19.        printf("%8d %8d %8d\n",lines,words,chars);  
  20.     return 0;  
  21. }  


.l文件通常分爲3部分:

[cpp] view plain copy
  1. %{  
  2.     definition  
  3. %}  
  4.   
  5. %%  
  6.     rules  
  7. %%  
  8.     code  


definition部分爲定義部分,包括引入頭文件,變量聲明,函數聲明,註釋等,這部分會被原樣拷貝到輸出的.c文件中。
rules部分定義詞法規則,使用正則表達式定義詞法,後面大括號內則是掃描到對應詞法時的動作代碼。
code部分爲C語言的代碼。yylex爲flex的函數,使用yylex開始掃描。
%option 指定flex掃描時的一些特性。yywrap通常在多文件掃描時定義使用。常用的一些選項有
noyywrap 不使用yywrap函數
yylineno 使用行號
case-insensitive 正則表達式規則大小寫無關

flex文件的編譯

[cpp] view plain copy
  1. flex  –o wc.c wc.l  
  2.    cc wc.c –o wc  


Bison簡介

Bison作爲一個語法分析器,輸入爲一個.y的文件,輸出爲一個.h文件和一個.c文件。通常Bison需要使用Flex作爲協同的詞法分析器來獲取記號流。Flex識別正則表達式來獲取記號,Bison則分析這些記號基於邏輯規則進行組合
計算器的示例:calc.y

[cpp] view plain copy
  1. %{  
  2. #include <stdio.h>  
  3. %}  
  4.   
  5. %token NUMBER  
  6. %token ADD SUB MUL DIV ABS  
  7. %token OP CP  
  8. %token EOL  
  9.   
  10. %%  
  11.   
  12. calclist:  
  13.     | calclist exp EOL {printf("=%d \n> ",$2);}  
  14.     | calclist EOL {printf("> ");}  
  15.     ;  
  16. exp: factor  
  17.     | exp ADD factor  {$$ = $1 + $3;}  
  18.     | exp SUB factor  {$$ = $1 - $3;}  
  19.     ;  
  20. factor:term  
  21.     | factor MUL term {$$ = $1 * $3;}  
  22.     | factor DIV term {$$ = $1 / $3;}  
  23.     ;  
  24. term:NUMBER  
  25.     | ABS term ABS { $$ = ($2 >= 0 ? $2 : -$2);}  
  26.     | OP exp CP    { $$ = $2;}  
  27.     ;  
  28. %%  
  29. int main(int argc,char *argv[])  
  30. {  
  31.     printf("> ");  
  32.     yyparse();  
  33.   
  34.     return 0;  
  35. }  
  36. void yyerror(char *s)  
  37. {  
  38.     fprintf(stderr,"error:%s:\n",s);  
  39. }  
  40.   
  41. Flex與Bison共享記號,值通過yylval在Flex與Bison間傳遞。對應的.l文件爲  
  42.   
  43. %option noyywrap  
  44. %{  
  45. #include "fb1-5.tab.h"  
  46. #include <string.h>  
  47. %}  
  48.   
  49. %%  
  50. "+" { return ADD;}  
  51. "-" { return SUB;}  
  52. "*" { return MUL;}  
  53. "/" { return DIV;}  
  54. "|" { return ABS;}  
  55. "(" { return OP;}  
  56. ")" { return CP;}  
  57. [0-9]+ {   
  58.                  yylval = atoi(yytext);  
  59.                  return NUMBER;  
  60.            }  
  61.   
  62. \n { return EOL; }  
  63. "//".*  
  64.   
  65. [ \t] {}  
  66. "q" {exit(0);}  
  67. .   { yyerror("invalid char: %c\n;",*yytext); }  
  68. %%  


Bision文件編譯

[cpp] view plain copy
  1. bison -d cacl.y  
  2.   flex cacl.l  
  3.   cc -o cacl cacl.tab.c lex.yy.c  


通常,Bison默認是不可重入的,如果希望在yyparse結束後保留解析的語法樹,可以採用兩種方式,一種是增加一個全局變量,另一種則是設置一個額外參數,其中ParseResult可以是用戶自己定義的結構體。
%parse-param {ParseResult *result}
在規則代碼中可以引用該參數:

[cpp] view plain copy
  1. stmt_list: stmt ';'  { 
    =$1;result>resulttree=
    ; }  
  2. | stmt_list stmt ';' { 
    =(($2!=NULL)?$2:$1);result>resulttree=
    ;}  
  3. stmt_list: stmt ';'  { 
    =$1;result>resulttree=
    ; }  
  4. | stmt_list stmt ';' { 
    =(($2!=NULL)?$2:$1);result>resulttree=
    ;}  


調用yyparse時則爲:
ParseResult p;
yyparse(&p);

SQL解析引擎中的數據結構

語法樹結構

在實現的時候可以把語法樹和邏輯計劃都看成是樹結構和列表結構,而物理計劃更像像是鏈式結構。樹結構要注意區分葉子節點(也叫終止符節點)和非葉子節點(非終止符節點)。同時葉子節點和非葉子節點都可能有多種類型。

語法樹的節點:包含兩個部分,節點的類型的枚舉值kind,表示節點值的聯合體u,聯合體中包含了各個節點所需的字段。

[cpp] view plain copy
  1. typedef struct node{  
  2.    NODEKIND kind;  
  3.   
  4.    union{  
  5.          //...  
  6.            /* query node */  
  7.          struct{  
  8.              int         distinct_opt;  
  9.               struct node *limit;   
  10.               struct node *select_list;  
  11.               struct node *tbl_list;  
  12.               struct node *where_clause;  
  13.               struct node *group_clause;  
  14.               struct node *having_clause;  
  15.               struct node *order_clause;  
  16.          } SELECT;  
  17.          /* delete node */  
  18.         struct{  
  19.             struct node *limit;  
  20.             struct node *table;  
  21.             struct node *where_clause;  
  22.             struct node *group_clause;  
  23.          } DELETE;  
  24. /* relation node */  
  25.           struct{  
  26.                 char * db_name;  
  27.                 char * tbl_name;  
  28.                 char * alias_name;  
  29.           } TABLE;  
  30.         //其他結構體  
  31.    }u;  
  32. }NODE ;  
  33. NODEKIND枚舉了所有可能出現的節點類型.其定義爲  
  34.   
  35. typedef enum NODEKIND{  
  36.     N_MIN,  
  37.     /* const node*/  
  38.     N_INT,    //int or long  
  39.     N_FLOAT,  //float  
  40.     N_STRING, //string  
  41.     N_BOOL,   //true or false or unknown  
  42.     N_NULL,   //null  
  43.     /* var node*/  
  44.     N_COLUMN, // colunm name  
  45.     //其他類型  
  46.     /*stmt node*/      
  47.     N_SELECT,  
  48.     N_INSERT,  
  49.     N_REPLACE,  
  50.     N_DELETE,  
  51.     N_UPDATE,  
  52.     //其他類型  
  53.     N_MAX  
  54. } NODEKIND;  


在語法樹中,分析樹的葉子節點爲數字,字符串,屬性等,其他爲內部節點。因此有些數據庫的實現中將語法樹的節點定義爲如下的ParseNode結構。

[cpp] view plain copy
  1. typedef struct _ParseNode  
  2. {  
  3.   ObItemType   type_;//節點的類型,如T_STRING,T_SELECT等  
  4.   
  5.   /* 終止符節點,具有實際的值 */  
  6.   int64_t      value_;  
  7.   const char*  str_value_;  
  8.   
  9.   /* 非終止符節點,擁有多個孩子 */  
  10.   int32_t      num_child_;//子節點的個數  
  11.   struct _ParseNode** children_;//子節點指針鏈  
  12.   
  13. } ParseNode;  


邏輯計劃結構

邏輯計劃的內部節點是算子,葉子節點是關係.

[cpp] view plain copy
  1. typedef struct plannode{  
  2.   
  3.     PLANNODEKIND kind;  
  4.   
  5.     union{  
  6.         /*stmt node*/  
  7.         struct {  
  8.             struct plannode *plan;  
  9.         }SELECT;  
  10.   
  11.         /*op node*/  
  12.         struct {  
  13.             struct plannode *rel;  
  14.             struct plannode *filters; //list of filter  
  15.         }SCAN;  
  16.         struct {  
  17.             struct plannode *rel;  
  18.             NODE *expr_filter; //list of compare expr  
  19.         }FILTER;  
  20.         struct {  
  21.             struct plannode *rel;  
  22.             NODE *select_list;      
  23.         }PROJECTION;  
  24.         struct {  
  25.             struct plannode *left;  
  26.             struct plannode *right;  
  27.         }JOIN;  
  28.         /*leaf node*/  
  29.         struct {  
  30.             NODE *table;  
  31.         }FILESCAN;  
  32.         //其他類型節點      
  33.     }u;  
  34. }PLANNODE;  


邏輯計劃節點的類型PLANNODEKIND的枚舉值如下:

[cpp] view plain copy
  1. typedef enum PLANNODEKIND{  
  2.     /*stmt node tags*/  
  3.     PLAN_SELECT,  
  4.     PLAN_INSERT,  
  5.     PLAN_DELETE,  
  6.     PLAN_UPDATE,  
  7.     PLAN_REPLACE,  
  8.     /*op node tags*/  
  9.     PLAN_FILESCAN, /* Relation     關係,葉子節點 */  
  10.     PLAN_SCAN,         
  11.     PLAN_FILTER,   /* Selection  選擇   */  
  12.     PLAN_PROJ,     /* Projection 投影*/  
  13.     PLAN_JOIN,     /* Join       連接 ,指等值連接*/  
  14.     PLAN_DIST,     /* Duplicate elimination( Distinct) 消除重複*/  
  15.     PLAN_GROUP,    /* Grouping   分組(包含了聚集)*/  
  16.     PLAN_SORT,     /* Sorting    排序*/  
  17.     PLAN_LIMIT,  
  18.     /*support node tags*/  
  19.     PLAN_LIST      
  20. }PLANNODEKIND;  


物理計劃結構

物理邏輯計劃中關係掃描運算符爲葉子節點,其他運算符爲內部節點。擁有3個迭代器函數open,close,get_next_row。其定義如下:

[cpp] view plain copy
  1. typedef int (*IntFun)(PhyOperator *);  
  2. typedef int (*RowFun)(Row &row,PhyOperator *);  
  3. struct phyoperator{  
  4.     PHYOPNODEKIND kind;  
  5.   
  6.     IntFun open;  
  7.     IntFun close;  
  8.     RowFun get_next_row;//迭代函數  
  9.   
  10.     union{  
  11.         struct {  
  12.             struct phyoperator *inner;  
  13.             struct phyoperator *outter;  
  14.             Row one_row;  
  15.         }NESTLOOPJOIN;  
  16.         struct {  
  17.             struct phyoperator *inner;  
  18.             struct phyoperator *outter;  
  19.         }HASHJOIN;  
  20.         struct {  
  21.             struct phyoperator *inner;  
  22.         }TABLESCAN;  
  23.         struct {  
  24.             struct phyoperator *inner;  
  25.             NODE * expr_filters;  
  26.         }INDEXSCAN;  
  27.         //其他類型的節點  
  28.     }u;  
  29. }PhyOperator;  


物理查詢計劃的節點類型PHYOPNODEKIND枚舉如下:

[cpp] view plain copy
  1. typedef enum PHYOPNODEKIND{  
  2.     /*stmt node tags*/  
  3.     PHY_SELECT,  
  4.     PHY_INSERT,  
  5.     PHY_DELETE,  
  6.     PHY_UPDATE,  
  7.     PHY_REPLACE,  
  8.     /*phyoperator node tags*/  
  9.     PHY_TABLESCAN,  
  10.     PHY_INDEXSCAN,  
  11.     PHY_FILESCAN,  
  12.     PHY_NESTLOOPJOIN,  
  13.     PHY_HASHJOIN,  
  14.     PHY_FILTER,  
  15.     PHY_SORT,  
  16.     PHY_DIST,  
  17.     PHY_GROUP,  
  18.     PHY_PROJECTION,  
  19.     PHY_LIMIT  
  20. }PHYOPNODEKIND;  


節點內存池

可以看到分析樹,邏輯計劃樹和物理查詢樹都是以指針爲主的結構體,如果每次都動態從申請的話,會比較耗時。需要使用內存池的方式,一次性申請多個節點內存,供以後調用。下面是一種簡單的方式,每次創建節點時都使用newnode函數即可。程序結束時再釋放內存池即可。

[cpp] view plain copy
  1. static NODE *nodepool = NULL;  
  2. static int MAXNODE = 256;  
  3. static int nodeptr = 0;  
  4.   
  5. NODE *newnode(NODEKIND kind)  
  6. {  
  7.     //首次使用時申請MAXNODE個節點  
  8.     if(nodepool == NULL){  
  9.         nodepool = (NODE *)malloc(sizeof(NODE)*MAXNODE);  
  10.         assert(nodepool);  
  11.     }  
  12.   
  13.     assert(nodeptr <= MAXNODE);  
  14.     //當節點個數等於MAXNODE時realloc擴展爲原來的兩倍節點  
  15.     if (nodeptr == MAXNODE){  
  16.         MAXNODE *= 2;  
  17.         NODE *newpool =   
  18. (NODE *)realloc(nodepool,sizeof(NODE)*MAXNODE) ;   
  19.         assert(newpool);  
  20.         nodepool = newpool;  
  21.     }  
  22.   
  23.     NODE *n = nodepool + nodeptr;  
  24.     n->kind = kind ;  
  25.     ++nodeptr;  
  26.   
  27.     return n;  
  28. }  


查詢分析

查詢分析需要對查詢語句進行詞法分析和語法分析,構建語法樹。詞法分析是指識別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 單表語法爲例

[sql] view plain copy
  1. DELETE  [IGNORE] [FIRST|LAST row_count]   
  2. FROM tbl_name   
  3. [WHERE where_definition]    
  4. [ORDER BY ...]  


用Bison可以表示爲:

[plain] view plain copy
  1. delete_stmt:DELETE opt_ignore opt_first FROM table_ident opt_where opt_groupby   
  2. {  
  3.            $$ = delete_node(N_DELETE,$3,$5,$6,$7);  
  4. }    
  5. ;  
  6. opt_ignore:/*empty*/  
  7.             | IGNORE  
  8. ;  
  9.   
  10. opt_first: /* empty */{ $$ = NULL;}  
  11. | FIRST INTNUM { $$ = limit_node(N_LIMIT,0,$2);}  
  12. | LAST INTNUM { $$ = limit_node(N_LIMIT,1,$2);}  
  13. ;  


然後在把opt_where,opt_groupbytable_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的情況下,原子表達式實際上就是一個比較關係表達式,其節點爲列名,數字,字符串,可以將原子表達式定義爲

[cpp] view plain copy
  1. struct CompExpr  
  2. {  
  3.     NODE * attr_or_value;  
  4.     NODE * attr_or_value;  
  5.     CompOpType kind;  
  6. };  


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的迭代器函數如下:

[cpp] view plain copy
  1. void NestLoopJoin::Open()  
  2. {  
  3.     R.Open();  
  4.     S.Open();  
  5.     r =R.GetNext();  
  6. }  
  7. void NestLoopJoin::GetNext(tuple &t)  
  8. {  
  9.     Row r,s;  
  10.     S.GetNext(s);  
  11.     if(s.empty()){  
  12.         S.Close();  
  13.         R.GetNext(r);  
  14.         if(r.empty())  
  15.             return;  
  16.         S.Open();  
  17.         S.GetNext(s);  
  18.     }  
  19.     t = join(r,s)  
  20. }  
  21. void NestLoopJoin::Close()  
  22. {  
  23.         R.Close();  
  24.         S.Close();  
  25. }  


如果TableScan,IndexScan,NestLoopJoin 3個運算符都支持迭代器函數。則圖5中的連接NestLoopJoin(t1,t2’)可表示爲:
phy = Projection(Filter(NestLoopJoin(TableScan(t1),IndexScan(t2’))));

執行物理計劃時:

[cpp] view plain copy
  1. phy.Open();  
  2.     while(!tuple.empty()){  
  3.         phy.GetNext(tuple);  
  4.     }  
  5.     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或者RedBaseOceanBase 是淘寶的開源數據庫,RedBase是斯坦福大學數據庫系統實現課程的一個開源項目。後面這兩個項目都是較近開始的項目,代碼量較少,結構較清晰,相對簡單易讀,在github上都能找到。但是OceanBase目前SQL解析部分也沒有全部完成,只有DML部分完成;RedBase設計更簡單,不過沒有設計邏輯計劃。
本文中就是參考了RedBase的方式進行解析。

參考文獻:

《數據庫系統實現》
《flex與bison》


歡迎光臨我的網站----蝴蝶忽然的博客園----人既無名的專欄
如果閱讀本文過程中有任何問題,請聯繫作者,轉載請註明出處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章