cmu15445 2023fall project3 详细过程(上)QUERY EXECUTION

2024-05-11 08:28

本文主要是介绍cmu15445 2023fall project3 详细过程(上)QUERY EXECUTION,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

首先,虽然会粘代码,但代码不全。
其次,文章基本没有一个字是我自己写的,代码都是粘的同门的,文字都是粘的网上的,可以看看我下面的参考文章。

QUERY EXECUTION task1/task2

    • 一、SQL语句执行流程
        • 1 总体
        • 2 例子
    • 二、bustub 表结构
        • 1 图的解读
        • 2 结合代码解读
    • Task #1 - Access Method Executors
        • 1 SeqScan
            • 1.1 思路
            • 1.2 代码
        • 2 Insert
            • 2.1 思路
            • 2.2 代码
        • 3 UpDate
            • 3.1 思路
            • 3.2 代码
        • 4 Delete
        • 5 Indexscan
            • 5.1 思路
            • 5.2 代码
    • Task #2 - Aggregation & Join Executors
        • 1 Aggregation
            • 1.1 思路
            • 1.2 代码
        • 2 NestedLoopJoin
            • 2.1 思路
            • 2.2 代码

一、SQL语句执行流程

1 总体

这部分课上有很详细的解释
在这里插入图片描述
在收到一条 SQL 语句后,查询处理层首先会通过解析器 Parser 将 SQL 语句解析为一颗抽象句法树 AST(Abstracct Syntax Tree)。接下来绑定器 Binder 会遍历这棵句法树,将表名、列名等映射到数据库中的实际对象上,并由计划器 Planner 生成初步的查询计划。查询计划会以树的形式表示,数据从叶子节点流向父节点。
在这里插入图片描述

最后,优化器 Optimizer 会优化生成最终的查询计划,然后交由查询执行层的执行器执行,而这里面的部分执行器需要我们来实现。

2 例子

(语句前面加explain可以打印出语句的查询计划)

bustub> explain select * from test_1;
/*这部分描述了查询绑定器(Binder)如何处理SQL查询。它解析了查询,并将SQL的文本表示转换为内部的数据结构。
在这个例子中,它表示查询选择了 test_1 表中的所有列(colA, colB, colC, colD)。
没有使用 GROUP BY, HAVING, WHERE, LIMIT, OFFSET, 或 ORDER BY 子句。
也不是一个DISTINCT查询,并且没有使用公共表表达式(CTE)。
*/
=== BINDER ===
BoundSelect {table=BoundBaseTableRef { table=test_1, oid=20 },columns=[test_1.colA, test_1.colB, test_1.colC, test_1.colD],groupBy=[],having=,where=,limit=,offset=,order_by=[],is_distinct=false,ctes=,
}
/*这部分展示了查询规划器(Planner)生成的查询执行计划。
Projection:这是一个投影操作,意味着它将从下一级操作(在这种情况下是 SeqScan)中选择特定的列(这里是 #0.0 到 #0.3,它们对应于 test_1 表的 colA 到 colD)。
SeqScan:这是一个顺序扫描操作,意味着它将扫描整个 test_1 表以获取数据。
*/
=== PLANNER ===
Projection { exprs=[#0.0, #0.1, #0.2, #0.3] } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER)SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER)
/*这部分展示了查询优化器(Optimizer)可能如何进一步优化查询执行计划。但在这个例子中,它似乎没有做任何优化,只是简单地返回了与Planner相同的计划。
所以,优化后的计划仍然是一个简单的顺序扫描(SeqScan)操作。
*/
=== OPTIMIZER ===
SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER)bustub> explain select * from test_1;

二、bustub 表结构

不得不说这个图真的做的太好了,这个看懂了才能做下去
在这里插入图片描述

1 图的解读

首先,Bustub 有一个 Catalog。Catalog 维护了几张 hashmap,保存了 table id 和 table name 到 table info 的映射关系。table id 由 Catalog 在新建 table 时自动分配,table name 则由用户指定。

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

table heap 是管理 table 数据的结构,包含 table 相关操作。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 本身还保存了类型信息。

2 结合代码解读

首先,我们从所有执行器的基类抽象执行器 AbstractExecutor 入手(路径: src/include/execution/executors/abstract_executor.h),抽象执行器实现了一个 tuple-at-a-time 形式的火山迭代器模型,tuple-at-a-time 表明每次调用 Next() 函数会从当前执行器获取一个 tuple,火山模型则表明数据从子执行器向父执行器传递,根节点的输出即为最终的执行结果。它主要包括 Init() 和 Next() 两个成员函数。

AbstractExecutor 只有执行器上下文 ExecutorContext 这一个成员变量(路径:src/include/execution/executor_context.h),ExecutorContext 包括当前事务 Transaction(变量名:transaction)、缓冲池管理器 BufferPoolManager(变量名:bpm)、事务管理器 TransactionManager(变量名:txn_mgr)、锁管理器 LockManager(变量名:lock_mgr) 以及目录 Catalog(变量名:catalog)。

与表管理直接相关的就是执行器上下文中的目录 Catalog(见上图),目录中主要包含了一个表标识符到表信息 TableInfo 的映射 std::unordered_map<table_oid_t, std::unique_ptr<TableInfo>> tables_; 和一个表名到表标识符的映射 std::unordered_map<std::string, table_oid_t> table_names_;(上图中绿色的tableInfo)。

表信息 TableInfo 主要保存了有关表的一些元数据,其中最主要的就是 TableHeap(路径:Identification: src/include/storage/table/table_heap_.h),TableHeap 表示磁盘上的物理表,它是一个双向链表的页集合,通过 TableHeap 即可得到首个表页面 TablePage 中首个元组 Tuple(每个元组对应表中的一行数据)的迭代器,从而实现表遍历。

建议充分了解了以后再写代码。

Task #1 - Access Method Executors

1 SeqScan
1.1 思路

Init 方法首先获取要扫描的表的堆(table_heap_),然后创建一个迭代器 iter来遍历这个堆。它遍历堆中的所有记录,并将它们的记录标识符(RID)存储在rids_列表中。最后,它将rid_iter_设置为rids_列表的起始位置。
Next方法用于从表中检索下一个满足条件的元组。它使用一个do-while循环来遍历rids_ 列表中的 RID。对于每个 RID,它首先获取该 RID 对应的元组的元数据(TupleMeta)。如果该元组没有被删除(!meta.is_deleted_),则将该元组复制到传入的 tuple参数中,并将 RID 复制到rid参数中。
然后,它检查是否有过滤条件(plan_->filter_predicate_)。如果有,它会评估这个过滤条件,并检查结果是否为 true。只有当元组没有被删除且满足过滤条件时,do-while 循环才会结束。

1.2 代码
#include "execution/executors/seq_scan_executor.h"
#include "concurrency/transaction_manager.h"namespace bustub {SeqScanExecutor::SeqScanExecutor(ExecutorContext *exec_ctx, const SeqScanPlanNode *plan): AbstractExecutor(exec_ctx), plan_(plan), table_heap_(nullptr), iter_(nullptr) {}void SeqScanExecutor::Init() {table_heap_ = GetExecutorContext()->GetCatalog()->GetTable(plan_->GetTableOid())->table_.get();auto iter = table_heap_->MakeIterator();rids_.clear();while (!iter.IsEnd()) {rids_.push_back(iter.GetRID());++iter;}rid_iter_ = rids_.begin();
}auto SeqScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {TupleMeta meta{};do {if (rid_iter_ == rids_.end()) {return false;}meta = table_heap_->GetTuple(*rid_iter_).first;if (!meta.is_deleted_) {*tuple = table_heap_->GetTuple(*rid_iter_).second;*rid = *rid_iter_;}++rid_iter_;} while (meta.is_deleted_ ||(plan_->filter_predicate_ != nullptr &&!plan_->filter_predicate_->Evaluate(tuple, GetExecutorContext()->GetCatalog()->GetTable(plan_->GetTableOid())->schema_).GetAs<bool>()));return true;
}
}  // namespace bustubprivate:/** The sequential scan plan node to be executed */const SeqScanPlanNode *plan_;std::vector<RID> rids_;std::vector<RID>::iterator rid_iter_;TableHeap *table_heap_{nullptr};std::unique_ptr<TableIterator> iter_;
};
2 Insert
2.1 思路

要求: 执行器生成一个整数类型的元组作为输出,并指示有多少行已经被插入到表中。当向表中插入数据时,如果表有关联的索引,请记得更新这些索引。
思路:把要插入的内容放在子执行器 child_executor_中,然后获取一些必要参数,其中插入到表中的行数的整数元组 和插入操作产生的下一个元组RID已经给出了。用while语句把子执行器中的每个元组都插进去(还有更新索引),最后算下一共插入多少行

2.2 代码
#include <memory>#include "execution/executors/insert_executor.h"namespace bustub {InsertExecutor::InsertExecutor(ExecutorContext *exec_ctx, const InsertPlanNode *plan,std::unique_ptr<AbstractExecutor> &&child_executor): AbstractExecutor(exec_ctx) {plan_ = plan;child_executor_ = std::move(child_executor);
}void InsertExecutor::Init() {this->child_executor_->Init();this->has_inserted_ = false;
}auto InsertExecutor::Next(Tuple *tuple, RID *rid) -> bool {//统计插入的元组数if (has_inserted_) {return false;}this->has_inserted_ = true;// count 表示插入操作期间插入的行数int count = 0;// 获取待插入的表信息及其索引列表auto table_info = this->exec_ctx_->GetCatalog()->GetTable(this->plan_->GetTableOid());auto schema = table_info->schema_;auto indexes = this->exec_ctx_->GetCatalog()->GetTableIndexes(table_info->name_);// 从子执行器 child_executor_ 中逐个获取元组并插入到表中,同时更新所有的索引// next函数是虚函数,会自动调用seq_scan中的实现  while (this->child_executor_->Next(tuple, rid)) {count++;std::optional<RID> new_rid_optional = table_info->table_->InsertTuple(TupleMeta{0, false}, *tuple);// 遍历所有索引,为每个索引更新对应的条目RID new_rid = new_rid_optional.value();for (auto &index_info : indexes) {// 从元组中提取索引键auto key = tuple->KeyFromTuple(schema, index_info->key_schema_, index_info->index_->GetKeyAttrs());// 向索引中插入键和新元组的RIDindex_info->index_->InsertEntry(key, new_rid, this->exec_ctx_->GetTransaction());}}// 创建了一个 vector对象values,其中包含了一个 Value 对象。这个 Value 对象表示一个整数值,值为 count// 这里的 tuple 不再对应实际的数据行,而是用来存储插入操作的影响行数std::vector<Value> result = {{TypeId::INTEGER, count}};*tuple = Tuple(result, &GetOutputSchema());return true;
}}  // namespace bustubprivate:/** The insert plan node to be executed*/const InsertPlanNode *plan_;// 子执行器std::unique_ptr<AbstractExecutor> child_executor_;// 看还能不能插入bool has_inserted_;
3 UpDate
3.1 思路

要求:执行器将输出一个整数类型的元组,表示已更新的行数。
思路: 更新逻辑是把原来的元组设置为已删除,然后插入新的元组。

3.2 代码
#include <memory>#include "execution/executors/update_executor.h"namespace bustub {UpdateExecutor::UpdateExecutor(ExecutorContext *exec_ctx, const UpdatePlanNode *plan,std::unique_ptr<AbstractExecutor> &&child_executor): AbstractExecutor(exec_ctx), plan_(plan), child_executor_(std::move(child_executor)) {}void UpdateExecutor::Init() {this->child_executor_->Init();this->has_inserted_ = false;
}auto UpdateExecutor::Next(Tuple *tuple, RID *rid) -> bool {// 只要执行一次next函數if (has_inserted_) {return false;}this->has_inserted_ = true;int count = 0;// 获取table_info indexes相关基本信息,后面需要使用auto table_info = this->exec_ctx_->GetCatalog()->GetTable(this->plan_->GetTableOid());auto indexes = this->exec_ctx_->GetCatalog()->GetTableIndexes(table_info->name_);Tuple child_tuple{};RID child_rid{};while (this->child_executor_->Next(&child_tuple, &child_rid)) {count++;// 更新逻辑不是直接更新相应的值,而是把原来的元组设置为已删除,然后插入新的元组// 将每个元组都标记为已删除table_info->table_->UpdateTupleMeta(TupleMeta{0, true}, child_rid);std::vector<Value> new_values{};// 预分配足够的内存空间,以便 new_values 能够在不重新分配内存的情况下存储至少.size() 个 Value 类型的元素new_values.reserve(plan_->target_expressions_.size());// 获取要插入的新元组for (const auto &expr : plan_->target_expressions_) {new_values.push_back(expr->Evaluate(&child_tuple, child_executor_->GetOutputSchema()));}auto update_tuple = Tuple{new_values, &table_info->schema_};// 插入新的元組std::optional<RID> new_rid_optional = table_info->table_->InsertTuple(TupleMeta{0, false}, update_tuple);// 遍历所有索引,为每个索引更新对应的条目RID new_rid = new_rid_optional.value();// 更新所有相关索引,需要先删除直接的索引,然后插入新的索引信息for (auto &index_info : indexes) {auto index = index_info->index_.get();auto key_attrs = index_info->index_->GetKeyAttrs();auto old_key = child_tuple.KeyFromTuple(table_info->schema_, *index->GetKeySchema(), key_attrs);auto new_key = update_tuple.KeyFromTuple(table_info->schema_, *index->GetKeySchema(), key_attrs);// 从索引中删除旧元组的条目index->DeleteEntry(old_key, child_rid, this->exec_ctx_->GetTransaction());// 向索引中插入新元组的条目index->InsertEntry(new_key, new_rid, this->exec_ctx_->GetTransaction());}}std::vector<Value> result = {{TypeId::INTEGER, count}};*tuple = Tuple(result, &GetOutputSchema());return true;
}
}  // namespace bustubprivate:/** The update plan node to be executed */const UpdatePlanNode *plan_;/** Metadata identifying the table that should be updated */const TableInfo *table_info_;/** The child executor to obtain value from */std::unique_ptr<AbstractExecutor> child_executor_;bool has_inserted_;
4 Delete

没什么说的,和update差不多,把更新的部分删掉,删除的部分保留就行

5 Indexscan
5.1 思路

要求: 使用哈希索引执行点查找以检索元组的RID。然后逐个发出这些元组。
思路: 把索引得到的值放到result_rids里,然后判断result_rids 是不是空,删除标志,都不是就把数据和RID分别赋值给传入的 tuple 和 rid 指针所指向的变量

5.2 代码
namespace bustub {
IndexScanExecutor::IndexScanExecutor(ExecutorContext *exec_ctx, const IndexScanPlanNode *plan): AbstractExecutor(exec_ctx) {this->plan_ = plan;
}void IndexScanExecutor::Init() {// 获取与计划节点中指定的表ID对应的表信息auto table_info = exec_ctx_->GetCatalog()->GetTable(plan_->table_oid_);table_heap_ = table_info->table_.get();// 获取与计划节点中指定的索引ID对应的索引信息auto index_info = exec_ctx_->GetCatalog()->GetIndex(plan_->index_oid_);// 把索引信息转化为哈希索引this->htable_ = dynamic_cast<HashTableIndexForTwoIntegerColumn *>(index_info->index_.get());// 获取表的schemaauto table_schema = index_info->key_schema_;// 获取键值auto key = plan_->pred_key_;auto value = key->val_;std::vector<Value> values{value};// 使用索引的键模式创建键Tuple index_key(values, &table_schema);// result_rids_清空,后面负责存索引查询到的ridresult_rids_.clear();// 执行索引扫描并填充result_rids_列表htable_->ScanKey(index_key, &result_rids_, exec_ctx_->GetTransaction());has_inserted_ = false;
}auto IndexScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {if (has_inserted_) {return false;}has_inserted_ = true;// 没有找到相关的元组信息if (result_rids_.empty()) {return false;}TupleMeta meta{};meta = table_heap_->GetTuple(*result_rids_.begin()).first;// 确保索引到的元组是没有被删除的if (!meta.is_deleted_) {*tuple = table_heap_->GetTuple(*result_rids_.begin()).second;*rid = *result_rids_.begin();}return true;
}
}  // namespace bustubprivate:/** The index scan plan node to be executed. */// 一个索引扫描操作const IndexScanPlanNode *plan_;HashTableIndexForTwoIntegerColumn *htable_;bool has_inserted_  ;// 存索引找到的ridstd::vector<RID> result_rids_;TableHeap *table_heap_;

task1 完成后,sql语句的1-6就都可以测试了

Task #2 - Aggregation & Join Executors

1 Aggregation
1.1 思路

注意:聚合在查询计划中是管道中断者,聚合操作不能简单地按行(tuple-by-tuple)从其子节点(或数据源)接收数据,然后逐行输出结果。
相反,聚合操作需要先从其子节点获取所有相关的数据,完成整个聚合计算过程(例如计算总和、平均值、最小/最大值等),然后才能产生输出结果
AggregationExecutor 需要先从子执行器中获取所有数据,然后对这些数据进行分组和聚合操作,
最后将结果输出,而这个过程必须在init函数中完成
思路:
在这里插入图片描述
我们以分组聚合查询语句 select count(name) from table group by camp; 为例简要说明一下聚合执行器的执行流程:
Init() 函数首先从子执行器中逐行获取数据,并根据每行数据构建聚合键和聚合值。其中聚合键用于标识该行数据属于哪一个聚合组,这里是按照阵营 camp 分组,因此聚合键会有 Piltover、Ionia 和 Shadow Isles 三种取值,这样所有数据被分成三个聚合组。而聚合值就是待聚合的列的值,这里的聚合列是 name,因此这五个 Tuple 中生成的聚合值即为对应的 name 属性的值。
对于每个提取的数据行,Init() 函数还会通过 InsertCombine(), 将相应的聚合值聚合到到相应的聚合组中。在InsertCombine()中调用CombineAggregateValues() 函数来实现具体的聚合规则。
经过 Init() 函数的处理,以上六条数据会被整理为 [{“Piltover”: 3}, {“Ionia”: 2}, {“Shadow Isles”: 1}] 三个聚合组(对应于聚合哈希表中的三个键值对)。其中groupby的值分别为Piltover、Ionia、Shadow Isles;aggregate的值分别为3、2、1。
最后,Next() 函数会通过哈希迭代器依次获取每个聚合组的键与值,返回给父执行器。如果没 group by 子句,那么所有数据都会被分到同一个聚合组中并返回。

1.2 代码
#include <memory>
#include <vector>
#include "execution/executors/aggregation_executor.h"namespace bustub {AggregationExecutor::AggregationExecutor(ExecutorContext *exec_ctx, const AggregationPlanNode *plan,std::unique_ptr<AbstractExecutor> &&child_executor): AbstractExecutor(exec_ctx) {this->plan_ = plan;this->child_executor_ = std::move(child_executor);
}void AggregationExecutor::Init() {child_executor_->Init();//获取聚合表达式auto agg_exprs = plan_->GetAggregates();//获取聚合类型auto agg_types = plan_->GetAggregateTypes();// 根据聚合表达式以及聚合类型创建哈希表// SimpleAggregationHashTable 是一个在聚合查询中专为计算聚合设计的散列表(哈希表)// 它用于快速地分组数据,并对每个分组应用聚合函数。aht_ = std::make_unique<SimpleAggregationHashTable>(plan_->GetAggregates(), plan_->GetAggregateTypes());// NEXT方法的輸出參數,用于存储查询结果Tuple child_tuple{};RID rid{};// 遍历子执行器,将子执行器中的获取的数据插入到聚合哈希表中// 不能在聚合执行器中完成,因为聚合执行器需要先从子执行器中获取所有数据,然后对这些数据进行分组和聚合操作,最后才能产生输出结果while (child_executor_->Next(&child_tuple, &rid)) {// 通过tuple获取聚合键和聚合值// 聚合键在聚合操作中用来区分不同的分组auto agg_key = MakeAggregateKey(&child_tuple);auto agg_val = MakeAggregateValue(&child_tuple);// 将聚合键和聚合值插入到聚合哈希表中aht_->InsertCombine(agg_key, agg_val);}// 一个指向哈希表开始的迭代器,后面用于遍历哈希表并生成聚合查询的结果。aht_iterator_ = std::make_unique<SimpleAggregationHashTable::Iterator>(aht_->Begin());has_inserted_ = false;
}auto AggregationExecutor::Next(Tuple *tuple, RID *rid) -> bool {if (aht_->Begin() != aht_->End()) {if (*aht_iterator_ == aht_->End()) {return false;}// 获取聚合键和聚合值auto agg_key = aht_iterator_->Key();auto agg_val = aht_iterator_->Val();//根据聚合键和聚合值生成查询结果元组std::vector<Value> values{};// 遍历聚合键和聚合值,生成查询结果元组// 根据文件要求,有groupby和aggregate两个部分的情况下,groupby也要算上,都添加到value中values.reserve(agg_key.group_bys_.size() + agg_val.aggregates_.size());for (auto &group_values : agg_key.group_bys_) {values.emplace_back(group_values);}for (auto &agg_value : agg_val.aggregates_) {values.emplace_back(agg_value);}*tuple = {values, &GetOutputSchema()};//迭代到下一个聚合键和聚合值++*aht_iterator_;// 表示成功返回了一个聚合结果return true;}if (has_inserted_) {return false;}has_inserted_ = true;// 没有groupby语句则生成一个初始的聚合值元组并返回if (plan_->GetGroupBys().empty()) {std::vector<Value> values{};Tuple tuple_buffer{};// 检查当前表是否为空,如果为空生成默认的聚合值,对于 count(*) 来说是 0,对于其他聚合函数来说是 integer_null(// 默认聚合值要求由GenerateInitialAggregateValue实现for (auto &agg_value : aht_->GenerateInitialAggregateValue().aggregates_) {values.emplace_back(agg_value);}*tuple = {values, &GetOutputSchema()};return true;}return false;
}auto AggregationExecutor::GetChildExecutor() const -> const AbstractExecutor * { return child_executor_.get(); }}  // namespace bustub
/*** TODO(Student)** Combines the input into the aggregation result.* @param[out] result The output aggregate value* @param input The input value*/void CombineAggregateValues(AggregateValue *result, const AggregateValue &input) {// 依照不同的聚合操作类型(agg_types_)进行不同的操作for (uint32_t i = 0; i < agg_exprs_.size(); i++) {Value &old_val = result->aggregates_[i];const Value &new_val = input.aggregates_[i];switch (agg_types_[i]) {//无论Value是否为null,均统计其数目case AggregationType::CountStarAggregate:old_val = old_val.Add(Value(TypeId::INTEGER, 1));break;//统计非null值case AggregationType::CountAggregate:if (!new_val.IsNull()) {if (old_val.IsNull()) {old_val = ValueFactory::GetIntegerValue(0);}old_val = old_val.Add(Value(TypeId::INTEGER, 1));}break;case AggregationType::SumAggregate:if (!new_val.IsNull()) {if (old_val.IsNull()) {old_val = new_val;} else {old_val = old_val.Add(new_val);}}break;case AggregationType::MinAggregate:if (!new_val.IsNull()) {if (old_val.IsNull()) {old_val = new_val;} else {old_val = new_val.CompareLessThan(old_val) == CmpBool::CmpTrue ? new_val.Copy() : old_val;}}break;case AggregationType::MaxAggregate:if (!new_val.IsNull()) {if (old_val.IsNull()) {old_val = new_val;} else {old_val = new_val.CompareGreaterThan(old_val) == CmpBool::CmpTrue ? new_val.Copy() : old_val;}}break;}}}/*** Inserts a value into the hash table and then combines it with the current aggregation.* @param agg_key the key to be inserted* @param agg_val the value to be inserted*/void InsertCombine(const AggregateKey &agg_key, const AggregateValue &agg_val) {if (ht_.count(agg_key) == 0) {ht_.insert({agg_key, GenerateInitialAggregateValue()});}CombineAggregateValues(&ht_[agg_key], agg_val);}
private:/** The aggregation plan node */const AggregationPlanNode *plan_;/** The child executor that produces tuples over which the aggregation is computed */std::unique_ptr<AbstractExecutor> child_executor_;/** Simple aggregation hash table */// TODO(Student): Uncomment SimpleAggregationHashTable aht_;std::unique_ptr<SimpleAggregationHashTable> aht_;/** Simple aggregation hash table iterator */// TODO(Student): Uncomment SimpleAggregationHashTable::Iterator aht_iterator_;std::unique_ptr<SimpleAggregationHashTable::Iterator> aht_iterator_;bool has_inserted_;
2 NestedLoopJoin
2.1 思路

实现内连接和左外连接
左外连接:左边确定找右边,分两种情况:1、找得到:把两个一样的放一块。2、找不到:左边写好右边放null。
内连接:也是左边确定找右边,找到就放value里,找不到就不放。

2.2 代码
#include "execution/executors/nested_loop_join_executor.h"
#include "binder/table_ref/bound_join_ref.h"
#include "common/exception.h"
#include "type/value_factory.h"namespace bustub {NestedLoopJoinExecutor::NestedLoopJoinExecutor(ExecutorContext *exec_ctx, const NestedLoopJoinPlanNode *plan,std::unique_ptr<AbstractExecutor> &&left_executor,std::unique_ptr<AbstractExecutor> &&right_executor): AbstractExecutor(exec_ctx) {this->plan_ = plan;this->left_executor_ = std::move(left_executor);this->right_executor_ = std::move(right_executor);if (!(plan->GetJoinType() == JoinType::LEFT || plan->GetJoinType() == JoinType::INNER)) {// Note for 2023 Fall: You ONLY need to implement left join and inner join.throw bustub::NotImplementedException(fmt::format("join type {} not supported", plan->GetJoinType()));}
}void NestedLoopJoinExecutor::Init() {left_executor_->Init();right_executor_->Init();left_bool_ = left_executor_->Next(&left_tuple_, &left_rid_);has_done_ = false;
}auto NestedLoopJoinExecutor::Next(Tuple *tuple, RID *rid) -> bool {Tuple right_tuple{};RID right_rid{};auto predicate = plan_->Predicate();if (plan_->GetJoinType() == JoinType::LEFT) {// 左连接while (left_bool_) {while (right_executor_->Next(&right_tuple, &right_rid)) {// 判断是否满足连接条件auto join_result = predicate->EvaluateJoin(&left_tuple_, left_executor_->GetOutputSchema(), &right_tuple,right_executor_->GetOutputSchema());// 对每一个左边的元组进行判断右边有没有和它匹配的,若有join_result.GetAs为trueif (join_result.GetAs<bool>()) {std::vector<Value> values;// 一个元组对应多个值,要把所有值都放进去for (uint32_t i = 0; i < this->left_executor_->GetOutputSchema().GetColumnCount(); i++) {values.emplace_back(left_tuple_.GetValue(&this->left_executor_->GetOutputSchema(), i));}// 连接操作右边元组的值均不为null的情况下for (uint32_t i = 0; i < this->right_executor_->GetOutputSchema().GetColumnCount(); i++) {values.emplace_back(right_tuple.GetValue(&this->right_executor_->GetOutputSchema(), i));}*tuple = Tuple{values, &GetOutputSchema()};has_done_ = true;return true;}}// has done为flase证明左表元组的找不到右边可以匹配的if (!has_done_) {std::vector<Value> values;// 文档要求:左连接操作要求左边的元组都存在,如果不存在满足条件的右边元组,则将右边元组的值都设置为nullfor (uint32_t i = 0; i < this->left_executor_->GetOutputSchema().GetColumnCount(); i++) {values.emplace_back(left_tuple_.GetValue(&this->left_executor_->GetOutputSchema(), i));}// 连接操作右边元组的值均为nullfor (uint32_t i = 0; i < this->right_executor_->GetOutputSchema().GetColumnCount(); i++) {values.emplace_back(ValueFactory::GetNullValueByType(this->right_executor_->GetOutputSchema().GetColumn(i).GetType()));}*tuple = Tuple{values, &GetOutputSchema()};has_done_ = true;return true;}// 接着匹配左表的下个元组left_bool_ = left_executor_->Next(&this->left_tuple_, &this->left_rid_);right_executor_->Init();has_done_ = false;}return false;}// 内连接和左外连接差不多
}  // namespace bustubprivate:/** The NestedLoopJoin plan node to be executed. */// 要执行的连接计划const NestedLoopJoinPlanNode *plan_;// 子执行器,用于生成连接左侧的元组std::unique_ptr<AbstractExecutor> left_executor_;// 子执行器,用于生成连接右侧的元组std::unique_ptr<AbstractExecutor> right_executor_;Tuple left_tuple_{};RID left_rid_{};bool has_inserted_;// 记录是否已经成功获取元组bool left_bool_;

task2 完成后,sql语句的7-12就都可以测试了

参考文章:
[1]https://zhuanlan.zhihu.com/p/570917775(BusTub 养成记:从课程项目到 SQL 数据库)
[2]https://zhuanlan.zhihu.com/p/587566135(做个数据库:2022 CMU15-445 Project3 Query Execution)
[3]https://blog.csdn.net/laiyuhua120/article/details/130494964(CMU 15445 P3 Query Execution)
[4] https://blog.csdn.net/qq_43686863/article/details/132711982?spm=1001.2014.3001.5506(CMU 15-445 Project #3 - Query Execution(Task #1、Task #2))
[5]https://zhuanlan.zhihu.com/p/690608079?(cmu15445fall2022笔记(完结撒花))
[6] https://blog.csdn.net/Tianweidadada/article/details/125340858?spm=1001.2014.3001.5506(记录一下 CMU 15445 项目)
[7] 文心一言

这篇关于cmu15445 2023fall project3 详细过程(上)QUERY EXECUTION的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/979015

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

沁恒CH32在MounRiver Studio上环境配置以及使用详细教程

目录 1.  RISC-V简介 2.  CPU架构现状 3.  MounRiver Studio软件下载 4.  MounRiver Studio软件安装 5.  MounRiver Studio软件介绍 6.  创建工程 7.  编译代码 1.  RISC-V简介         RISC就是精简指令集计算机(Reduced Instruction SetCom

arduino ide安装详细步骤

​ 大家好,我是程序员小羊! 前言: Arduino IDE 是一个专为编程 Arduino 微控制器设计的集成开发环境,使用起来非常方便。下面将介绍如何在不同平台上安装 Arduino IDE 的详细步骤,包括 Windows、Mac 和 Linux 系统。 一、在 Windows 上安装 Arduino IDE 1. 下载 Arduino IDE 打开 Arduino 官网

Solr 使用Facet分组过程中与分词的矛盾解决办法

对于一般查询而言  ,  分词和存储都是必要的  .  比如  CPU  类型  ”Intel  酷睿  2  双核  P7570”,  拆分成  ”Intel”,”  酷睿  ”,”P7570”  这样一些关键字并分别索引  ,  可能提供更好的搜索体验  .  但是如果将  CPU  作为 Facet  字段  ,  最好不进行分词  .  这样就造成了矛盾  ,  解决方法

GPT系列之:GPT-1,GPT-2,GPT-3详细解读

一、GPT1 论文:Improving Language Understanding by Generative Pre-Training 链接:https://cdn.openai.com/research-covers/languageunsupervised/language_understanding_paper.pdf 启发点:生成loss和微调loss同时作用,让下游任务来适应预训

多路转接之select(fd_set介绍,参数详细介绍),实现非阻塞式网络通信

目录 多路转接之select 引入 介绍 fd_set 函数原型 nfds readfds / writefds / exceptfds readfds  总结  fd_set操作接口  timeout timevalue 结构体 传入值 返回值 代码 注意点 -- 调用函数 select的参数填充  获取新连接 注意点 -- 通信时的调用函数 添加新fd到

Python:豆瓣电影商业数据分析-爬取全数据【附带爬虫豆瓣,数据处理过程,数据分析,可视化,以及完整PPT报告】

**爬取豆瓣电影信息,分析近年电影行业的发展情况** 本文是完整的数据分析展现,代码有完整版,包含豆瓣电影爬取的具体方式【附带爬虫豆瓣,数据处理过程,数据分析,可视化,以及完整PPT报告】   最近MBA在学习《商业数据分析》,大实训作业给了数据要进行数据分析,所以先拿豆瓣电影练练手,网络上爬取豆瓣电影TOP250较多,但对于豆瓣电影全数据的爬取教程很少,所以我自己做一版。 目

【详细介绍一下GEE】

GEE(Google Earth Engine)是一个强大的云计算平台,它允许用户处理和分析大规模的地球科学数据集,如卫星图像、气候模型输出等。以下是对GEE用法的详细介绍: 一、平台访问与账户设置 访问GEE平台: 用户可以通过访问Google Earth Engine的官方网站来开始使用GEE。 创建账户: 用户需要注册并登录Google账户,然后申请访问GEE平台。申请过程可能需要提