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 进行初始化.

浙公网安备 33010602011771号