实现执行引擎和执行器

本文介绍了数据库执行引擎的实现,主要采用迭代模型,通过执行引擎类处理执行计划,返回查询结果。执行器根据计划类型创建,如顺序扫描、插入、嵌套循环等,实现了基本的数据库操作。在执行过程中,不断调用Next方法获取结果,并处理异常。执行器接口定义了初始化、执行等方法,不同的执行器实现特定的操作,如SeqScanExecutor、InsertExecutor等。
摘要由CSDN通过智能技术生成

执行引擎

我们使用迭代模型来实现执行引擎 ExecutionEngine, 该类最重要的方法就是 Execute, 包含执行的计划,需要返回的结果,事务,和执行上下文信息。该方法首先通过工厂方法来生产具体的执行器,然后不断调用Next方法获取执行的结果。

graph LR
    A[执行计划] ---> B{执行引擎} ---> C[查询结果]
    subgraph 执行
    D ---> |返回数据| B
    B ---> |查询数据| D[(数据库)]
    end
bool Execute(const AbstractPlanNode *plan, std::vector<Tuple>*result_set, 
             Transaction *txn, ExecutorContext *exec_ctx) {
  // construct executor
  auto executor = ExecutorFactory::CreateExecutor(exec_ctx, plan);
  // prepare
  executor->Init();
  // execute
  try {
    Tuple tuple;
    RID rid;
    while (executor->Next(&tuple, &rid)) {
      if (result_set != nullptr) {
        if (plan->GetType() == PlanType::Insert || 
            plan->GetType() == PlanType::Update ||
            plan->GetType() == PlanType::Delete) {
          continue;
        }
        result_set->push_back(tuple);
      }
    }
  } catch (Exception &e) {
    // TODO(student): handle exceptions
  }
  return true;
}

所有的执行器都需要实现抽象执行接口 AbstractExecutor, 具体的执行器有对应的基本执行操作实现, 如顺序扫描SeqScanExecutor、插入InsertExecutor、嵌套循环NestedLoopJoinExecutor等。

«abstract» AbstractExecutor #ExecutorContext* exec_ctx_ +Init() +Next() +GetOutputSchema() +GetExecutorContext() SeqScanExecutor -SeqScanPlanNode *plan_ InsertExecutor -InsertPlanNode *plan_ LimitExecutor -LimitPlanNode *plan_ -AbstractExecutor *child_executor_ NestedLoopJoinExecutor -NestedIndexJoinPlanNode *plan_ -AbstractExecutor *left_executor_ -AbstractExecutor *right_executor_ implements implements implements implements
  • Init(): 设置有关操作符调用的内部状态(例如,检索要扫描的对应表)。
  • Next(): 提供迭代器接口,该接口在每次调用时返回一个元组和相应的 rid (如果没有更多的元组,则返回 null)

执行代价估计

  • 磁盘上一个块的平均传输时间: t T t_T tT
  • 磁盘上一个块的平均访问时间(磁盘寻道时间 + 旋转延迟时间): t S t_S tS
  • 如果一个操作传输了 b b b 个块和进行了 S S S 次随机 I/O 访问 则需要耗费: $ b * t_T + S * t_S $
  • 关系 r 包含的块个数: b r b_r br
  • 关系 s 包含的块个数: b s b_s bs
  • 关系 r 包含的元组个数: n r n_r nr
  • 关系 s 包含的元组个数: n s n_s ns
  • 缓冲区的块个数: M M M
  • B+树索引的高度: h i h_i hi

顺序扫描(Sequential Scan)

顺序扫描也称为表扫描或线性扫描。顺序扫描执行器遍历一个表并一次返回它的元组。顺序扫描由SeqScanPlanNode 指定。plan节点指定要遍历哪个表。计划节点也可以包含谓词;如果一个元组不满足谓词,则跳过它。

顺序扫描的时间代价: t s + b r ∗ t T t_s + b_r * t_T ts+brtT
关键码上等值条件的顺序扫描: t s + ( b r / 2 ) ∗ t T t_s + (b_r/2) * t_T ts+(br/2)tT

提示:使用TableIterator对象时要小心。请务必理解前置自增操作符和后置自增操作符之间的区别。通过在 ++iteriter++ 之间切换,你可能会发现自己得到奇怪的输出。

提示:你将希望在顺序扫描计划节点中使用谓词。特别是,看看AbstractExpression::Evaluate。注意,这会返回一个 Value,你可以使用 GetAs<bool>来获取布尔值。

提示:顺序扫描的输出应该是匹配元组的值副本及其原始记录ID

索引扫描(Index Scan)

索引扫描遍历索引以获得元组 rid 。这些 rid 用于在与索引对应的表中查找元组。最后,这些元组一次返回一个。索引扫描由 IndexScanPlanNode 指定。计划节点指定要遍历的索引。计划节点也可以包含谓词;如果一个元组不满足谓词,则跳过它。

聚簇B+树索引关键码上的等值查询时间代价: ( h i + 1 ) ∗ ( t T + t S ) (h_i + 1) * (t_T + t_S) (hi+1)(tT+tS) h i h_i hi次索引遍历, 额外加1次获取元组。

聚簇B+树索引非关键码上的等值查询时间代价: h i ∗ ( t T + t S ) + t s + b ∗ t T h_i * (t_T + t_S) + t_s + b * t_T hi(tT+tS)+ts+btT h i h_i hi次索引遍历, 额外加1次记录的访问, 这里 b b b 代表满足条件的记录的块数。

提示: 一旦从索引中检索了 RID,请确保从表中检索元组。记住要使用目录来查找表。

提示: 利用 B+ 树的迭代器来同时支持点查询和范围扫描。

提示:对于这个项目,可以安全地假设索引的键值类型是 <GenericKey<8>, RID, GenericComparator<8>> ;不要担心模板,不过也欢迎更通用的解决方案

插入(Insert)

插入执行器将元组插入到表中并更新索引。有两种类型的插入,第一种插入是将插入的值直接嵌入到计划节点本身中;另一种是获取从子执行程序获取要插入的值。例如,可以使用其子节点为 SeqScanPlanNodeIndexScanPlanNodeInsertPlanNode 将一个表复制到另一个表中。

提示: 你将希望在执行器构造期间查找表和索引信息,并在执行期间必要时插入索引。

更新(Update)

修改指定表中的现有元组并更新其索引。子执行程序将提供更新执行程序将修改的元组(及其RID)。例如,一个 UpdatePlanNode 可以有一个 SeqScanPlanNodeIndexScanPlanNode 来提供目标元组。

提示:我们提供了一个 GenerateUpdatedTuple,它为你构造了一个更新的元组。

删除(Delete)

从表中删除元组,并从索引中删除它们的条目。与更新类似,目标元组集来自子执行器,如 SeqScanExecutorIndexScanExecutor

提示:你只需要从子执行器中获取 RID,并调用 MarkDelete (src/include/storage/table/table_heap.h) 使其不可见。删除将在事务提交时应用。

嵌套循环连接

R ( X , Y ) ⋈ θ S ( Y , Z ) R(X, Y) \Join_{\theta} S(Y, Z) R(X,Y)θS(Y,Z)

基于元组的嵌套循环连接算法

嵌套循环的系列算法中最简单的形式是其中的循环是对所涉及关系的各个元组来进行的。

for each tuple tr in r do begin
    for each tuple ts in s do begin
        test pair (tr, ts) to see if they satisfy the join condition θ
        if they do, add tr ⋅ ts to the result;
    end
end

如果我们不在内存中缓存下 关系 r r r s s s 的块,则这种算法需要的磁盘 I/O 可多达 t r ∗ t s t_r * t_s trts

嵌套循环连接的一个优点是它非常适用于迭代器结构,下面给出迭代函数的代码:

Init() {
   r_executor->Init();
   s_executor->Init();
   r_tuple = r_executor->Next();
}
Next() {
    while (r_tuple is not null) {
        s_tuple =  s_executor->Next();
        if (s_tuple is null) {
            r_tuple = r_executor->Next();
            if (r_tuple is null) {
                return null;
            }
            s_executor->Init();
            s_tuple =  s_executor->Next();
        }
        if (r_tuple and s_tuple can join on predicate condition) {
            resule_tuple = Join(r_tuple, s_tuple);
            return   resule_tuple;
        }
    }
    return null;
}
Close() {
    r_executor->close();
    s_executor->close();
}

提示: 嵌套循环连接计划节点中使用谓词。具体来说,请查看 AbstractExpression::EvaluateJoin,它处理左元组和右元组及其各自的模式。这会返回一个 Value,你可以使用 GetAs<bool>来获取布尔值。

基于块的嵌套循环连接算法

基于块的的嵌套循环对两个关系的访问均按块访问。我们先考虑使用内存来缓存块的最坏和最好情况。最坏情况下,缓冲中只能发下每个关系的一个块,则需要传输的块个数为: b r + n r ∗ b s b_r + n_r * b_s br+nrbs , 内部关系 s s s 是顺序读取需要进行 n r n_r nr 次访问,对外部关系 b r b_r br 是随机读取需要进行 b r b_r br 次访问,所以总的访问块个数为: n r + b r n_r + b_r nr+br。最好情况下,缓冲中有足够的内存来存放所有关系,这样就只需要访问 2 2 2次磁盘和进行 b r + b s b_r + b_s br+bs 个块的传输。

for each block Br of r do begin
    for each block Bs of s do begin
        for each tuple tr in Br do begin
            for each tuple ts in Bs do begin
                test pair (tr, ts) to see if they satisfy the join condition
                if they do, add tr ⋅ ts to the result;
            end
        end
    end
end

对于一般情况,我们假设 b r < b s b_r < b_s br<bs, 我们再假定 M < b r M < b_r M<br,这样的话,任何一个关系都不能完整地装入内存。对上面给出地块嵌套算法进行改进我们可以不使用磁盘块作为外部关系的块单元,而是使用尽可能多的内存来存储外层关系 r r r 中的元组,同时为内部关系和输出的缓冲区留出足够的空间。换句话说,如果内存有 M M M个块,我们一次读取 M − 2 M-2 M2个外部关系的块,当我们读取每个内部关系的块时,我们把它与所有的 M − 2 M-2 M2个外部关系的块连接起来。

for read M-2 block of r do begin
    for each block Bs of s do begin
        for each tuple ts in Bs do begin
            tr from M-2 block
            test pair (tr, ts) to see if they satisfy the join condition, 
            if they do, add tr ⋅ ts to the result;
        end
    end
end

这样地话总的传输块为: b r + ⌈ b r / ( M − 2 ) ⌉ ∗ b s b_r + \lceil b_r / (M-2) \rceil * b_s br+br/(M2)bs, 磁盘访问次数为: 2 ⌈ b r / ( M − 2 ) ⌉ 2\lceil b_r / (M-2) \rceil 2br/(M2)

索引循环嵌套连接

假设 S S S 在属性集 Y Y Y 上存在索引

b r ∗ ( t T + t S ) + n r ∗ c b_r * (t_T + t_S) + n_r * c br(tT+tS)+nrc

如果一个索引在内部循环的join属性上可用,我们可以用更有效的索引查找代替表扫描

其中 c c c 是使用连接条件对内层关系 s s s 进行一次选择的代价。

  1. fetch the tuple from the outer table
  2. construct the index probe key by looking up the column value and index key schema
  3. look up the RID in the index to retrieve the corresponding tuple for the inner table.

提示:从外部表中获取元组,通过查找列值和索引键模式来构造索引探测键,然后在索引中查找RID来检索内部表的相应元组。

提示:可以假设内部元组总是有效的(即没有谓词)

聚合(Aggregation)

该执行器将来自单个子执行器的多个元组结果组合为单个元组。在这个项目中,我们要求您实现 COUNTSUMMINMAX 。我们提供了一个SimpleAggregationHashTable,它具有所有必要的聚合功能。

您还需要实现有HAVING的GROUP BY子句。我们提供了一个SimpleAggregationHashTable Iterator,它可以遍历散列表。

提示: 您将希望聚合结果并使用 HAVING 作为约束。特别是,请查看AbstractExpression::EvaluateAggregate,它处理不同类型表达式的聚合求值。注意,这会返回一个Value,你可以使用 GetAs<bool> 将其转换成 bool 值

如果结果的所有元组都放入内存,那么基于排序和基于散列的实现不需要将任何元组写入磁盘。当元组被读入时,可以将它们插入有序树结构或散列索引中。当我们使用动态聚合技术时,每个组只需要存储一个元组。因此,有序树结构或散列索引适合内存,并且聚合可以通过 b r b_r br 块传输(和1次访问)来处理,而不是使用 3 b r 3b_r 3br 传输(最坏的情况是 2 b r 2b_r 2br 次访问),否则就需要 3 b r 3b_r 3br 传输。

b r ∗ t T + t S b_r * t_T + t_S brtT+tS

有限数目(Limit)

最后,这个执行器限制了单个子执行器的输出数量。offset值表示执行器在发出元组之前需要跳过的元组数量。

一些坑:

  1. you should check the executor type in execution_engine.h to make sure that you don’t insert tuple into result_set for insert/update/delete.
  2. You will need to evaluate all the columns of the output schema on the input tuple/schema to produce the output values, then construct the new tuple with those values + the output schema
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值