暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

cascade optimizer如何确定table spool

手机用户2895 2023-10-27
302

TableSpool带来的问题

目前IMCI的优化器在处理TableSpool时,由于IMCI优化器需要结合延迟物化执行器和传统的非延迟物化执行器,因此对于每个TableSpool的PrimarySpool(producer)与RefSpool(Consumer),优化器都会生成两个TableSpool: RIDTableSpool与DataTableSpool,用以下这个Plan为例,我们可以看到在引入Spool之后可能存在的问题:

flowchart TD
    subgraph Primary
        direction RL
        a1(PrimaryRidSpool)
        a2(PrimaryDataSpool)
    end
    subgraph Ref
        direction RL
        a3(RefRidSpool)
        a4(RefDataSpool)
    end
    A(Project) --> B(HashMatch)
    B --> Ref
    
    B --> F(Groupby)
    F --> Primary
    Primary --> E(...)

volcano optimizer需要选出代价最小的table spool来执行这条Query,但是并不是任意选择TableSpool,查询计划都是合法的,因为PrimarySpool与RefSpool含有依赖关系,他们的输出结果必须一样,也就是说,RIDSpool与DataSpool必须成对选择,而在cascade optimizer的框架中,优化每个plan的分支时,对另一个分支的信息都所知甚少,因此我们需要改造优化器的流程,来保证优化器总是选择到合法的Plan

思路

目前解决这个问题的大致思路是:牺牲全局最优,换得plan的合法性,这个做法有点类似PostgreSQL的做法,即:先从RIDTableSpool和DataSpool中选择一个固定下来,随后Primary和Ref都选取同一个TableSpool,以此避免选到非法的plan,为此目前对cascade optimizer的流程做了如下改动:

应用ImplementRule的步骤

在cascade优化器中,这个步骤对应的是OptimizerGroupExprTask,这个Task中,我们保证了Spool的应用顺序,即:PrimarySpool所在的Group总是更先RefSpool所在的Group生成PhysicalPlan,这一步因为我们需要先确定PrimarySpool,如果这一步没有这个保证,那么在JoinReorder等规则等应用下,可能导致RefSpool先被优化,后续需要确定PrimarySpool的时候发现PrimarySpool还未生成PhysicalTableSpool的问题,代码示例如下

void OptimizeGroupExprTask::Execute() { RelationBase *rel = group_expr_->GetRelation(); TableSpool *spool = nullptr; if (IsLogicalTableSpool(rel)) { spool = static_cast<TableSpool *>(rel); } if (spool && !spool->IsPrimary() && !has_explore_primary_spool_) { /* If it is ref table spool, optimize primary spool first */ has_explore_primary_spool_ = true; PushOptTask<OptimizeGroupExprTask>(ctx_, *this); Memo *memo = ctx_->GetMemo(); uint16_t primary_rel_id = spool->GetPrimaryRelID(); GroupExpr *primary_ge = memo->GetGroupExprByRelId(primary_rel_id); ASSERT(primary_ge, "GroupExpr for primary table spool not generated"); PushOptTask<OptimizeGroupExprTask>(ctx_, primary_ge, /*explore_only*/ false); /** * Return now to optimize primary spool first. * We will come back here because a duplicated task has been added. */ return; } DoExecute(); }

计算Physcial Plan的代价时的步骤

在cascade优化器,这个步骤对应的OptimizeInputsTask,该Task中,当我们优化RefSpool时,会先在PrimarySpool对应的group中确定一个可用的Spool,这里根据一个简单的规则来确定RIDSpool和DataSpool:

  • 如果能使用rid spool,就选择RIDSpool
  • 否则,选择DataSpool,例如:Spool用来缓存Groupby的结果(只能使用DataFmt)
    这个规则很接近最优策略,原因在于:
  • RID Spool的优势在于缓存/落盘的数据更少,劣势则需要反复的访问磁盘去取对应的数据,这里实际上是落盘数据量与访问磁盘次数多tradeoff,优劣对比的关键在于用过RID访问此盘数据的代价,在LRU cache足够大的情况下,通过RID访问磁盘的代价变得很低,因此RID是更优的选择。
  • 目前优化器的代价模型实际上倾向于使用延迟物化技术(假设lru cache很大,因此选择传递RID),在这个情况下,选用rid spool是符合优化器的倾向的
  • 对于LRU cache不够大的情况,目前考虑到线上小内存实例更多,因此关闭了延迟物化功能,在该情况下,无法出现TableSpool缓存乱序RID的情形,即:缓存顺序RID,或者缓存Data,也符合我们关闭延迟物化的诉求。

随后判断当前优化的RefSpool是否与我们留下的Spool一样,如果当前ref spool能匹配primary group中对应的spool,则计算代价,并返回一个合法的cost,否则不计算cost表示该spool是不合法的。

这个流程带来的问题

删除Spool导致上方算子的requirement无法被满足导致plan生成失败

假设本文开始的例子中PrimaryGroup中含有三个GroupExpr:

GroupExpr Property COST
DATA_SPOOL OUTPUT_DATA(c1, c2, c3) 100
RID_SPOOL OUTPUT_RID 10
RID_SPOOL + COMPUTE_SCALAR OUTPUT_DATA(c1, c2) 200

由于上文提到的,PrimarySpool总是先被优化以及计算cost,因此经过cost计算之后,可以发现DATA_SPOOL输出的列能够满足RID_SPOOL + COMPUTE_SCALAR,同时cost更低,因此上文中的cost表会被更新

GroupExpr Property COST
DATA_SPOOL OUTPUT_DATA(c1, c2, c3) 100
RID_SPOOL OUTPUT_RID 10
DATA_SPOOL OUTPUT_DATA(c1, c2) 100

之后如果我们按照OptimizeInputsTask中的步骤所说,删除了DATA_SPOOL,对应的表中DATA_SPOOL相关的cost也需要被删除,因此变成了这样:

GroupExpr Property COST
RID_SPOOL OUTPUT_RID 10

本文开头时的图例中,PrimarySpool的parent expr是Groupby,由于Groupby要求输入必须是DATA,但是Group中相关的条目已经被删除,当FindBestPlan通过这个cost table寻找时,由于DATA相关的expr都被删掉了,因此查询会因此找不到可用的plan而报错

HOW TO FIX

上文流程分析中可以看到,主要问题在于,在这个 生成cost table -> 更新cost table -> 删除部分条目的过程中,删除的条目不应该直接从cost table中消除,而是应该用更新前,但是合法的expr与cost补上,也就是说,删除之后,上文的cost table应当是

GroupExpr Property COST
RID_SPOOL + COMPUTE_SCALAR OUTPUT_DATA(c1, c2, c3) 200
RID_SPOOL OUTPUT_RID 10
RID_SPOOL + COMPUTE_SCALAR OUTPUT_DATA(c1, c2) 200

因此,针对OptimizeInputsTask中对Spool的处理,在删除Spool时增加了以下步骤

  • 在删除一个SPOOL时,仍然删除cost table中对应的条目,但是记录下删除条目对应的output property
  • 将所有cost table中对应条目的expr包含enforcer全部删除,类似地,也记录下删除条目对应的output property
    • 这是因为DATA_SPOOL被删除之后,enforcer相关的plan可能需要重新计算cost,例如DATA_SPOOL + EXCHANGE可能需要变成RID_SPOOL + CS + EXCHANGE
  • 针对所有property,对所有enforcer再下发一个OptimizeInputsTask,目的是重新计算被删除的Property对应的plan的cost
    • 如果是DATA_SPOOL,是不是也要对DATA_SPOOL也要重新下发任务用来更新代价呢?

代码示意如下:

// .... bool pick_rid_spool = ShouldPickRidSpool(); std::vector<PropSet *> erased_psets; for (auto it = begin(lowest_cost_exprs); it != end(lowest_cost_exprs);) { if (it->second.expr->IsEnforcer()) { it = lowest_cost_exprs.erase(it); erased_psets.push_back(it->first); } else { // set cost erased spool invalid if (pick_rid_spool && it->second.expr->GetOutFmt() == OutFmt::DATA) { it->second.cost = kCostMaximum; erased_psets.push_back(it->first); } else if (!pick_rid_spool && it->second.expr->GetOutFmt() == OutFmt::RID) { it->second.cost = kCostMaximum; erased_psets.push_back(it->first); } it++; } } PushOptTask<OptimizeInputsTask>(ctx_, *this); // reoptimize all enforcer for (auto prop_set : erased_psets) { for (auto expr : rimary_group->GetPhysicalExprs()) { if (!expr->IsEnforcer()) continue; expr->ClearGeForReOptimize(); auto new_ctx = std::make_shared<VolcanoCtx>(*ctx_); new_ctx->SetRequiredProperties(prop_set); PushOptTask<OptimizeInputsTask>(new_ctx, enforcer); } } return;
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论