CMU15445 FALL2022 Project #3 - Query Execution
Task #1 - Access Method Executors
SeqScan
-
顺序扫描
-
/** 要执行的顺序扫描计划节点 */ const SeqScanPlanNode *plan_; /** 用于扫描表的迭代器,初始化为空指针 */ TableIterator table_iter_ = {nullptr, RID(), nullptr}; /** 指向存储实际表数据的表堆的指针 */ TableHeap *table_heap_; /** 指向包含模式和元数据的表信息的指针 */ TableInfo *table_info_;
-
Next
- 通过
table_iter_
取相应内容后++即可
- 通过
Insert
-
/** 要执行的插入计划节点 */ const InsertPlanNode *plan_; /** 子执行器,对于插入操作来说通常是 Values 执行器 */ std::unique_ptr<AbstractExecutor> child_executor_; /** 标识是否已经完成插入操作 */ bool has_inserted_ = false;
-
Init
child_executor_->Init()
-
Next
- 获取待插入的表信息及其索引列表
- 循环调用
child_executor_->Next
获取待插入的tuple
和rid
InsertTuple
插入tuple
和rid
指定内容- 根据索引的模式从数据元组中构造索引元组,并从索引中删除
- 最后返回插入操作的影响行数
Delete
-
/** 要执行的删除计划节点 */ const DeletePlanNode *plan_; /** 从中获取要删除的元组的RID的子执行器 */ std::unique_ptr<AbstractExecutor> child_executor_; /** 标志,指示是否已删除元组 */ bool has_deleted_ = false;
-
Init
child_executor_->Init()
-
Next
- 获取待删除的表信息及其索引列表
- 循环调用
child_executor_->Next
获取待删除的tuple
和rid
MarkDelete
删除rid
指定内容- 根据索引的模式从数据元组中构造索引元组,并从索引中删除
- 最后返回插入操作的影响行数
IndexScan
-
/** 要执行的index扫描节点 */ const IndexScanPlanNode *plan_; /** 待扫描表的 B+ 树索引 */ BPlusTreeIndexForOneIntegerColumn *tree_; /** 待扫描表的 B+ 树索引迭代器 */ BPlusTreeIndexIteratorForOneIntegerColumn table_iter_; /** 待扫描表的表堆 */ TableHeap *table_heap_;
-
tree_ = dynamic_cast<BPlusTreeIndexForOneIntegerColumn *>( exec_ctx_->GetCatalog()->GetIndex(plan_->GetIndexOid())->index_.get()); table_iter_ = tree_->GetBeginIterator(); IndexInfo *index_info = exec_ctx_->GetCatalog()->GetIndex(plan_->GetIndexOid()); table_heap_ = exec_ctx_->GetCatalog()->GetTable(index_info->table_name_)->table_.get();
-
Next
- 通过
table_iter_
取相应内容后++即可
- 通过
Task #2 - Aggregation & Join Executors
Aggregation
/** 聚合计划节点 */
const AggregationPlanNode *plan_;
/** 子执行器,用于生成计算聚合的元组 */
std::unique_ptr<AbstractExecutor> child_;
/** 简单的聚合哈希表 */
SimpleAggregationHashTable aht_;
/** 简单的聚合哈希表迭代器 */
SimpleAggregationHashTable::Iterator aht_iterator_;
Init
- 调用子执行器的
Init
方法 - 循环调用子执行器的
Next
方法获取元组,并将其插入到聚合哈希表中 - 如果聚合哈希表为空且输出模式只有一列,则插入初始组合
- 将哈希表迭代器设置为哈希表的起始位置
- 调用子执行器的
Next
- 如果哈希表迭代器到达末尾,返回
false
表示没有更多元组 - 否则,从迭代器中获取键(分组依据)和值(聚合结果),构造输出元组并递增迭代器
- 返回
true
表示成功获取下一个元组
- 如果哈希表迭代器到达末尾,返回
NestedLoopJoin
/** NestedLoopJoin计划节点 */
const NestedLoopJoinPlanNode *plan_;
/** 左表子执行器,对于循环连接操作来说通常是左表上的 Scan 执行器 */
std::unique_ptr<AbstractExecutor> left_executor_;
/** 右表子执行器,对于循环连接操作来说通常是右表上的 Scan 执行器 */
std::unique_ptr<AbstractExecutor> right_executor_;
/** 连接结果集 */
std::queue<Tuple> results_;
Init
- 调用左执行器和右执行器的
Init
方法 - 从左执行器和右执行器中获取所有元组并分别存储在
left_tuples
和right_tuples
向量中 - 通过嵌套循环依次连接
left_tuples
和right_tuples
中的所有元组- 使用连接条件评估每对元组是否匹配
- 如果匹配,将连接结果追加到结果集(
results_
)中 - 对于左连接,如果左元组没有匹配的右元组,右元组中的字段用空值填充,并将连接结果追加到结果集中
- 调用左执行器和右执行器的
Next
- 从结果集队列中获取下一个连接后的元组
- 如果结果集为空,返回
false
表示没有更多元组 - 否则,返回
true
表示成功获取下一个元组
NestedIndexJoin
/** nested index join计划节点 */
const NestedIndexJoinPlanNode *plan_;
/** 左表子执行器,对于索引连接操作来说通常是左表上的 Scan 执行器 */
std::unique_ptr<AbstractExecutor> left_executor_;
/** 连接结果集 */
std::queue<Tuple> results_;
Init
- 调用左执行器的
Init
方法 - 获取右表的索引信息和表信息,包括索引对象、表堆和表模式
- 从左执行器中获取所有元组
- 对于每个左元组,使用连接字段的值在右表的索引中查找匹配的 RID
- 对于每个匹配的右表 RID,获取对应的右元组,将左元组和右元组的值组合并添加到结果集中
- 如果左元组没有匹配的右元组且连接类型为左连接,将右元组的字段填充为空值,并将组合结果添加到结果集中
- 调用左执行器的
Next
-
- 从结果集队列中获取下一个连接后的元组
- 如果结果集为空,返回
false
表示没有更多元组 - 否则,返回
true
表示成功获取下一个元组
-
Task #3 - Sort + Limit Executors and Top-N Optimization
Sort
/** 排序计划节点 */
const SortPlanNode *plan_;
/** 子执行器,用于生成需要排序的元组 */
std::unique_ptr<AbstractExecutor> child_;
/** 存储子执行器生成的元组的向量 */
std::vector<Tuple> child_tuples_;
/** 子元组向量的常量迭代器 */
std::vector<Tuple>::const_iterator child_iter_;
Init
- 调用子执行器的
Init
方法 - 从子执行器中获取所有元组,并存储在
child_tuples_
向量中 - 使用
std::sort
函数对child_tuples_
进行排序,排序依据为计划节点中的排序键和排序类型- 对每对元组,依次比较每个排序键的值
- 根据排序类型(升序或降序)决定排序顺序
- 如果所有排序键的值都相等,则认为这对元组相等
- 初始化迭代器
child_iter_
指向排序后的元组向量的起始位置
- 调用子执行器的
Next
- 从排序后的元组向量中依次获取元组
- 如果迭代器达到向量末尾,返回
false
表示没有更多元组 - 否则,返回
true
表示成功获取下一个元组,并更新迭代器
Limit
/** 要执行的限制计划节点 */
const LimitPlanNode *plan_;
/** 从中获取元组的子执行器 */
std::unique_ptr<AbstractExecutor> child_;
/** 限制值 */
std::size_t limit_;
Init
- 调用子执行器的
Init
方法,初始化子执行器 - 从限制计划节点中获取限制值,并存储在成员变量
limit_
中
- 调用子执行器的
Next
- 检查当前限制值
limit_
是否大于零 - 如果限制值大于零,则尝试从子执行器获取下一个元组
- 调用子执行器的
Next
方法,如果成功获取到元组,则将限制值limit_
减一,并返回true
表示成功获取下一个元组
- 调用子执行器的
- 如果限制值不大于零或子执行器没有更多元组,则返回
false
表示没有更多元组
- 检查当前限制值
Top-N Optimization Rule
/** 要执行的 Top-N 计划节点 */
const TopNPlanNode *plan_;
/** 子执行器,用于获取元组 */
std::unique_ptr<AbstractExecutor> child_;
/** Top-N 查询中的 N 值 */
std::size_t n_;
/** 存储排序后的子元组 */
std::vector<Tuple> child_tuples_;
/** 子元组向量的迭代器 */
std::vector<Tuple>::const_iterator child_iter_;
Init
- 调用子执行器的
Init
方法,初始化子执行器。 - 获取 Top-N 计划节点中的 N 值,并存储在成员变量
n_
中。 - 定义用于比较元组的函数
comparator
,根据计划节点中的排序键和排序类型进行比较。- 遍历计划节点中的排序键,比较元组的值,根据排序类型(升序或降序)决定排序顺序。
- 如果所有排序键的值都相等,则认为元组相等。
- 使用定义的比较函数构建优先队列
pq
,注意使用greater
,因为默认priority_queue
是最大堆。 - 从子执行器获取所有元组,将其插入到优先队列中:
- 如果优先队列的大小小于 N,则直接插入元组。
- 如果优先队列的大小已达到 N,且新元组比堆顶元组更优,则替换堆顶元组。
- 将优先队列中的元素转移到
child_tuples_
向量中,并反转该向量以保证元组按正确顺序排列。
- 调用子执行器的
Next
- 从排序后的元组向量中依次获取元组。
- 如果 N 值小于等于零或迭代器已到达向量末尾,返回
false
表示没有更多元组。 - 否则,返回
true
表示成功获取下一个元组,并更新迭代器和 N 值。
sort_limit_as_topn
- 递归优化子节点:首先,函数会遍历
plan
的子节点,对每个子节点调用OptimizeSortLimitAsTopN
函数进行递归优化。这是一个自底向上的递归优化过程,会一直递归到叶子节点。 - 克隆优化后的计划:递归优化完成后,函数会使用
CloneWithChildren
方法克隆当前节点,并将优化后的子节点替换原有的子节点。这样就完成了对当前节点的优化。 - 检查Limit节点:接下来,函数会检查当前优化后的计划是否是一个Limit节点。如果是,就获取Limit的值,并确保Limit节点有且仅有一个子节点。
- 检查Sort节点:如果Limit节点的子节点是一个Sort节点,那么就进一步获取Sort节点的排序方式(OrderBy)。
- 替换为TopN节点:最后,如果Limit节点的子节点是Sort节点,函数会将Limit节点和Sort节点替换为一个TopN节点,并保留与Sort节点相同的子节点、排序方式和Limit值。
- 返回优化后的计划:最终,函数返回优化后的计划。
最后放个线上测试结果