CMU_15445_P3_Part2

Aggregation & Join Executors 实现

AggregationExecutor 的实现

AggregationExecutor 的实现需要关注 AggregationExecutor.hAggregationPlanNode, 以理解其支持的 SQL 语句及其执行方式.在 BUSTUB 中, AggregationExecutor 支持以下类型的 SQL 语句:

EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
EXPLAIN SELECT COUNT(colA), MIN(colB) FROM __mock_table_1;
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA HAVING MAX(colB) > 10;
EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;

GROUP BY 语句

在 MySQL 中, GROUP BY 语句的特点是根据某列进行分组, 然后对分组后的数据执行聚合操作. 其基本语法如下:

SELECT column_name, aggregate_function(column_name)
FROM table_name
GROUP BY column_name;
  • column_name: 分组依据的列.
  • aggregate_function(): 聚合函数, 如 COUNT()SUM()AVG()MAX()MIN().
  • table_name: 数据表名称.

GROUP BY + HAVING 语句

HAVING 子句用于对分组后的数据进行过滤, 其作用类似于 WHERE, 但针对的是聚合结果.例如:

SELECT category, SUM(amount) AS total_amount
FROM sales
GROUP BY category
HAVING total_amount > 50;

在本项目中, HAVING 子句由 AggregationPlanNodeFilterPlanNode 实现, 因此无需在 AggregationPlanNode 中单独处理.

SELECT DISTINCT 语句

SELECT DISTINCT colA, colB FROM __mock_table_1; 用于返回表 __mock_table_1colAcolB 的唯一组合, 去除重复行.

BUSTUB 中 AggregationPlanNode 的实现

AggregationExecutor 支持 GROUP BY 语句的实现. SQL 的执行流程是:

  1. 如果包含 GROUP BY, 则先对数据进行分组(GROUP).
  2. 然后对每组数据执行聚合操作(Aggregate).

在 BUSTUB 中, 分组操作通过哈希表实现, 聚合操作则在分组结果基础上完成.以下是一个示例:

Employee Name Department Job Title
Alice HR Manager
Bob Engineering Engineer
Carol HR Assistant
David Engineering Engineer
Eve Engineering Manager
Frank HR Manager

执行以下 GROUP BY 语句:

SELECT department, job_title, COUNT(*) AS employee_count
FROM employees
GROUP BY department, job_title;

结果将会分为下列四组:

在 BUSTUB 中, 分组键 (AggregateKey) 定义如下:

struct AggregateKey {
  std::vector<Value> group_bys_;

  auto operator==(const AggregateKey &other) const -> bool {
    for (uint32_t i = 0; i < other.group_bys_.size(); i++) {
      if (group_bys_[i].CompareEquals(other.group_bys_[i]) != CmpBool::CmpTrue) {
        return false;
      }
    }
    return true;
  }
};

对于每个元组(tuple):

  • 使用 MakeAggregateKey(&child_tuple) 生成分组键.
  • 使用 MakeAggregateValue(&child_tuple) 获取聚合值.

插入和合并操作通过 InsertCombine 实现:

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);
}

因此实际的执行流程就是:
首先获取整个 HashTable, 得到所有分组以及对应的键值对, 然后遍历这些键值对, 使用上述的 InsertCombine() 得到最后的返回值.

NestedLoopJoinExecutor

在 BUSTUB 中, NestedLoopJoinExecutor 支持以下 JOIN 语句:

EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE;

NestedLoopJoinExecutor 支持 INNER JOINLEFT JOIN.

执行流程

直观上 JOIN 的简单的执行流程如下:

  1. left_executor_right_executor_ 分别读取元组(tuple).
  2. 根据 NestedLoopJoinPlanNode 的条件表达式 predicate_ 判断是否满足 JOIN 条件.
  3. 如果满足, 则返回 JOIN 结果.

以下是示例:

SELECT *
FROM __mock_table_tas_2023_fall
INNER JOIN __mock_table_schedule_2023
ON office_hour = day_of_week
WHERE has_lecture = 1;

上述语句中的判断条件在实际执行的时候会分为:

  1. JOIN 条件 office_hour = day_of_weekNestedLoopJoinPlanNode 中的 predicate_ 处理.
  2. 筛选条件 WHERE has_lecture = 1FilterPlanNode 中处理.
  3. 即使有时候 ON 和 WHERE 在 MySQL 查询语句的结果上一致, 但是这两种不同的书写方式实际上对应着不同的执行过程.

实现要点

实现 NestedLoopJoinExecutor 时需要注意以下问题:

  1. 每次调用 Next() 只返回一个元组(tuple), 但一个 left_executor_ 元组(tuple)可能对应多个 right_executor_ 元组(tuple).

  2. LEFT JOIN
    的特殊情况:

    • 如果没有匹配的 right_executor_ 元组(tuple), 需要用 NULL 填充.

优化实现如下:

  1. 初始化时, 将 left_executor_ 元组(tuple)存储为数组 left_tuples_.
  2. 使用变量 right_executor_end_ 标记 right_executor_ 是否遍历完.
  3. 使用 join_without_null_ 标记是否存在匹配的 right_executor_ 元组(tuple).

主要代码如下:

auto NestedLoopJoinExecutor::Next(Tuple *tuple, RID *rid) -> bool {
  while (left_tuple_iter_ != left_tuples_.end()) {
    /** 
     * 如果右孩子节点已经遍历完, 重新初始化右孩子节点
     * 每一个左孩子 tuple 最多与一个右孩子 tuple 匹配, 因此也需要重新初始化 join_without_null_
     */
    if(right_executor_end_) {
      right_executor_end_ = false;
      join_without_null_ = false;
      right_executor_->Init();
    }
    Tuple right_tuple;
    const auto &left_tuple = *left_tuple_iter_;
    /** 
     * 每次使用 Next() 函数获取一个 tuple, right_executor 使用 Next() 函数是每次调用
     * NestedLoopJoinExecutor::Next 时可以获取一个匹配的 tuple
     */
    while (right_executor_->Next(&right_tuple, rid)) {
      if (plan_->predicate_->EvaluateJoin(&left_tuple, left_executor_->GetOutputSchema(), &right_tuple, right_executor_->GetOutputSchema()).GetAs<bool>()) {
        std::vector<Value> values;
        for (size_t left_col_idx = 0; left_col_idx < left_executor_->GetOutputSchema().GetColumnCount(); left_col_idx++) {
          values.push_back(left_tuple.GetValue(&left_executor_->GetOutputSchema(), left_col_idx));
        }
        for (size_t right_col_idx = 0; right_col_idx < right_executor_->GetOutputSchema().GetColumnCount(); right_col_idx++) {
          values.push_back(right_tuple.GetValue(&right_executor_->GetOutputSchema(), right_col_idx));
        }
        *tuple = Tuple(values, &plan_->OutputSchema());
        join_without_null_ = true;
        return true;
      }
    }
    /** 右孩子节点遍历完, 对下一个左孩子节点进行遍历 */
    right_executor_end_ = true;
    left_tuple_iter_++;
    /** 
     * 如果是 LEFT JOIN 即使右孩子节点没有匹配的 tuple, 会使用 NULL 作为输出
     * 但是如果有匹配的 tuple, 并且已经匹配过, 那么不需要输出, 使用后 join_without_null_ 判断
     */
    if(!join_without_null_ && plan_->GetJoinType() == JoinType::LEFT) {
      std::vector<Value> values;
      for (size_t left_col_idx = 0; left_col_idx < left_executor_->GetOutputSchema().GetColumnCount(); left_col_idx++) {
        values.push_back(left_tuple.GetValue(&left_executor_->GetOutputSchema(), left_col_idx));
      }
      for(size_t right_col_idx = 0; right_col_idx < right_executor_->GetOutputSchema().GetColumnCount(); right_col_idx++) {
        values.push_back(ValueFactory::GetNullValueByType(right_executor_->GetOutputSchema().GetColumn(right_col_idx).GetType()));
      }
      *tuple = Tuple(values, &plan_->OutputSchema());
      return true;
    }
  }
  return false;
}

总结

在 Part1 完成之后, 本次的 Part2 的完成要稍微简单一点了, 当对 Next() 函数熟悉之后, 以及每次调用 Next() 函数的原理熟悉之后, 主要就是在 MySQL 的语法上面了, 还有就是一些细节方面的问题, 下次还是得先做好笔记, 先准备好, 然后再写代码.

BUG 纪录

  1. 如果 AggregationExecutor 的孩子节点为空, 没有返回 tuple, 此时如果没有 GROUP BY 而是对全局使用的 Aggregate 函数, 此时需要返回结果
  /** 孩子节点中没有 tuple 返回, 但是需要将 Aggregate HashTable 初始化, 返回初始化的值 */
  AggregateKey init_aggregate_key;
  /** 初始化的值是空值 */
  AggregateValue init_aggregate_value = aht_.GenerateInitialAggregateValue();
  aht_.InsertCombine(init_aggregate_key, init_aggregate_value);
  aht_iterator_ = aht_.Begin();

返回的是初始化的结果, 例如 MAX(), SUM(), MIN() 返回的是 NULL, COUNT(*) 返回的是 0.
2. 还是 AggregationExecutorchild_executor_ 返回为空的情况下, 此时如果有 GROUP BY 语句, 但是 GROUP BY 的分组为 0, 表示无法分组, 此时应该返回空, 而不是 NULL, 数据库没有返回, 我的实现如下, 感觉有点丑:

  /** 
    * 如果表中返回的结果是空值, 有两种情况, 一种是 GROUP BY 没有分组, 应该直接返回错误
    * 另一种是没有使用 GROUP BY, 对全局使用 Aggregate 函数, 因此还是会有输出
    */
  if(init_aggregate_value.aggregates_.size() != (GetOutputSchema()).GetLength()) {
    return false;
  }
  *tuple = Tuple(aht_iterator_.Val().aggregates_, &GetOutputSchema());
  ++aht_iterator_;
  return true;
  1. 在后续的重复检测的案例中, 如果 AggregatePlanNode 被重复的多次调用, 每次都会初始化, 需要在 AggregateExecutor 的初始化中清空原来的 HashTable, 如下:
/** 清空以及初始化 HashTable, 避免重复累加 */
  aht_.Clear();
  1. NestedLoopJoinExecutor 中实际上我也遇到了不少的 BUG, 都是我在前面分析的部分, 例如一个小细节, right_executor_->Init(), 需要对于每个 left_tuples 中的 tuple 进行初始化.
posted @ 2025-01-02 16:27  虾野百鹤  阅读(68)  评论(0)    收藏  举报