2021 OceanBase数据大赛 初赛总结

2021 OceanBase数据大赛 初赛总结

最终成绩:240/240

本次在比赛中负责update、order by、group by、drop table、聚合运算、一次插入多条数据、NULL类型、子查询、复杂子查询和表达式10个模块共130分,具体代码见lyxiong0/miniob (github.com)。

插入多条数据

题目要求:

insert into t3 values (1,1),(2,2),(3,3);

一次插入的数据有一个失败则整个语句失败

代码在src/observer/storage/default/default_storage_stage.cpp:178

思路:

  1. 首先语法分析中,将多个值存放在结构体数组inserts.values内
  2. 遍历inserts.values进行原先已实现的单插入操作。
  3. 注意插入失败的时候不能直接调用rollback回滚,因为可能会将之前成功的插入语句一并回滚。所以这里采用临时数组std::vector records;记录所有插入的record,一旦某一个插入失败则嗲用delete_record删除records内的所有记录。

支持NULL类型

一种摆烂的做法是内部定义一个测试不可能用到的字符,例如"Eu83"当作NULL存储…考虑到这么做没有意义,还是采用了其他正常一些的做法:

  1. 每列都有自己的类型,识别到NULL的时候int和float类型存放0,char类型存放"NULL",date类型存放"1970-01-01"(这里值随意,自己记得住就行)
  2. 在每条记录Record结构体的data成员最后增加N bits空间(N为列数量),每个bit = 1表示该列成员为NULL值。也可在data成员里每列对应值后面增加一个bit位,但这样实现起来比较复杂,因为每个类型所占比特长度不同。

代码位置src/observer/storage/common/table.cpp:407

简单子查询与复杂子查询

简单子查询和复杂子查询的区别在于复杂子查询功能上支持多层、支持运算符左右两侧都是子查询以及关联子查询,而子查询本身支持的语法只有简单的select /列、聚合/ from // where …

  1. 语法分析和词法分析上,本质是套娃,所以实现了简单子查询也实现了复杂子查询。这里要注意两点,一是把where/from_rel/select_attr作为单独节点,和原先select的全局变量分离,二是子查询的Select结构体是放在Condition内,即Condition左右两侧有三种情况:子查询、值和变量。

代码src/observer/sql/parser/yacc_sql.y:910

  1. 多层子查询:do_select函数兼任在第一层主查询给客户端输出查询结果和第N层子查询向上一层查询返回结果的职责,因此需要给do_select函数增加一个bool is_sub_select参数。在之前语法分析中,我们已经将Select结构体塞入Condition数组中,遍历conditions判断到子查询时,递归调用do_select即可。
  2. 关联子查询:定义见如何正确理解SQL关联子查询 - 何大卫 - 博客园 (cnblogs.com),可能会出现第N层和第一层主表关联的情况,因此需要在do_select函数内增加一个参数main_table用于传递主表信息,若main_table为nullptr说明是第一层查询,此时给main_table赋值为当前表名。

由于子查询采用了递归调用do_select的方法实现,那怎么把外部查询的每一行数据拿去跟内部查询比较呢?这里采用了一种语义转换的方法,把关联子查询等价成包含主表的多表group by。例如select * from t1 where t1.v1 > (select t2.v2 from t2 where t2.v3 = t1.v1);中子查询部分相当于select t1.v1, max(t2.v2) from t1, t2 where t2.v3 = t1.v1 group by t1.v1。这种语义转换是我自己想的,没有去找资料验证,不保证正确性

细节上,在ExecuteStage类里增加了is_related成员,由于ExecuteStage生命周期贯穿整个Sever运行期间,在此需要注意在每个SCF_SELECT调用do_select之前将is_related置为false。is_related成员的作用在于做一个合法性判断,在非关联子查询的情况下,子查询结果查过一列都是非法的。

代码src/observer/sql/executor/execute_stage.cpp:751

  1. 左右两侧都是子查询:首先注意加上合法性判断,两侧都是子查询的时候,两个查询结果必须为值,即行列数均为1。比较结果不相等则清空result,否则不变。
  2. 子查询结果为空的情况:如果比较符号为not in则保留所有结果,否则清空result。

order by排序

由于测试数据量很小,这里直接采用了快排的方法。

  1. 定义一个OrderInfo结构体,用于存放order by后面跟着的列信息,包括列名以及升序/降序。
  2. 定义tuple_compare函数,内部按照OrderInfo的信息比较两个tuple的大小,注意order by v1, v2应该先按v2排序,再按v1排序
  3. 利用快排对result排序即可。

代码src/observer/sql/executor/execute_stage.cpp:1712

group by分组(含聚合函数)

group by利用了哈希桶的方法,定义std::unordered_map tuple_to_indexes记录将result分割为包含多个TupleSet的vector results。

哈希桶思路很好理解,这里唯一的问题就是怎么利用group by的列信息给每一行构造一个哈希值。首先我们给调用STL中hash模板,给每一个类型(StringValue/IntValue/FloatValue)都定义一个to_hash方法。再给Tuple类也定义一个to_hash方法,传入参数为生成哈希值所包括的列,实现上将传入的每个列对应TupleValue的哈希值做一个异或操作(比较常见,换成与/或操作可能也行?),生成Tuple所对应的哈希值。

代码src/observer/sql/executor/execute_stage.cpp:1320

表达式

  1. 代码实现部分:参考Leetcode 基本计算器III,其实就是对Tuple的加减乘除
  2. 语法解析部分:有两个难点。一是表达式本身的解析,采用递归解析表达式,并定义符号优先级和左/右结合消除语法解析的shift/reduce warning。二是由于表达式本身包含id/number/id.id三种情况,所以要在用新的结构存储表达式的同时与原先的condition、where代码兼容。

这里将id/number/id.id/左右括号全部视作单个字符串,按输入顺序存储,通过字符串数组中字符串个数是否与select中以逗号分割的个数是否相同判断为字符串还是普通select,若是字符串则进行计算。

你可能感兴趣的:(数据库,数据库,OceanBase)