首页 > 代码库 > 淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树

淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树


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解析相对简单,更容易窥见全貌,利于理解设计开发中要解决的主要问题。
  • 与其他数据库的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有一个编码规范要求:一个函数只能有一个返回出口.按单出口的规范写代码会使得写代码的思路非常清晰,不容易出现内存泄露等问题,在大型项目中还是应该尽量保持函数单出口.当然,我觉得保持一个函数功能简洁、简单易懂也是非常重要的。

在你阅读源码的过程中,遇到的大部分函数都会是这个样.刨去其他干扰信息,结合注释,可以看到,SQL执行分为5个步骤:

  1. 初始化
    parse_init(&parse_res)
  2. 解析SQL语法树
    parse_sql(&parse_res, stmt.ptr(), static_cast<size_t>(stmt.length()));
  3. 制定逻辑计划
    resolve(&logical_plan, parse_res.result_tree_)
    ObMultiPlan* multi_plan = static_cast<ObMultiPlan*>(logical_plan.plan_tree_);
  4. 生成物理计划:
    trans.add_logical_plans(multi_plan);
    physical_plan = trans.get_physical_plan(0)
  5. 执行物理计划:
    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中采用的这种方式缺点很明显,就是使用这些结构体时必须要仔细辨别各个子节点代表的意义,否则容易出错。优点同样也很明显,可以非常灵活的构建出语法树。

语法树的节点的设计,主要是为了解决如何表达语法结构。不同的数据库有不同的具体实现。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支持转义字符,包括:\b,\f,\n,\r,\t.

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中的字符串用双引号括起来的表示,而且需要进行转义处理。

\"(\\.|\"\"|[^"\\\n])*\" {
  ParseNode* node = NULL;
  malloc_new_node(node, ((ParseResult*)yyextra)->malloc_pool_, T_IDENT, 0);
  yylval->node = node;
  char* src = http://www.mamicode.com/yytext+1;"color:rgb(174,129,255)">* 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] = ‘\0‘; //remove finalnode->str_value_ = dest;//字面值

  ret = sscanf(dest, "%4d-%2d-%2d", &year, &month, &day);
  assert(ret == 3);

  memset(&time, 0, sizeof(struct tm));
  time.tm_year = year - 1900;
  time.tm_mon = month - 1;
  time.tm_mday = day;
  time.tm_hour = 0;
  time.tm_min = 0;
  time.tm_sec = 0;
  time.tm_isdst = -1;

  node->value_ = mktime(&time) * 1000000L;//转成微秒
  yylval->node = node;
  return DATE_VALUE;
}

从表达式中可以看到,OceanBase支持直接传入日期时间,OceanBase也支持Time和Timestamp类型。具体如下表:

类型格式(不区分大小写)表达式
DateDate ‘YYYY-MM-DD‘Date{whitespace}?‘[0-9]{4}(-[0-9]{2}){2}‘
TimeTime ‘HH:MI:SS.FF‘或Time ‘HH:MI:SSTime{whitespace}?‘[0-9]{2}(:[0-9]{2}){2}[.][0-9]{1,6}‘,Time{whitespace}?‘[0-9]{2}(:[0-9]{2}){2}[.]?‘
TimestampTimestamp ‘YYYY-MM-DD HH:MI:SS.FF‘或 Timestamp ‘YYYY-MM-DD HH:MI:SS‘Timestamp{whitespace}?‘[0-9]{4}(-[0-9]{2}){2}[ ][0-9]{2}(:[0-9]{2}){2}[.][0-9]{1,6}‘,Timestamp{whitespace}?‘[0-9]{4}(-[0-9]{2}){2}[ ][0-9]{2}(:[0-9]{2}){2}[.]?‘

如下是一个使用日期的示例:

select id,name from student where createtime <= date ‘2014-09-12‘;

语法树中存在日期类型的节点,其str_value_存储时间的字面值,value_存储该时间与基准时间(1970年1月1日0时0分0秒)之差的毫秒值。

OceanBase通过拓展标准的SQL语法支持直接在SQL语句中写入时间 ,在词法分析阶段就识别到时间类型,而 PostgreSQL 是在语法分析阶段才会识别时间 。 相比而言 , OceanBase 这种方式更直观,方便。

在0.4版本的OceanBase中,增加了预编译的功能,需要识别占位符"?",系统变量和用户变量。对应性质如下:

名称表达式动作代码返回值
占位符(?)"?"QUESTIONMARK
系统变量(system_variable)(@@[A-Za-z_][A_Za-z0-9_]*)SYSTEM_VARIABLE
用户变量(temp_variable)(@[A-Za-z_][A_Za-z0-9_]*)TEMP_VARIABLE

语法分析

OceanBase的SQL语法文件为sql_parser.y。.y语法文件最终由Bison转为可编译的.c文件。其结构与Flex的.l文件类似,分为选项部分,规则部分和代码部分。下面的代码都是去掉了关联的动作代码的规则语法,这样有助于更专注的理解语法的实现。

select语法

在SQL的语句语法中,最复杂的语法莫过于Select语句。我们先来看其在OceanBase中的语法。一个select语句可以使带括号的select语句,也可以使不带括号的select语句:

select_stmt: 
    select_no_parens    %prec UMINUS    /* 不到括号的select语句 */
  | select_with_parens    %prec UMINUS  /* 带括号的select语句 */
  ;

%prec用于交换左右两个标记的优先级和结合性。即select_no_parens与UMINUS互换优先级和结核性,select_with_parens 与UMINUS互换优先级和结核性。UMINUS的定义为%right UMINUS。所有上面两句的结果是select_no_parens和select_with_parens都变成了右结合。

带括号的select语句可以是只带一个括号,也可以带多对括号:

select_with_parens:
    ‘(‘ select_no_parens ‘)‘      /*只带一个括号*/
  | ‘(‘ select_with_parens ‘)‘    /*带多对括号*/
  ;

不带括号的select语句包括简单的select,带order by排序,带order by排序和limit限制的select语句3种:

select_no_parens:
    simple_select              
  | select_clause order_by
  | select_clause opt_order_by select_limit
  ;

select子句包括简单slect和带括号的select:

select_clause:
    simple_select                 
  | select_with_parens           
  ;

为什么不包括不带括号的select,暂时没想通。

简单select语句包括我们常见的select 语句,但没有order by和limit选项;同时还包括UNION,INTERSECT,EXCEPT三种运算:

simple_select: 
    SELECT opt_distinct select_expr_list 
    FROM from_list
    opt_where opt_groupby opt_having
  | select_clause UNION opt_distinct select_clause
  | select_clause INTERSECT opt_distinct select_clause
  | select_clause EXCEPT opt_distinct select_clause
  ;

select语句的定义相对其他语句很复杂,而且上面这段代码还没有包括无表的select和for update的select。(这两项在0.4版本的OceanBase中已经实现)。

下面这段是从PostgreSQL9.2.3中截取的Select语法:

SelectStmt: select_no_parens      %prec UMINUS
      | select_with_parens    %prec UMINUS
    ;

select_with_parens:
      ‘(‘ select_no_parens ‘)‘       
      | ‘(‘ select_with_parens ‘)‘   
    ;
select_no_parens:
      simple_select           
      | select_clause sort_clause
      | select_clause opt_sort_clause for_locking_clause opt_select_limit
      | select_clause opt_sort_clause select_limit opt_for_locking_clause
      | with_clause select_clause
      | with_clause select_clause sort_clause
      | with_clause select_clause opt_sort_clause for_locking_clause opt_select_limit
      | with_clause select_clause opt_sort_clause select_limit opt_for_locking_clause
    ;

select_clause:
      simple_select             
      | select_with_parens      
    ;
simple_select:
      SELECT opt_distinct target_list
      into_clause from_clause where_clause
      group_clause having_clause window_clause
      | values_clause             
      | TABLE relation_expr
      | select_clause UNION opt_all select_clause
      | select_clause INTERSECT opt_all select_clause
      | select_clause EXCEPT opt_all select_clause
    ;

不知道你是不是有种似曾相识的感觉。没错,在这里,OceanBase借鉴了PostgreSQL的语法分析方式。PostgreSQL号称世界上最优秀的开源数据库,绝非浪得虚名。

update语法

为保证示例的完整性,我们在来分析下update的语法。

/*0.3版本的OceanBase*/
update_stmt: 
    UPDATE relation_factor SET update_asgn_list opt_where
  ;

update_asgn_list: 
    update_asgn_factor
  | update_asgn_list ‘,‘ update_asgn_factor
  ;

update_asgn_factor:
    NAME COMP_EQ expr
  ;

上面为0.3版本中的update的语法,支持常见的update的SQL语句,如果将它和0.4版本的update的语法相比,就会发现:0.4版本的update在set时可以使用保留关键字作为列名,支持hint语法,支持when子句。随着OceanBase逐渐完善,其解析的语法也必然会越来越复杂。在学习的时候,从一个简单的版本开始,由易到难,就显得很重要。

/*0.4版本的OceanBase*/
update_stmt:
    UPDATE opt_hint relation_factor SET update_asgn_list opt_where opt_when
  ;
update_asgn_factor:
    column_name COMP_EQ expr
  ;
column_name:
    NAME
  | unreserved_keyword
  ;
opt_when:
    /* EMPTY */
  | WHEN expr
  ;

除了select,比较复杂还有create table,alter table,expr等。本节最后只讨论表达式的语法问题。其他的不再讨论。

表达式语法

expression 是指表达列名或者是一个能够给出真假的布尔表达式。如
a+3 > b
a is not null
等等。表达式最常出现在where子句中,用于过滤数据行。

表达式的语法之所以复杂,是因为表达式可以嵌套多个子表达式,多个子表达式出现后可能会出现语法上的歧义。BETWEEN ... AND 用于测试给的表达式是否在给定的范围内,这个操作符中的AND 与逻辑与的AND有歧义。需要使用%prec来改变其优先级和结合性,该方法能奏效,但是PostgreSQL和OceanBase都选择从语法树规则上完全消除该歧义。

OceanBase中表达式有3种类型,分别为expr , arith_expr , simple_expr。其中

  • expr :expr为表达式的核心,是不受约束的表达式,可以直接用于where,having等子句中。
  • arith_expr :arith_expr是可以用于between ... and之间的表达式,是受约束的表达式。因此arith_expr是expr的一个真子集。
  • simple_expr :simple_expr是简单表达式,不能直接进行自我递归的表达式。是expr和aritharith_expr的共有的部分。
expr_const:
    STRING 
  | DATE_VALUE 
  | INTNUM 
  | APPROXNUM 
  | BOOL 
  | NULLX 
  | QUESTIONMARK 
  | TEMP_VARIABLE 
  | SYSTEM_VARIABLE 
  | SESSION_ALIAS ‘.‘ column_name 
  ;

simple_expr:
    column_ref
  | expr_const
  | ‘(‘ expr ‘)‘
  | ‘(‘ expr_list ‘,‘ expr ‘)‘
  | case_expr
  | func_expr
  | when_func
  | select_with_parens        %prec UMINUS
  | EXISTS select_with_parens
  ;

/* used by the expression that use range value, e.g. between and */
arith_expr:
    simple_expr   
  | ‘+‘ arith_expr %prec UMINUS
  | ‘-‘ arith_expr %prec UMINUS
  | arith_expr ‘+‘ arith_expr 
  | arith_expr ‘-‘ arith_expr 
  | arith_expr ‘*‘ arith_expr 
  | arith_expr ‘/‘ arith_expr 
  | arith_expr ‘%‘ arith_expr 
  | arith_expr ‘^‘ arith_expr 
  | arith_expr MOD arith_expr 

expr:
    simple_expr   
  | ‘+‘ expr %prec UMINUS
  | ‘-‘ expr %prec UMINUS
  | expr ‘+‘ expr 
  | expr ‘-‘ expr 
  | expr ‘*‘ expr 
  | expr ‘/‘ expr 
  | expr ‘%‘ expr 
  | expr ‘^‘ expr 
  | expr MOD expr 
  | expr COMP_LE expr 
  | expr COMP_LT expr 
  | expr COMP_EQ expr 
  | expr COMP_GE expr 
  | expr COMP_GT expr 
  | expr COMP_NE expr 
  | expr LIKE expr 
  | expr NOT LIKE expr 
  | expr AND expr %prec AND
  | expr OR expr %prec OR
  | NOT expr
  | expr IS NULLX
  | expr IS NOT NULLX
  | expr IS BOOL
  | expr IS NOT BOOL
  | expr IS UNKNOWN
  | expr IS NOT UNKNOWN
  | expr BETWEEN arith_expr AND arith_expr        %prec BETWEEN
  | expr NOT BETWEEN arith_expr AND arith_expr      %prec BETWEEN
  | expr IN in_expr
  | expr NOT IN in_expr
  | expr CNNOP expr
  ;

simple_expr为简单表达式,并不是说它很简单,因为simple_expr也包含了select_with_parens(带括号的select语句)等语句,只能说simple_expr不能像arith_expr和expr等一样递归调用。

expr:expr BETWEEN arith_expr AND arith_expr        %prec BETWEEN
  | expr NOT BETWEEN arith_expr AND arith_expr      %prec BETWEEN
  ;

arith_expr用于between ... and 之间,只包含了算术表达式,没有is,like 以及比较关系表达式。

expr,arith_expr,simple_expr实际上等同于PostgreSQL中的a_expr,b_expr,c_expr。

总之,在语法树规则和词法分析方面,OceanBase大量的借鉴了PostgreSQL。

节点的创建

确定了节点的结构和语法树的形状,就到了实际创建语法树的过程。通常,1条SQL语句的语法树存在多个节点,数据库中要高效的执行多条语句,就会遇到大量的节点创建问题。如果每次创建都调用new/malloc等系统函数必然会浪费大量的时,而且还会造成内存碎片。这种问题常常会出现在现实中,各个数据库都有自己不同的解决办法,但基本原理是一次性向系统申请较大的内存,然后在需要的时候自行分配。
接下来我将对比PostgreSQL,RedBase,OceanBase 3个数据库是各自如何解决这一问题的。

PostgreSQL创建语法树节点

PostgreSQL使用两个宏完成节点的创建,即makeNode和newNode.

#define makeNode(_type_)  ((_type_ *) newNode(sizeof(_type_),T_##_type_))

/* 针对gcc版本的newNode */#define newNode(size, tag) ({    Node *_result;     AssertMacro((size) >= sizeof(Node));/* 检测申请的内存大小,>>=sizeof(Node) */     _result = (Node *) palloc0fast(size); /* 申请内存 */     _result->type = (tag); /*设置TypeTag */     _result; /*返回值*/})

#define palloc0fast(sz)     ( MemSetTest(0, sz) ?         MemoryContextAllocZeroAligned(CurrentMemoryContext, sz) :         MemoryContextAllocZero(CurrentMemoryContext, sz) )

注意:要避免直接使用newNode来创建节点,因为节点的大小在不同的环境下可能是不同的。使用makeNode即可,如:
Stmt *s = makeNode(Stmt);

我们知道,PostgreSQL中继承自NODE*的结构体们各自大小不一,都大于等于NODE 的大小。newNode中使用palloc0fast申请内存,它仍然是个宏,最终调用的是PostgreSQL中的MemoryContextAllocZeroAligned或者MemoryContextAllocZero函数,这两个函数来自PostgreSQL中内存管理模块--内存上下文,该模块能够很好的应对内存碎片,向数据库进程提供缓冲区管理。

RedBase创建语法树节点

RedBase没有提供向PostgreSQL一样复杂的内存管理机制,而是采用简单的方法来解决频繁创建节点的问题。因为RedBase中语法树的节点都是NODE类型的,所有RedBase使用一个NODE的数组来作为缓冲区,默认最大值为100,超过这个值就会创建出错,并最终退出程序。如果你的SQL语句需要的节点超过100,需要在编译之前修改最大节点数目MAXNODE以适应需求。

#define MAXNODE        100 //最大节点数目

static NODE nodepool[MAXNODE];
static int nodeptr = 0;

/*
 * newnode: allocates a new node of the specified kind and returns a pointer
 * to it on success.  Returns NULL on error.
 */
NODE *newnode(NODEKIND kind)
{
    NODE *n;

    /* if we‘ve used up all of the nodes then error */
    if(nodeptr == MAXNODE){
    fprintf(stderr, "out of memory\n");
    exit(1);
    }

    /* get the next node */
    n = nodepool + nodeptr;
    ++nodeptr;

    /* initialize the `kind‘ field */
    n -> kind = kind;
    return n;
}

OceanBase创建语法树节点

OceanBase创建语法树节点的方式与PostgreSQL类似,上层调用,底层有负责内存池管理的模块来负责真正的创建,不过不是内存上下文,而是一个类ObStringBuf.

OceanBase的new_node函数申请内存时调用的是parse_malloc函数,该函数专用于语法树结构体的创建,从obStringBuf中获取内存,直到obStringBuf线程退出时才会真正释放这些内存。

大型的数据库都会有专门的内存管理模块来负责语法程序运行过程中的内存申请和释放,小型的数据库则结合自身特点,量身定制轻量的内存管理机制。目的都只有一个:降低频繁创建和销毁语法树节点的开销

总结

SQL的执行包括了解析语法树,制定逻辑查询计划,执行物理查询计划等步骤。通过对比3个不同的数据库的源代码,得出在解析语法树中遇到的一些共性问题:

  • 如何表示一个语法树的节点
  • 利用Bison定义语法树的结构时的一些难点,如select,expr等
  • 如何降低频繁创建和销毁语法树节点的开销

如果你要自己来写一个SQL解析程序的话,这些问题同样难以避免。


欢迎光临我的网站----我的博客园----我的CSDN。
如果阅读本文过程中有任何问题,请联系作者,转载请注明出处!