首页 > 代码库 > 淘宝数据库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解析相对简单。更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个步骤:

  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中採用的这样的方式缺点非常明显,就是使用这些结构体时必需要细致辨别各个子节点代表的意义。否则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支持转义字符,包含:\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;>

当匹配到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 final ‘

  node->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类型。详细例如以下表:

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

Timestamp Timestamp ‘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时能够使用保留keyword作为列名。支持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为简单表达式,并非说它非常easy。由于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。


假设阅读本文过程中有不论什么问题,请联系作者。转载请注明出处。


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