OceanBase是阿里巴巴集团自主研发的可扩展的关系型数据库,实现了跨行跨表的事务,支持数千亿条记录、数百TB数据上的SQL操作。在阿里巴巴集团下,OceanBase数据库支持了多个重要业务的数据存储。包含收藏夹、直通车报表、天猫评价等。
截止到2013年4月份。OceanBase线上业务的数据量已经超过一千亿条。
看起来挺厉害的,今天我们来研究下它的源码。
关于OceanBase的架构描写叙述有非常多文档。这篇笔记也不打算涉及这些东西,仅仅讨论OceanBase的SQL编译部分的代码。
OceanBase是一个开源的数据库,托管在github上,点击下载。本文讨论的源代码路径相应为:oceanbase_0.3/src/sql。最新的版本号为0.4,本文讨论的代码基于0.3版本号的OceanBase.眼下OceanBase的能解析的SQL还比較少,包含Select,Insert,Update,Delete,Show,Explain等.
选择OceanBase 0.3 版本号进行学习。基于几个原因:
- OceanBase 的高质量。高可读性
- OceanBase 版本号低,没有历史负担
- OceanBase SQL解析相对简单。更easy窥见全貌。利于理解设计开发中要解决的主要问题。
- 与其它数据库的SQL解析部分进行对照,深入理解问题本质
该部分主要功能包含了。SQL语句解析,逻辑计划的生成。物理操作符运算等。
入口:ObSql类
本部分的入口函数在ob_sql.h中,调用函数ObSql::direct_execute能够直接运行SQL语句,并返回结果集ObResultSet。
函数stmt_prepare用于解析要预编译的SQL语句,stmt_execute则用于运行Prepare过的SQL语句。
class ObSql { public: ObSql(){} ~ObSql(){} int direct_execute(const common::ObString &stmt, ObResultSet &result) int stmt_prepare(const common::ObString &stmt, ObStmtPrepareResult &result); int stmt_execute(const uint64_t stmt_id, const common::ObArray<common::ObObj> params, ObResultSet &result); int stmt_close(const uint64_t stmt_id); };
在0.4版本号中。direct_execute,stmt_prepare,stmt_execute等函数都被声明为static函数,意味着调用SQL语句运行时能够直接ObSql::direct_execute能够运行SQL语句,而不必再先定义一个ObSql对象。OceanBase还有年轻。还存在不足,我们阅读源代码时应该带着批判思考的精神。
直接进入direct_execute函数,能够看到整个运行的过程,函数中有许多的if,else语句。主要是由于OceanBase有一个编码规范要求:一个函数仅仅能有一个返回出口.按单出口的规范写代码会使得写代码的思路很清晰,不easy出现内存泄露等问题。在大型项目中还是应该尽量保持函数单出口.当然,我认为保持一个函数功能简洁、简单易懂也是很重要的。
在你阅读源代码的过程中,遇到的大部分函数都会是这个样.刨去其它干扰信息,结合凝视,能够看到,SQL运行分为5个步骤:
- 初始化
parse_init(&parse_res)
- 解析SQL语法树
parse_sql(&parse_res, stmt.ptr(), static_cast<size_t>(stmt.length()));
- 制定逻辑计划
resolve(&logical_plan, parse_res.result_tree_)
ObMultiPlan* multi_plan = static_cast<ObMultiPlan*>(logical_plan.plan_tree_);
- 生成物理计划:
trans.add_logical_plans(multi_plan);
physical_plan = trans.get_physical_plan(0)
- 运行物理计划:
exec_plan->open()
初始化不过初始化一个缓冲区,能够略过来研究后面关键的4步。
解析SQL语法树
像PostgreSQL,MySQl等一样,OceanBase採用lex和yacc系的词法和语法解析工具生成语法树。
GNU下的词法和语法工具为Flex和Bison.Flex利用正則表達式识别SQL语句中的全部词语,Bison则依据类似BNF的语法规则识别SQL语义,从而构建语法树。
不熟悉Flex与Bison的同学推荐阅读《FLEX与BISON》(貌似我也没找到其它类似的书籍,^_^),里面也有专门的一章讲述利用Flex与Bison实现一个简单的SQL分析器。
OceanBase的SQL语法树与PostgreSQL更为相似。可是设计上也有非常多差别。
节点设计
语法树由一系列的节点串连而成。我选取较为简单的Update语句作为演示样例,以下是一个例句:
Update student set sex="M" where name="小明";
其SQL语法树能够表示为:
|--Update Stmt
|--Table:student
|--TargeList:
|--sex = "M"
|--Qualifications:
|--name="小明"
语法解析的作用就是怎样用数据结构来表示上面这棵语法树。不同的数据库有不同的方案。为加强对照,我选取了PostgreSQL,RedBase与OceanBase作为參照。
PostgreSQL语法树的节点设计
PostgreSQL中每一种语法都会有一个相应的结构体,比方更新语句Update相应的结构体为UpdateStmt:
typedef struct UpdateStmt { NodeTag type; /* 枚举类型 */ RangeVar *relation; /* 要更新的关系 */ List *targetList; /* 要更新的项 */ Node *whereClause; /* 更新条件 */ List *fromClause; /* optional from clause for more tables */ List *returningList; /* 返回的表达式列表 */ WithClause *withClause; /* WITH clause */ } UpdateStmt;
当中type是个枚举值,表示结构体的类型,在UpdateStmt中为T_UpdateStmt。
其它字段分别相应UPdate语句的各个部分,该结构体能够支持更复杂的Update语句。
PostgreSQL中另一个基础的结构体:
typedef struct Node { NodeTag type; } Node;
用于语法解析的结构体都能够强制转换成Node * 类型。PostgreSQL中传递语法结构体是都会转化成Node *类型。仅仅有在须要明白类型的时候依据type枚举值转换成须要的类型。
Node *的作用有的类似于void * ,可是更利于调试。我们也能够简单的觉得:诸如UpdateStmt的语法解析结构体们都继承自Node。
因为每一个语法相应一个结构体,因此在PostgreSQL中存在非常多类似的结构体,包含SelectStmt,InsertStmt,DeleteStmt等。
终于这些结构体还会被统一转换成Query结构体。即Query是统一的语法树结构体。
在PostgreSQL中。演示样例中的SQL语法树可表示为:
|--UpdateStmt
|--type: T_UpdateStmt
|--relation: student
|--targetList:
|--targest[0]:
|--name: sex
|--val: "M"
|--whereClause:
|--expr: =
|--left: name
|--right: "小明"
RedBase的语法树的节点设计
RedBase是斯坦福的数据库系统实现这门课程(cs346)的一个项目。RedBase比起PostgreSQL,OceanBase这种复杂数据库而言,十分的简单。可是其语法树的节点设计与其它数据库不同,因此提出来做对照。
typedef struct node{ NODEKIND kind;/*枚举类型*/ union{ /* SM component nodes */ /* create table node */ struct{ char *relname; struct node *attrlist; } CREATETABLE; /*此处省略n多个结构体...*/ /* QL component nodes */ /* query node */ ... /* update node */ struct{ char *relname; /* 关系名 */ struct node *relattr; /* 属性 */ struct node *relorvalue; /* 改动后值 */ struct node *conditionlist; /* 条件列表 */ } UPDATE; /*此处省略n多个结构体...*/ } u; } NODE;
RedBase数据库的语法树结构体仅仅有一个,就是NODE,可是这个NODE结构体的声明有150多行(^-^).NODE包含一个枚举类型,作用于PostgreSQL中的type一样。全部的语法结构如UPDATE,SELECT,CREATETABLE等构成巨大的联合体。针对Update语句的结构体包含了关系名,属性,改动后的值,条件列表等字段,显然这样的设计仅仅能支持简单的Update语句。
RedBase採用“巨型”联合体代替PostgreSQL中的多个结构体,免去了类型转换(语法结构体到Node*的转换)。
假设把PostgreSQL语法树节点看成是“继承”结构,那么RedBase的语法树节点能够看成是“组合”结构。
在RedBase中,演示样例中的SQL语法树可表示为:
|--NODE:
|--kind: N_UPDATE
|--u:UPDATE
|--relname: student
|--relattr:
|--kind: N_RELATTR
|--u:RELATTR
|--relname: (null)
|--attrname: sex
|--relorvalue:
|--kind: N_RELATTR_OR_VALUE
|--u:RELATTR_OR_VALUE
|--relattr: (null)
|--value:
|--kind:N_VALUE
|--u:VALUE
|--sval = "M"
|--conditionlist:
|--kind:N_LIST
|--u: LIST
|--next: (null)
|--curr:
|--kind: N_CONDITION
|--u: CONDITION
|--lhsRelattr:
|--kind: N_RELATTR
|--u:RELATTR
|--relname: (null)
|--attrname: name
|--op:
|--kind: N_EQ
|--rhsRelattr:(null)
|--rhsValue:
|--kind:N_VALUE
|--u:VALUE
|--sval = "M"
OceanBase的语法树的节点设计
OceanBase 的语法树节点结构体也仅仅有一个,该结构体包含一个枚举类型变量type_,和PostgreSQL与RedBase一样,代表该结构体相应的类型。还有两组属性,相应终止符节点,仅仅能使用vakue_和str_value_两个字段,分别相应64位整形值和字符串值;非终止符节点使用最后两个字段,num_child_表示子节点的个数,children_指向子节点数组的首地址。
typedef struct _ParseNode { ObItemType type_; /* 终止符节点的真实的值 */ int64_t value_; const char* str_value_; /* 非终止符节点的孩子节点*/ int32_t num_child_; /*孩子节点的个数*/ struct _ParseNode** children_; // BuildPlanFunc m_fnBuildPlan; } ParseNode;
相应一个节点而言,要么是终止符节点要么是非终止符节点,它仅仅会使用两组属性中的一组。int,long,float,double,string等都是终止符类型,能够看出int,long都是用64位整形int64表示。float,double,string则用char *字符串表示。终止符的num_child_为0,children_为null.
PostgreSQL的子节点都是有名字的子节点。能够使用名字进行訪问,如在PostgreSQL中。Update语句的where条件能够通过
updatestmt.whereClause
来訪问 。 但在OceanBase中不行 。 全部的子节点都是匿名的 , 仅仅能通过下标来訪问。
打个例如,在PostgreSQL和RedBase中。孩子是有名字的,能够叫小明、小红等。依据名字你大概能够知道这个孩子是男是女;可是在OceanBase家,他们分别叫老大。老二,老三,听名字全然听不出是男是女的。
OceanBase家有点不讲究^-^。
能够在执行时查看语法树的结构。也能够在代码中能够推各个子节点代表的类型。可是不如PostgreSQL和RedBase方便。在sql_parser.y文件里,定义了SQL的语法规则。同一时候也规定了各种类型的子节点的结构。
update_stmt: UPDATE relation_factor SET update_asgn_list opt_where { ParseNode* assign_list = merge_tree(result->malloc_pool_, T_ASSIGN_LIST, $4); $$ = new_non_terminal_node(result->malloc_pool_, T_UPDATE, 3, $2, assign_list, $5); } ;
从上述代码能够看出,Update语法结构体中有3个子节点。第一个表示表名,第二个表示要更新列表项,第三个表示更新的条件语句。
演示样例中的Update语句在OceanBase中能够表示为例如以下形式:
|--ParseNode
|--type: T_UPDATE
|--num_child: 3
|--children[0]:
|--type: T_IDENT
|--str_value: student
|--children[1]:
|--type: T_ASSIGN_LIST
|--num_child:1
|--children[0]:
|--type: T_ASSIGN_ITEM
|--children[0]:
|--type: T_IDENT
|--str_value: sex
|children[1]:
|--type: T_EXPR
|--children[0]:
|--type: T_STRING
|--str_value: "M"
|--children[2]:
|--type: T_OP_EQ
|--num_child: 2
|--children[0]:
|--type: T_IDENT
|--str_value: name
|--children[1]:
|--type: T_IDENT
|--str_value: "小明"
OceanBase中採用的这样的方式缺点非常明显,就是使用这些结构体时必需要细致辨别各个子节点代表的意义。否则easy出错。长处相同也非常明显,能够非常灵活的构建出语法树。
语法树的节点的设计,主要是为了解决怎样表达语法结构。不同的数据库有不同的详细实现。
OceanBase採用终止符和非终止符分类,使用OceanBase的设计极具灵活性,但使用时须要细致验证。避免出错。
构建语法树
SQL的语法规则非常多 。 SELECT , INSERT , UPDATE , DELETE , CREATE TABLE 等 dml , ddl 等语句都有相应的语法树。
在上一节节中我们已经看到了UPDATE语句在内存中的语法树形状。本节须要些Flex和Bison的基础知识,假设之前没有接触过Flex与Bison的话,能够阅读《Flex与Bison中文版》或者GNU
Bison 2.1 中文手冊。
SQL全称为结构化查询语言,有独立于各数据库厂家的SQL标准,各数据库基本上都会大体上遵循该标准。
像PostgreSQL数据库兼容某些版本号的SQL标准,同一时候也有些自己独有的语句。每一个数据库都有自己的擅长之处,这些细微的差别有时候就体如今查询语言的细微差别上。
OceanBase在0.3版本号仅支持有限的几条SQL语句,依照其官方的介绍。其目标之中的一个是兼容MySQL的语法,让我们拭目以待。
词法分析
利用Flex进行词法分析是构建语法树的第一个。Flex源文件包括3部分:选项定义、词法规则、代码部分。
选项定义部分定义了该词法分析器使用的特性和一些自己定义的函数。词法规则部分为匹配语法+匹配动作。
匹配语法为正則表達式,flex从上往下按顺序对每一个规则进行匹配。一旦匹配成功则运行该项后面大括号内相应的动作代码;代码部分则是C语言代码。
OceanBase的词法文件为sql_parser.l。
当中定义了一些函数,选取部分来看。
以下这两个函数能够使得OceanBase支持转义字符,包含:,f, , , .
inline unsigned char escaped_char(unsigned char c); /* quote_type: 0 - single quotes; 1 - double quotation marks */ int64_t parse_string(const char* src, char* dest, int64_t len, int quote_type);
OceanBase中的字符串用双引號括起来的表示。并且须要进行转义处理。
"(\.|""|[^"\ ])*" { ParseNode* node = NULL; malloc_new_node(node, ((ParseResult*)yyextra)->malloc_pool_, T_IDENT, 0); yylval->node = node; char* src = yytext+1; int len = strlen(src) - 1; //remove last quote charactor char* dest = (char*) parse_malloc(len + 1, ((ParseResult*)yyextra)->malloc_pool_); check_value(dest); node->str_value_ = dest; node->value_ = parse_string(src, dest, len, 1);/*字符串转义处理*/ return NAME; }
当匹配到NULL时,返回的值类型不能为NULL,由于NULL是c语言的保留字,所以须要换成其它名字。此处将NULL的类型定义为NULLX。
NULL { /* yylval->node = new_node(((ParseResult*)yyextra)->malloc_pool_, T_NULL, 0); */ malloc_new_node(yylval->node, ((ParseResult*)yyextra)->malloc_pool_, T_NULL, 0); return NULLX; }
再来看关系符号的规则代码。每一个符号分别返回一个值。
"||" {return CNNOP;} "=" {return COMP_EQ;} ">=" {return COMP_GE;} ">" {return COMP_GT;} "<=" {return COMP_LE;} "<" {return COMP_LT;} "!="|"<>" {return COMP_NE;}
在《Flex与Bison》一书中。作者让全部关系符号返回同一个值。通过yylval的成员值来标记不同符号。
代码类型例如以下这段。
"=" { yylval.subtok = EXPR_EQ; return COMPARISON; } "<=>" { yylval.subtok = EXPR_COMP; return COMPARISON; } ">=" { yylval.subtok = EXPR_GE; return COMPARISON; } ">" { yylval.subtok = EXPR_GT; return COMPARISON; } "<=" { yylval.subtok = EXPR_LE; return COMPARISON; } "<" { yylval.subtok = EXPR_LT; return COMPARISON; } "!=" | "<>" { yylval.subtok = EXPR_NE; return COMPARISON; }
尽管都能完毕同样的功能,可是从个人喜好来讲,我更喜欢OceanBase这样的做法,由于够直接。
整数的值保留在node->value_中返回,为long long 类型 ( 64位 ) 。 而浮点数的值字面值则保存在node->str_value_ 中。
接下来。看日期时间的提取。
Date{whitespace}?'[0-9]{4}(-[0-9]{2}){2}' { int year, month, day; struct tm time; int ret = 0; /* ParseNode* node = new_node(((ParseResult*)yyextra)->malloc_pool_, T_DATE, 0); */ ParseNode* node = NULL; malloc_new_node(node, ((ParseResult*)yyextra)->malloc_pool_, T_DATE, 0); char* dest = strchr(yytext, '''); dest = parse_strdup(dest + 1, ((ParseResult*)yyextra)->malloc_pool_); // skip left quote check_value(dest); size_t len = strlen(dest); dest[len - 1] = '