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

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

一、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 bustub

private:
  /** 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());
      // 向索引中插入键和新元组的RID
      index_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 bustub




 private:
  /** 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 bustub




 private:
  /** 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());
  // 获取表的schema
  auto 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_清空,后面负责存索引查询到的rid
  result_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 bustub



 private:
  /** The index scan plan node to be executed. */
  // 一个索引扫描操作
  const IndexScanPlanNode *plan_;
  HashTableIndexForTwoIntegerColumn *htable_;
  bool has_inserted_  ;
  // 存索引找到的rid
  std::vector<RID> result_rids_;
  TableHeap *table_heap_;

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

6 优化
6.1 思路

要求: 实现将SeqScanPlanNode优化为IndexScanPlanNode的优化器规则
思路:
只有一种情况可以把顺序的变成索引的,即以下条件同时满足
1、谓词(即where之后的语句)不为空
2、表支持索引扫描
3、只有一个谓词条件(如SELECT * FROM t1 WHERE v1 = 1 AND v2 = 2就不行)
4、谓词是等值条件(即WHERE v1 = 1)
所以要把所有条件都判断一遍,才能用索引扫描

6.2 代码
#include "execution/expressions/column_value_expression.h"
#include "execution/expressions/comparison_expression.h"
#include "execution/expressions/logic_expression.h"
#include "execution/plans/index_scan_plan.h"
#include "execution/plans/seq_scan_plan.h"
#include "optimizer/optimizer.h"
#include "storage/index/generic_key.h"

namespace bustub {

auto Optimizer::OptimizeSeqScanAsIndexScan(const bustub::AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
  // TODO(student): implement seq scan with predicate -> index scan optimizer rule
  // The Filter Predicate Pushdown has been enabled for you in optimizer.cpp when forcing starter rule
  // 对所有子节点递归应用这一优化
  std::vector<bustub::AbstractPlanNodeRef> optimized_children;
  for (const auto &child : plan->GetChildren()) {
    optimized_children.emplace_back(OptimizeSeqScanAsIndexScan(child));
  }
  // 递归转化其所有的子节点
  auto optimized_plan = plan->CloneWithChildren(std::move(optimized_children));
  // 如果plan计划为顺序扫描,则转化为索引扫描
  if (optimized_plan->GetType() == PlanType::SeqScan) {
    const auto &seq_plan = dynamic_cast<const bustub::SeqScanPlanNode &>(*optimized_plan);
    // 获取计划的谓词(where语句之后的内容)
    auto predicate = seq_plan.filter_predicate_;
    // 如果谓词为空仍然执行顺序扫描
    if (predicate != nullptr) {
      auto table_name = seq_plan.table_name_;
      // 获取表的索引,看该表是否支持索引扫描
      auto table_idx = catalog_.GetTableIndexes(table_name);
      // 将predicate转化为LogicExpression,查看是否为逻辑谓词
      auto logic_expr = std::dynamic_pointer_cast<LogicExpression>(predicate);
      // 沒有索引或者有多个谓词条件,返回顺序扫描
      if (!table_idx.empty() && !logic_expr) {
        auto equal_expr = std::dynamic_pointer_cast<ComparisonExpression>(predicate);
        // 需要判断是否为条件谓词
        if (equal_expr) {
          auto com_type = equal_expr->comp_type_;
          // 只能是等值判断才能转化为索引扫描
          if (com_type == ComparisonType::Equal) {
            // 获取表的id
            auto table_oid = seq_plan.table_oid_;
            // 返回索引扫描节点
            auto column_expr = dynamic_cast<const ColumnValueExpression &>(*equal_expr->GetChildAt(0));
            // 根据谓词的列,获取表的索引信息
            auto column_index = column_expr.GetColIdx();
            auto col_name = this->catalog_.GetTable(table_oid)->schema_.GetColumn(column_index).GetName();
            // 如果存在相关索引,获取表索引info
            for (auto *index : table_idx) {
              const auto &columns = index->index_->GetKeyAttrs();
              std::vector<uint32_t> column_ids;
              column_ids.push_back(column_index);
              if (columns == column_ids) {
                // 获取pred-key
                auto pred_key = std::dynamic_pointer_cast<ConstantValueExpression>(equal_expr->GetChildAt(1));
                // 从智能指针中获取裸指针
                ConstantValueExpression *raw_pred_key = pred_key ? pred_key.get() : nullptr;
                return std::make_shared<IndexScanPlanNode>(seq_plan.output_schema_, table_oid, index->index_oid_,
                                                           predicate, raw_pred_key);
              }
            }
          }
        }
      }
    }
  }
  return optimized_plan;
}

}  // namespace bustub

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为true
        if (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;
        // 文档要求:左连接操作要求左边的元组都存在,如果不存在满足条件的右边元组,则将右边元组的值都设置为null
        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(
              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 bustub




private:
  /** 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] 文心一言

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CMU 15445 课程的 Project 0 是一个调试练习,旨在帮助学生熟悉调试工具和技术。在这个项目中,你将开始使用 GDB 和 Valgrind 这两个常用的调试工具,以及一些其他辅助工具。以下是一些问题和步骤,帮助你完成这个练习: 1. 你需要查看项目中提供的代码,并了解它的结构和功能,这样你才能更好地理解程序的逻辑和可能出现的 bug。 2. 接下来,你需要编译项目,并确保没有编译错误。如果出现错误,你需要修复它们,这可能需要检查一些语法错误或缺失的库。 3. 一旦成功编译项目,你就可以使用 GDB 进行调试了。GDB 是一个强大的调试器,可以帮助你找出程序中的错误。你可以使用 GDB 来单步执行代码、设置断点、查看变量的值等等。通过使用 GDB,你可以逐步查看代码运行的路径,并找出程序崩溃或产生错误的原因。 4. 在使用 GDB 进行调试时,你可以通过设置断点来暂停程序的执行,并查看变量的值和程序的状态。你可以使用“break”命令在程序中设置断点,并通过“run”命令启动程序。当程序到达这个断点时,它会停止执行,你可以使用“print”命令查看变量的值,或者“step”命令逐步执行代码。 5. 另一个常用的调试工具是 Valgrind。Valgrind 可以帮助你检测内存泄漏和错误的访问方式。你可以使用“valgrind”命令来运行程序,并查看 Valgrind 的输出。它会告诉你有关程序中任何潜在问题的信息,例如未初始化的变量、访问越界等。 6. 最后,当你发现 bug 并修复它们后,可以运行各种测试用例来验证程序的正确性。测试用例可以帮助你确定程序是否按预期工作,并且在修改代码后,它们可以帮助你确保你的修复没有引入新的错误。 通过完成 CMU 15445 项目 0 的调试练习,你将掌握一些重要的调试技巧和工具,这对于进一步开发和调试软件应用程序将非常有用。希望上述步骤和建议对你有所帮助,祝你顺利完成这个项目!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值