功能:将用户输入的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》
欢迎光临个人站点----蝴蝶突然的博客园----人既无名的专栏。
假设阅读本文过程当中有不论什么问题,请联系做者,转载请注明出处!