CMU_15445_P3_Part2
Aggregation & Join Executors 实现
AggregationExecutor 的实现#
AggregationExecutor
的实现需要关注 AggregationExecutor.h
和 AggregationPlanNode
, 以理解其支持的 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
子句由 AggregationPlanNode
和 FilterPlanNode
实现, 因此无需在 AggregationPlanNode
中单独处理.
SELECT DISTINCT 语句#
SELECT DISTINCT colA, colB FROM __mock_table_1;
用于返回表 __mock_table_1
中 colA
和 colB
的唯一组合, 去除重复行.
BUSTUB 中 AggregationPlanNode 的实现#
AggregationExecutor
支持 GROUP BY 语句的实现. SQL 的执行流程是:
- 如果包含 GROUP BY, 则先对数据进行分组(GROUP).
- 然后对每组数据执行聚合操作(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 JOIN
和 LEFT JOIN
.
执行流程#
直观上 JOIN 的简单的执行流程如下:
- 从
left_executor_
和right_executor_
分别读取元组(tuple). - 根据
NestedLoopJoinPlanNode
的条件表达式predicate_
判断是否满足 JOIN 条件. - 如果满足, 则返回 JOIN 结果.
以下是示例:
SELECT *
FROM __mock_table_tas_2023_fall
INNER JOIN __mock_table_schedule_2023
ON office_hour = day_of_week
WHERE has_lecture = 1;
上述语句中的判断条件在实际执行的时候会分为:
- JOIN 条件
office_hour = day_of_week
由NestedLoopJoinPlanNode
中的predicate_
处理. - 筛选条件
WHERE has_lecture = 1
在FilterPlanNode
中处理. - 即使有时候 ON 和 WHERE 在 MySQL 查询语句的结果上一致, 但是这两种不同的书写方式实际上对应着不同的执行过程.
实现要点#
实现 NestedLoopJoinExecutor
时需要注意以下问题:
-
每次调用
Next()
只返回一个元组(tuple), 但一个left_executor_
元组(tuple)可能对应多个right_executor_
元组(tuple). -
LEFT JOIN
的特殊情况:- 如果没有匹配的
right_executor_
元组(tuple), 需要用 NULL 填充.
- 如果没有匹配的
优化实现如下:
- 初始化时, 将
left_executor_
元组(tuple)存储为数组left_tuples_
. - 使用变量
right_executor_end_
标记right_executor_
是否遍历完. - 使用
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 纪录#
- 如果
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. 还是 AggregationExecutor
的 child_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;
- 在后续的重复检测的案例中, 如果 AggregatePlanNode 被重复的多次调用, 每次都会初始化, 需要在 AggregateExecutor 的初始化中清空原来的 HashTable, 如下:
/** 清空以及初始化 HashTable, 避免重复累加 */
aht_.Clear();
NestedLoopJoinExecutor
中实际上我也遇到了不少的 BUG, 都是我在前面分析的部分, 例如一个小细节,right_executor_->Init()
, 需要对于每个left_tuples
中的 tuple 进行初始化.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构