CMU_15445_P3_Part2

Aggregation & Join Executors 实现

AggregationExecutor 的实现#

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

Copy
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 语句的特点是根据某列进行分组, 然后对分组后的数据执行聚合操作. 其基本语法如下:

Copy
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, 但针对的是聚合结果.例如:

Copy
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 语句:

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

结果将会分为下列四组:

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

Copy
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 实现:

Copy
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 语句:

Copy
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 结果.

以下是示例:

Copy
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).

主要代码如下:

Copy
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 函数, 此时需要返回结果
Copy
/** 孩子节点中没有 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, 数据库没有返回, 我的实现如下, 感觉有点丑:

Copy
/** * 如果表中返回的结果是空值, 有两种情况, 一种是 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, 如下:
Copy
/** 清空以及初始化 HashTable, 避免重复累加 */ aht_.Clear();
  1. NestedLoopJoinExecutor 中实际上我也遇到了不少的 BUG, 都是我在前面分析的部分, 例如一个小细节, right_executor_->Init(), 需要对于每个 left_tuples 中的 tuple 进行初始化.
posted @   虾野百鹤  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示
CONTENTS