CMU 15445 P3 Query Execution

CMU 15445 P3 Query Execution

课程地址
做个数据库:2022 CMU15-445 Project3 Query Execution - 知乎 (zhihu.com),看完项目文档其实还是有点不清楚到底要干什么,这篇文章讲的比较详细,就不再重复说了,看完应该都知道应该干什么。
在这里插入图片描述

Task #1 - Access Method Executors

​ Bustub中数据的存储结构如上图所示,首先,Bustub 有一个 Catalog。Catalog 维护了几张 hashmap,保存了 table id 和 table name 到 table info 的映射关系。table id 由 Catalog 在新建 table 时自动分配,table name 则由用户指定。

这里的 table info 包含了一张 table 的 metadata,有 schema、name、id 和指向 table heap 的指针。系统的其他部分想要访问一张 table 时,先使用 name 或 id 从 Catalog 得到 table info,再访问 table info 中的 table heap。

table heap 是管理 table 数据的结构,包含 table 相关操作。table heap 本身并不直接存储 tuple 数据,tuple 数据都存放在 table page 中。table heap 可能由多个 table page 组成,仅保存其第一个 table page 的 page id。需要访问某个 table page 时,通过 page id 经由 buffer pool 访问。

table page 是实际存储 table 数据的结构,当需要新增 tuple 时,table heap 会找到当前属于自己的最后一张 table page,尝试插入,若最后一张 table page 已满,则新建一张 table page 插入 tuple。table page 低地址存放 header,tuple 从高地址也就是 table page 尾部开始插入。

tuple 对应数据表中的一行数据。每个 tuple 都由 RID 唯一标识。RID 由 page id + slot num 构成。tuple 由 value 组成,value 的个数和类型由 table info 中的 schema 指定。value 则是某个字段具体的值,value 本身还保存了类型信息。

在了解了其基本结构就可以开始Task1了:

SeqScan

每个算子我们需要分别实现其Next()Init函数:SeqScan其功能为扫描一张表的所有Tuple。因为项目中为我们提供了TableIterator所以实现起来不是很困难。但由于TableIterator将赋值删除了,所以需要将其定义为独享指针。

std::unique_ptr<TableIterator> iterator_;

Init中初始化迭代器,每调用一次Next就返回一个Tuple,由于项目中使用的是伪删除,所以还需要检查该Tuple对应的TupleMeta是不是删除的。

Insert Delete

Insert和Delete算子,实现起来差不多,可以先看看Filter算子的实现过程,table中也提供了往表中插入的函数,

RID rid1 = (tableinfo_->table_->InsertTuple(tuplemeta, *tuple)).value();

插入后返回的是其RID,因为写算子会改变索引结构,所以需要修改其对应的索引结构,函数都是现成的也不是特别难。

auto key = tuple->KeyFromTuple(tableinfo_->schema_, index->key_schema_, index->index_->GetKeyAttrs());
      index->index_->InsertEntry(key, rid1, exec_ctx_->GetTransaction());

Delete使用的是伪删除,需要将对应Tuple的TupleMata更新为删除的,并删除对应索引。

Update

Update算子其实就是先将满足要求的Tuple先删除后插入。因为插入的值是更新后的值,需要使用目标表达式对值进行更新。同时还需要修改两次索引。

std::vector<Value> value1;
for (auto &a : plan_->target_expressions_) {
   value1.emplace_back(a->Evaluate(tuple, child_executor_->GetOutputSchema()));
}
Tuple tuple1 = Tuple(value1, &child_executor_->GetOutputSchema());

Index Sacn

tree_ = dynamic_cast<BPlusTreeIndexForTwoIntegerColumn *>(index_info_->index_.get());

该类中直接包含了迭代器,实现过程和SeqScan差不多。

Task #2 - Aggregation & Join Executors

Task 1中实现的算子都是比较简单的单算子,Task 2难度就增加了一些。

Aggregation

因为Bustub使用的是火山模型,一条一条的输入,而该算子每计算一次都要遍历整张表,所以直接一次性直接全部算完保存到HashTable中。

在该算子中我们需要完成CombineAggregateValues,Init,Next三个函数。这里我只说AggregationType::MaxAggregate的实现:

if (!input.aggregates_[i].IsNull() && result->aggregates_[i].IsNull()) {
     result->aggregates_[i] = Value(INTEGER, -10000000);  // 用INT_MIN 会变成null
 }
          // input 非空且 result 是整型

 if (!input.aggregates_[i].IsNull() && result->aggregates_[i].CheckInteger()) {
    result->aggregates_[i] = result->aggregates_[i].Max(input.aggregates_[i]);
  }
  break;

一开始是想直接使用INT_MIN赋值的,但这边INT_MIN好像代表空值。

auto aggregate_key = MakeAggregateKey(&tuple);
auto aggregate_value = MakeAggregateValue(&tuple);

提供了返回Key和Value的函数,直接插到HashTable中即可。Next函数,因为其提供了迭代器,实现起来和前面的差不多。

NestedLoopJoin

该算子需要我们实现左连接和内连接,

plan_->predicate_->EvaluateJoin(&left_tuple_, left_schema_, &right_tuple, right_schema_).GetAs<bool>()

其实现也是有两种主要的方式,一种是将左右的tuple都读到数组中,通过下边移动来判断是否匹配,另外一种是将右边的tuple读到数组中,进行匹配。在匹配成功后构造新的Tuple,即(将左右两边的tuple的value放入vector<Value>)

std::vector<Value> value;

Tips:由于在2023spring中,增加了Init_check和判定条件,所以需要在left_tuple每次Init时,相应的right_tuple需要next以增加其各自的init和next数量。

HashJoin

我觉得这个算子应该是整个项目中最难的算子了(优化除外)。

顾名思义这边我们需要将右边tuple的value存到HashTable中,相较于2022fall,2023spring相等条件增加到了两个,所以用了两个map存储相对应的value。

因为是使用Value当作键值,但Value中没有重载==运算符,所以将Value包装到另外一个类中,重载其==运算符。同时需要添加相应的hash映射。

namespace bustub {
struct JoinKey {
  Value value_;
  auto operator==(const JoinKey &other) const -> bool { return value_.CompareEquals(other.value_) == CmpBool::CmpTrue; }
};
}  // namespace bustub
namespace std {
template <>
struct hash<bustub::JoinKey> {
  auto operator()(const bustub::JoinKey &agg_key) const -> std::size_t {
    size_t curr_hash = 0;
    if (!agg_key.value_.IsNull()) {
      curr_hash = bustub::HashUtil::CombineHashes(curr_hash, bustub::HashUtil::HashValue(&agg_key.value_));
    }
    return curr_hash;
  }
};
}  // namespace std

Init函数中将相应的value值存到map中,在Next中查询,后面的实现和上一个算子差不多。

Optimizing NestedLoopJoin to HashJoin

在实现该优化器之前可以先看看order_by_index优化器的实现,其主要的思想就是将NestedLoopJoinPlanNode修改为HashJoinPlanNode。

NestedLoopJoinPlanNode中的Expression有两种情况,一种是LogicExpression,有两个等式的逻辑表达式,一种是只含一种的ComparisonExpression的比较表达式。根据不同的情形将其改写为HashJoinPlanNode。

Task #3 - Sort + Limit Executors and Top-N Optimization

在完成了前面两个Task后,Task3是比较简单的。

Sort

直接将所有数据读进来,使用std::sort排序一下即可。根据数据库的规则依据从前往后的关键字依次进行排序,只要有一个关键字不相等即得出排序规则。

**疑惑:**其实在这个地方我还卡了一会,一致报错越内存了,后面我在每次调用Init的时候将数组clear一下就不会了。具体原因还未知。

limit

用一个数纪录当前next的个数即可。

Top-N Optimization Rule

做到后面实在是不想在写了,直接用前面Sort的代码,再截取前面n个即可。

优化:应该可以使用优先队列来写

优化

//TODO

总结

写完这个项目,自己对整个数据库的整体架构更深了一些,也知道了优化真的好难~~~~~~,一共用时6天,4.29-5.4,发现了一个Project2的BUG,在实现PageGuard中对锁重复释放了,导致了一下未定义的行为,前面实现乐观锁老是出一些莫名其妙的错误,等做完P4后再回去完成。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值