背景
建议看这位大佬的文章,讲解的挺清楚。
project3就是实现一系列算子,本身编码并不难,难的是看懂各个接口的使用,以及许多细节处理。笔者在实验的过程中遇到了一些bug,本文会主要说明这些bug及其处理。
Task 1
SeqScan
直接用TableIterator,注意区分前置++和后置++,这个实验只定义了前置++。
由于TableIterator只能用移动构造,所以可以直接在SeqScanExecutor构造函数初始化列表中使用TableIterator移动构造函数,或者使用unique_ptr(推荐)。
Insert
大部分接口都提供了,直接用。
这里讲一个坑,insert的返回值tuple是插入个数,将Int转换为Tuple找了半天,最后还是参考projection_executor.cpp的写法,代码如下
std::vector<Value> values;
values.emplace_back(INTEGER, count_rows);
*tuple = Tuple{values, &GetOutputSchema()};
另外,即使只插入0个也要返回true,为了不陷入死循环,又加了个bool类型的数据成员来判断是第一次执行还是第二次执行。
Update
也是返回更新数量,处理同上。另外,不能一条一条地更新(删除与插入),否则会陷入死循环,需要先将更新元组存储再执行更新。
IndexScan
可以直接转成B+Tree索引
tree_ = dynamic_cast<BPlusTreeIndexForTwoIntegerColumn *>(index_info_->index_.get())
对迭代器进行operator*()可以得到<key, rid>,再根据rid再page heap取tuple。
遇到了project2的一个bug,之前B+Tree删除时可能会也空节点,导致这次project出现访问end迭代器的情况。
Task #2 - Aggregation & Join Executors
Aggregation
遇到一个小bug,在第一次执行max操作时,我先将result->aggregates_[i]置为INT32_MIN,然后再执行max操作,但好像执行过程中将INT32_MIN当作最大值了…
最后改成INT32_MIN + 1了。
result->aggregates_[i] = ValueFactory::GetIntegerValue(INT32_MIN + 1);
一个关键点是输出结果是否含有key,如果没有key,则即使输出结果为空也要输出一行,但是如果有key,则输出为空。
NestedLoopJoin
-
inner join: 每次遍历完right_executor_,需要执行right_executor_->Init(),对于之前实现的算子,要特别注意Init(),别忘记初始化元数据。
-
left join: 如果right_executor_没有与left_tuple对应的,则应该与null结合。关键代码如下:
std::vector<Value> values;
for (uint32_t i = 0; i < left_schema.GetColumns().size(); ++i) {
values.emplace_back(current_left_tuple_.GetValue(&left_schema, i));
}
for (uint32_t i = 0; i < right_schema.GetColumns().size(); ++i) {
values.emplace_back(ValueFactory::GetNullValueByType(right_schema.GetColumn(i).GetType()));
}
*tuple = Tuple(values, &GetOutputSchema());
HashJoin & Optimizing NestedLoopJoin to HashJoin
哈希表的key一开始选为value,但是没有定义hash,会报错,可以参考聚合算子的聚合key写法,写一个HashKey。
Optimizing一开始不知道怎么下手,可以参考/src/optimizer/merge_projection.cpp。
我贴下关键代码:
if (type == PlanType::NestedLoopJoin) {
...
if (dynamic_cast<ColumnValueExpression *>(key_expressions[0].get()) != nullptr) { // t1.a = t2.a
for (auto const &expr : key_expressions) {
if (dynamic_cast<ColumnValueExpression *>(expr.get())->GetTupleIdx() == 0) {
left_exprs.emplace_back(expr);
} else {
right_exprs.emplace_back(expr);
}
}
} else { // t1.a = t2.a and t1.b = t2.b and ...
...
return std::make_shared<HashJoinPlanNode>(nlj_plan.output_schema_, left_child_plan, right_child_plan, left_exprs,
right_exprs, nlj_plan.GetJoinType());
}
哈希join只支持=,所以不用在乎nested的predicate。以及注意递归调用。
另外,expr属于左join还是右join需要根据tuple_index的值判断。如果有多个等式,key_expressions还会嵌套。
Task #3
Sort and limit
非常简单,但是记得init时元数据初始化,比如sort中,vector需要清空,limit中index需要置零。
贴一个排序参考
case OrderByType::ASC: {
auto value_a = expr->Evaluate(&a, schema);
auto value_b = expr->Evaluate(&b, schema);
if (value_a.CompareLessThan(value_b) == CmpBool::CmpTrue) {
return true;
}
if (value_a.CompareGreaterThan(value_b) == CmpBool::CmpTrue) {
return false;
}
}
Top-N
实现一个堆,这也不难,priority_queue走起。将sort中的比较函数实现成一个类。
将sort+limit优化成top-n别忘记递归优化。
优化
暂时没空。
评测与总结
截止提交时间(2023.05-18),gradescope上有25人完成了project2。
这次实验略微加深了对sql执行的理解,增加了vscode debug经验。