本实验的目标是为 OneBase 数据库实现 查询执行引擎 (Query Execution Engine)。你将实现 11 个查询执行算子 (Executor),使数据库能够执行 SQL 查询语句,包括顺序扫描、索引扫描、插入、删除、更新、连接、聚合、排序、Limit 和投影。
你将实现以下执行器:
| # | 执行器 | 文件 |
|---|---|---|
| 1 | Sequential Scan (顺序扫描) | src/execution/executors/seq_scan_executor.cpp |
| 2 | Index Scan (索引扫描) | src/execution/executors/index_scan_executor.cpp |
| 3 | Insert (插入) | src/execution/executors/insert_executor.cpp |
| 4 | Delete (删除) | src/execution/executors/delete_executor.cpp |
| 5 | Update (更新) | src/execution/executors/update_executor.cpp |
| 6 | Nested Loop Join (嵌套循环连接) | src/execution/executors/nested_loop_join_executor.cpp |
| 7 | Hash Join (哈希连接) | src/execution/executors/hash_join_executor.cpp |
| 8 | Aggregation (聚合) | src/execution/executors/aggregation_executor.cpp |
| 9 | Sort (排序) | src/execution/executors/sort_executor.cpp |
| 10 | Limit (限制行数) | src/execution/executors/limit_executor.cpp |
| 11 | Projection (投影) | src/execution/executors/projection_executor.cpp |
OneBase 使用 Volcano 迭代器模型 (iterator model),也称为"火山模型"。每个执行器实现两个方法:
class AbstractExecutor {
virtual void Init() = 0; // 初始化/重置执行器
virtual auto Next(Tuple *tuple, RID *rid) -> bool = 0; // 获取下一行
};工作流程:
SELECT id FROM students WHERE age > 20 ORDER BY id LIMIT 5;
┌────────────┐
│ Projection │ ← 投影:只选取 id 列
└─────┬──────┘
│ Next()
┌─────▼─────┐
│ Limit │ ← 限制输出行数
└─────┬─────┘
│ Next()
┌─────▼─────┐
│ Sort │ ← 物化全部子结果,排序
└─────┬─────┘
│ Next()
┌─────▼─────┐
│ SeqScan │ ← 扫描 students 表,过滤 age > 20
└───────────┘
每个执行器通过调用子执行器的 Next() 来获取输入行,一次一行,形成自底向上的"拉取"(pull) 模式。
大多数执行器是流式的——一次处理一行数据(如 SeqScan、Limit、Projection)。但有些执行器需要先物化全部子数据才能开始输出:
| 类型 | 执行器 | 原因 |
|---|---|---|
| 流式 | SeqScan, Insert, Delete, Update, Limit, Projection | 逐行处理 |
| 物化 | Sort, Aggregation | 排序需要看全部数据,聚合需要分组 |
| 物化 | Hash Join | Build 阶段需要全部左表数据 |
| 物化 | Nested Loop Join | 对左表每一行要重扫右表 |
Catalog(目录):
auto GetTable(table_oid_t oid) -> TableInfo *;
auto GetTableIndexes(const std::string &table_name) -> std::vector<IndexInfo *>;TableHeap(堆表):
auto InsertTuple(const Tuple &tuple) -> std::optional<RID>;
void UpdateTuple(const Tuple &new_tuple, const RID &rid);
void DeleteTuple(const RID &rid);
auto GetTuple(const RID &rid) -> Tuple;
auto Begin() -> Iterator;
auto End() -> Iterator;Expression(表达式求值):
// 单表表达式(用于 SeqScan、Sort、Projection 等)
auto Evaluate(const Tuple *tuple, const Schema *schema) -> Value;
// 双表表达式(用于 Join)
auto EvaluateJoin(const Tuple *left, const Schema *left_schema,
const Tuple *right, const Schema *right_schema) -> Value;ExecutorContext:
auto GetCatalog() -> Catalog *;
auto GetBufferPoolManager() -> BufferPoolManager *;
auto GetTransaction() -> Transaction *;创建新 Tuple 的方式:
// 从 Value 列表构造
std::vector<Value> values = {Value(TypeId::INTEGER, 42), Value(TypeId::VARCHAR, "hello")};
Tuple new_tuple(std::move(values));
// 通过表达式求值构造更新后的 Tuple
const auto &exprs = plan_->GetUpdateExpressions();
std::vector<Value> new_values;
for (const auto &expr : exprs) {
new_values.push_back(expr->Evaluate(&old_tuple, &schema));
}
Tuple updated(std::move(new_values));建议实现顺序: SeqScan → Insert → Delete → Update → NLJ → HashJoin → Aggregation → Sort → Limit → Projection → IndexScan
每个执行器需要实现 Init() 和 Next() 两个方法。
顺序扫描表中所有行,并应用可选的谓词 (predicate) 进行过滤。
从子执行器获取待插入的行,插入到目标表中,同时更新所有相关索引。只调用一次 Next(),返回插入行数。
从子执行器获取待删除的行,从目标表中删除。只调用一次 Next(),返回删除行数。
从子执行器获取旧行,使用更新表达式计算新值,执行更新。只调用一次 Next(),返回更新行数。
实现嵌套循环连接。对于左表的每一行,扫描右表的全部行,输出满足连接谓词的行组合。
实现哈希连接。Build 阶段建立左表的哈希表,Probe 阶段逐行扫描右表并通过哈希表查找匹配。
实现分组聚合,支持 COUNT(*)、COUNT、SUM、MIN、MAX。需要处理 GROUP BY 分组、空输入的默认值。
物化所有子数据,按照 ORDER BY 表达式排序。支持多列排序和升/降序。
限制输出行数,透传前 N 行后停止。
对子执行器输出的每一行进行投影,根据投影表达式列表计算出新的列值,生成只包含所选列/表达式的新 Tuple。用于 SELECT col1, col2, expr... 这类非 SELECT * 的列选择查询。
使用 B+ 树索引扫描符合条件的行。
学生应主要在以下执行层文件中完成实现:
src/execution/executors/seq_scan_executor.cppsrc/execution/executors/index_scan_executor.cppsrc/execution/executors/insert_executor.cppsrc/execution/executors/delete_executor.cppsrc/execution/executors/update_executor.cppsrc/execution/executors/nested_loop_join_executor.cppsrc/execution/executors/hash_join_executor.cppsrc/execution/executors/aggregation_executor.cppsrc/execution/executors/sort_executor.cppsrc/execution/executors/limit_executor.cppsrc/execution/executors/projection_executor.cppsrc/execution/executors/executor_factory.cpp,用于注册PlanType -> Executor的分发逻辑。
对于每个执行器,学生至少需要完成 Init() 和 Next();如果需要保存中间状态,还应同步修改对应头文件。
Lab 3 完成后,学生应能够:
- 正确遵循 Volcano 迭代器模型,
- 区分流式执行器和物化执行器,
- 使用正确的 schema 进行谓词和表达式求值,
- 保证 DML 行数、输出 tuple 以及 join / aggregation 语义与计划树一致。
- 严格遵守 Volcano 模型:
Init()负责初始化状态,Next()按需返回一行,除非算子明确需要先物化。 - 保持输出行数、schema 和列顺序与计划节点定义一致。
- 对 DML 语句,更新基础表和相关索引元数据要同步完成。
- 对需要计算表达式的算子,始终使用子执行器的输出 schema 进行求值。
- 连接、聚合、排序和索引扫描都应把中间状态限制在执行器内部,不要泄漏到文档层面。
# 编译项目
cd build && cmake --build . -j$(nproc)
# 运行 Lab 3 相关测试
ctest --test-dir build -R executor_test --output-on-failure
# 运行全部测试
ctest --test-dir build --output-on-failure-
Evaluate vs EvaluateJoin:单表操作(SeqScan 谓词、Sort 排序键、Projection 投影表达式)使用
Evaluate(tuple, schema)。双表操作(NLJ/HashJoin 谓词)使用EvaluateJoin(left_tuple, left_schema, right_tuple, right_schema)。 -
Insert/Delete/Update 只返回一次:这些执行器应该在第一次
Next()调用时执行所有操作并返回受影响的行数,后续调用返回false。使用has_inserted_类似的标志控制。 -
聚合空输入:没有 GROUP BY 时,即使输入为空也要返回一行默认聚合值(COUNT 返回 0,SUM/MIN/MAX 返回 NULL)。有 GROUP BY 时,空输入应该不返回任何行。
-
Hash Join 的 key 类型:使用
Value::ToString()作为哈希 key 是最简单的方式。 -
NLJ 右表重置:每处理完一个左表行对,需要调用
right_executor_->Init()重新初始化右表扫描。 -
索引更新:Insert 和 Delete 操作必须同时更新表和所有相关索引。
-
Join 结果构造:连接两行时,需要将左表和右表的所有列合并成一个新的 Tuple。
-
需要在执行器头文件中添加成员变量:部分执行器(NLJ、HashJoin、Aggregation 等)的头文件可能需要你添加额外的成员变量来存储中间状态。
-
Projection 使用子执行器的 Schema:投影执行器对子执行器输出的行求值,因此应使用
child_executor_->GetOutputSchema()而非plan_->GetOutputSchema()作为Evaluate()的 schema 参数。
| 组件 | 分值 |
|---|---|
| SeqScan Executor (顺序扫描) | 15 |
| Insert Executor (插入) | 10 |
| Delete Executor (删除) | 5 |
| Update Executor (更新) | 5 |
| Nested Loop Join Executor (嵌套循环连接) | 10 |
| Hash Join Executor (哈希连接) | 10 |
| Aggregation Executor (聚合) | 15 |
| Sort Executor (排序) | 10 |
| Limit Executor (限制行数) | 5 |
| Projection Executor (投影) | 10 |
| Index Scan Executor (索引扫描) | 5 |
| 总计 | 100 |