CMU-15445 2021 Project 3-Query Exection (请求执行)
先贴结果图和LeaderBoard:
CMU禁止公开源代码哦~, 有问题欢迎私聊, 评论或者加群: 484589324交流~
这是我受益最大的一个Project, 本来还以为按照惯例实现三个算子即可, 结果只有一道题, 要求实现全部算子hh, 但是做完之后可以理解数据库增删改查分组筛选等所有操作的底层思想, 受益确实非常的大
比较坑的一点是GradeScope的GTEST更新后代码风格不满足clang-tidy要求的代码风格, 导致提交全0, github上有相关的issue, 但是已经被close了而且问题并没有解决, 我又重新提了issue, 希望早日修复~ 现在用的解决方案是比较蠢的, 看图
这个方案是有效的, 我帮大家整理成傻瓜式操作, 在打包成zip的提交的时候, 执行:
zip project3-submission.zip src/include/buffer/lru_replacer.h src/buffer/lru_replacer.cpp src/include/buffer/buffer_pool_manager_instance.h src/buffer/buffer_pool_manager_instance.cpp src/include/storage/page/hash_table_directory_page.h src/storage/page/hash_table_directory_page.cpp src/include/storage/page/hash_table_bucket_page.h src/storage/page/hash_table_bucket_page.cpp src/include/container/hash/extendible_hash_table.h src/container/hash/extendible_hash_table.cpp src/include/execution/execution_engine.h src/include/execution/executors/seq_scan_executor.h src/include/execution/executors/insert_executor.h src/include/execution/executors/update_executor.h src/include/execution/executors/delete_executor.h src/include/execution/executors/nested_loop_join_executor.h src/include/execution/executors/hash_join_executor.h src/include/execution/executors/aggregation_executor.h src/include/execution/executors/limit_executor.h src/include/execution/executors/distinct_executor.h src/execution/seq_scan_executor.cpp src/execution/insert_executor.cpp src/execution/update_executor.cpp src/execution/delete_executor.cpp src/execution/nested_loop_join_executor.cpp src/execution/hash_join_executor.cpp src/execution/aggregation_executor.cpp src/execution/limit_executor.cpp src/execution/distinct_executor.cpp src/include/storage/page/tmp_tuple_page.h
下面进入正题, 这次的Project算子们相对比较独立, 我们按照顺序一个个算子的实现即可
我们要实现的是火山 (volcano) 模型的算子, 也就是一个tuple一个tuple的返回结果, 而不是一次返回全部结果
注意, 不应该以任何手段缓存全部结果再一个一个的返回, 而是每次都要获取一个新结果返回, 否则不过是在自欺欺人罢了, 这样的东西不是火山模型
每个算子都要实现Init()
和 Next()
方法
你不需要在意任何并发问题, 这次的Project是基于单线程的
SEQUENTIAL SCAN
-
顺序扫描一张表, 每次调用Next()返回一个tuple即可
-
也许你需要这些成员变量来遍历表
// 要扫描的表的info TableInfo *tb_info_; // 用来遍历表 TableIterator tb_iter_;
-
然后在Init时, 你需要初始化这些成员
// catalog记录了一些表和索引的元信息, 要获取表信息就得靠catalog tb_info_ = exec_ctx_->GetCatalog()->GetTable(plan_->GetTableOid()); // 表的迭代器肯定由table_info维护, 顺藤摸瓜摸出来即可 tb_iter_ = tb_info_->table_->Begin(exec_ctx_->GetTransaction());
-
你还需要根据条件筛掉一些tuple
auto predicate = plan_->GetPredicate(); if (predicate == nullptr || predicate->Evaluate(&t, schema).GetAs<bool>()) { ... }
INSERT
-
有两种可能的插入方式, 一种是从child_executor获取tuple, 一种是自身携带tuple, 封装一些函数会很helpful, 比如
// 子执行器 std::unique_ptr<AbstractExecutor> child_executor_; // 要插入的表的info const TableInfo *tb_info_; // 和这个表有关的所有索引, 更新索引避不开的 std::vector<IndexInfo *> idxs_; // 有两种方式获取NextTuple, 我们封装一个函数, 同时需要一个raw_iterator来对应raw_insert的情况 bool GetNextTuple(Tuple *tuple); std::vector<std::vector<Value>>::const_iterator raw_it_;
-
然后在Init时, 用if判断是否存在child_executor_
auto catalog = exec_ctx_->GetCatalog(); tb_info_ = catalog->GetTable(plan_->TableOid()); idxs_ = catalog->GetTableIndexes(tb_info_->name_); if (child_executor_) { child_executor_->Init(); } else { raw_it_ = plan_->RawValues().begin(); }
-
注意, Insert不应该修改result, 也就是说, 他不能返回true(返回true, 上层算子就会认为有数据来了), 应该一次性插入所有的内容然后返回false
DELETE/UPDATE
- 和Insert差不多, 也一次性进行全部操作, 然后直接返回false
NESTED LOOP JOIN
- 朴素的Join算法, 成员中应记录一个left_tuple, 然后每次Next()都从右表获取一个right_tuple和left_tuple做连接, 然后当获取不到right_tuple了, 再重置右算子, 然后对左算子调用Next(), 直到左算子也为空了返回false, 这样才能真正的实现火山模型
- 尽量用迭代去写, 我用递归好像栈溢出了, 改成迭代就好了
HASH JOIN
-
哈希的key要专门实现一下, 语法比较麻烦, 我认为不是重点, 可以用我的
namespace bustub { struct HashJoinKey { Value val_; bool operator==(const HashJoinKey &other) const { return val_.CompareEquals(other.val_) == CmpBool::CmpTrue; } }; } // namespace bustub namespace std { /** Implements std::hash on HashJoinKey */ template <> struct hash<bustub::HashJoinKey> { std::size_t operator()(const bustub::HashJoinKey &key) const { size_t curr_hash = 0; if (!key.val_.IsNull()) { curr_hash = bustub::HashUtil::CombineHashes(curr_hash, bustub::HashUtil::HashValue(&key.val_)); } return curr_hash; } }; } // namespace std
-
因为同一个key可能对应很多tuple, 所以成员变量我认为应该像我这样, 才能真正的实现火山模型
std::unordered_map<HashJoinKey, std::vector<Tuple>> ht_;
// 可能一个key会对应多个tuple, 我们记录这些tuple和当前正在与这些tuple做连接的右表的tuple_right
std::vector<Tuple> temp_l_tuples_;
Tuple t_r_;
也就是说, 每次连接, 我们
- 与temp_l_tuples_中的一条tuple做连接, 然后直接在temp_l_tuples_中删除该tuple
- 如果temp_l_tuples为空, 就重新获取tuple_right, 最后更新temp_l_tuples_
AGGREGATION (统计)
认真做, 真的大彻大悟
- 我们要实现的是Hash Agg, 其思路是将一个group的tuples存在同一个bucket中
while (child_->Next(&t, &rid)) {
// key就是group_by需要的key, 一个key对应一个group的内容, 完美
// val是该group对应的信息, 因为agg_val底层是vector, 所以我们可以同时进行很多类型的agg
// 比如agg[0]是count的话, 那就必须在insert的时候将该group对应的agg[0]自增1
// 同时agg[1]可能是max, 或者min等
aht_.InsertCombine(MakeAggregateKey(&t), MakeAggregateValue(&t));
}
- 大彻大悟发病瞬间 (逃)
auto group = aht_iterator_.Key();
auto group_agg_vals = aht_iterator_.Val();
...
for (const auto &col : plan_->OutputSchema()->GetColumns()) {
// 比如col是max + 10
// 那我就可以从获取group的agg_vals中获取max的统计信息再加10, 塞进返回值*tuple里
// 完美!!!!!!!!!
// 一个group对应一个哈希表的bucket, 通过不断的insert对哈希表中的agg_vals进行更新
vals.push_back(col.GetExpr()->EvaluateAggregate(group.group_bys_, group_agg_vals.aggregates_));
}
LIMIT
- 你肯定会写…
DISTINCT
- 也是用哈希的思路, 多个tuple而key相同时, 哈希表只会存储一个
- 如果官方的test说你10和100比较错误, 那是SeqScan写错了, 没有考虑col的Expr(), 回去修改一下即可