10.2 只读事务

只读事务(SELECT语句),经过词法分析、语法分析,预处理后,转化为逻辑查询计划和物理查询计划。以SQL语句select c1,c2 from t1 where id=1 group by c1 order by c2为例,MergeServer收到该语句后将调用ObSql类的静态方法direct_execute,执行步骤如下:

1)调用flex、bison解析SQL语句生成一个语法树。

2)解析语法树,生成逻辑执行计划ObSelectStmt。ObSelectStmt结构中记录了SQL语句扫描的表格名(t1),投影列(c1,c2),过滤条件(id=1),分组列(c1)以及排序列(c2)。

3)根据逻辑执行计划生成物理执行计划。ObSelectStmt只是表达了一种意图,但并不知道实际如何执行,ObTransformer类的generate_physical_plan将ObSelectStmt转化为物理执行计划。

逻辑查询计划的改进以及物理查询计划的选择,即查询优化器,是关系数据库最难的部分,OceanBase目前在这一部分的工作不多。因此,本节不会涉及太多关于如何生成物理查询计划的内容,下面仅以两个例子说明OceanBase的物理查询计划。

例10-1 假设有一个单表SQL语句如图10-2所示。

10.2 只读事务 - 图1

图 10-2 单表物理查询计划示例

单表SQL语句执行过程如下:

1)调用TableScan操作符,读取子表t1中的数据,该操作符还将执行投影(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含c1、c2、c3三列。

2)调用HashGroupBy操作符(假设采用基于哈希的分组算法),按照c1对数据分组,同时计算每个分组内c2列的总和。

3)调用Filter操作符,过滤分组后生成的结果,只返回上一层sum(c2)>=10的行。

4)调用Sort操作符将结果按照c1排序。

5)调用Project操作符,只返回c1和sum(c2)这两列数据。

6)调用Limit操作符执行分页操作,只返回前20条数据。

例10-2 假设有一个需要联表的SQL语句如图10-3所示。

10.2 只读事务 - 图2

图 10-3 多表物理查询计划示例

多表SQL语句执行过程如下:

1)调用TableScan分别读取t1和t2的数据。对于t1,使用条件c3=10对结果进行过滤,t1和t2都只需要返回c1,c2,c3这三列数据。

2)假设采用基于排序的表连接算法,t1和t2分别按照t1.c2和t2.c2排序后,调用Merge Join运算符,以t1.c2=t2.c2为条件执行等值连接。

3)调用HashGroupBy运算符(假设采用基于哈希的分组算法),按照t1.c1对数据分组,同时计算每个分组内t2.c3列的总和。

4)调用Filter运算符,过滤分组后的生成的结果,只返回上一层sum(t2.c3)>=10的行。

5)调用Sort操作符将结果按照t1.c1排序。

6)调用Project操作符,只返回t1.c1和sum(t2.c3)这两列数据。

7)调用Limit操作符执行分页操作,只返回前20条数据。

10.2.1 物理操作符接口

9.4.2节介绍一期分布式存储引擎中的迭代器接口为ObIterator,通过它,可以将读到的数据以cell为单位逐个迭代出来。然而,数据库操作总是以行为单位的,因此,二期实现数据库功能层时考虑将基于cell的迭代器修改为基于行的迭代器。

行迭代器接口如下:


//ObRow表示一行数据内容

class ObRow

{

public:

//根据表ID以及列ID获得指定cell

//@param[in]table_id表格ID

//@param[in]column_id列ID

//@param[out]cell读到的cell

int get_cell(const uint64_t table_id,const uint64_t column_id,ObObj*&cell);

//获取第cell_idx个cell

int raw_get_cell(const int64_t cell_idx,const ObObj*&cell,uint64_t&table_id,

uint64_t&column_id);

//获取本行的列数

int64_t get_column_num()const;

};


每一行数据(ObRow)包括多个列,每个列的内容包括所在的表ID(table_id),列ID(column_id)以及列内容(cell)。ObRow提供两种访问方式:根据table_id和column_id随机访问某个列,以及根据列下标(cell_idx)获取某个指定列。

物理运算符接口如下:


//物理运算符接口

class ObPhyOperator

{

public:

//添加子运算符,所有非叶子节点物理运算符都需要调用该接口

virtual int set_child(int32_t child_idx,ObPhyOperator&child_operator);

//打开物理运算符。申请资源,打开子运算符等

virtual int open()=0;

//关闭物理运算符。释放资源,关闭子运算符等

virtual int close()=0;

//获得下一行数据内容

//@param[out]row下一行数据内容的引用

//@return返回码,包括成功、迭代过程中出现错误以及迭代完成

virtual int get_next_row(const ObRow*&row)=0;

};


ObPhyOperator每次获取一行数据,使用方法如下:


ObPhyOperator rootoperator=root_operator;//根运算符

root_operator->open();

ObRow*row=NULL;

while(OB_SUCCESS==root_operator->get_next_row(row))

{

Output(row);//输出本行

}

root_operator->close();


为什么ObPhyOperator类中有一个set_child接口呢?这是因为所有的物理运算符构成一个树,每个物理运算的输出结果都可以认为是一个临时的二维表,树中孩子节点的输出总是作为它的父亲节点的输入。例10-1中,叶子节点为一个TableScan类型的物理运算符(称为table_scan_op),它的父亲节点为一个HashGroupBy类型的物理运算符(称为hash_group_by_op),接下来依次为Filter类型物理运算符filter_op,Sort类型物理运算符sort_op,Project类型物理运算符project_op,Limit类型物理运算符limit_op。其中,limit_op为根运算符。那么,生成物理运算符时将执行如下语句:


limit_op->set_child(0,project_op);

project_op->set_child(0,sort_op);

sort_op->set_child(0,filter_op);

filter_op->set_child(0,hash_group_by_op);

hash_group_by_op->set_child(0,table_scan_op);

root_op=limit_op;


SQL最终执行时,只需要迭代root_op(即limit_op)就能够把需要的数据依次迭代出来。limit_op发现前一批数据迭代完成则驱动下层的project_op获取下一批数据,project_op发现前一批数据迭代完成则驱动下层的sort_op获取下一批数据。以此类推,直到最底层的table_scan_op不断地从原始表t1中读取数据。