Calcite CBO 分析1

不想看文章直接访问https://github.com/yuqi1129/schema/tree/master/mysql-protocol
(Java版本的Mysql)、https://github.com/yuqi1129/calcite-test,这里有关于Calcite RBO,CBO使用具体用例

CBO 概念

提到CBO(Cost based optimization) , 如果学习过Spark SQL, HSQ的读者应该对此不会陌生,CBO的主要思想是利用等价表式替换的方式加上代价计算框架和模型迭代来优化一个SQL执行计划, 也就是说
CBO = RBO + Cost Model + Model Iteration
即CBO 是以RBO为基础, 通过代价模型,在一定的时间空间范围内重复迭代代价模型来获得最终的执行计划

以下简单的列子

JoinAssocaiteRule.png

图中为SQL中常碰到的三张表Join问题,Calicte中内置的JoinAssocaiteRule 会将的LDT(Left deep tree) 转换成右边的执行计划, 从左图到右图这个转化在RBO就已经有,不同的是CBO会在转化过程中记录每次转化中总的执行代价,最终会选择代价最小执行计划为最终的的执行结果。本文的内容主要是细说Calcite代价计算模型与方法,关于CBO具体的执行优化步骤,会在后续的文章中慢慢说明.

Calcite 代价模型

Calcite 的代价计算模型为Volcano/Cascades模型, 关于Volcano/Cascades模型,大家可以参考一下The Volcano Optimizer Generator: Extensibility and Efficient SearchThe Cascades Framework for Query Optimization
, 其主要思想是采用动态的规划中的贪心算法计算执行计划的代价。Cascades 相对于Volcano
的区别在于Volcano先枚举所有可能的执行计划后面再统一计算代价并最终选择最低的代价作为最后结果,而Cascades会在枚举的过程进行局部最优优化即修枝剪枝,使得需要迭代的执行计划空间不会像Vocano那么大的也可以获得相对较好的执行计划。Cascades 模型通过在一定的时间活代价范围内迭代RBO的Rule, 每次迭代时比较当前的计划与迭代前的计划的代价,选择代价较低的计划并保存,最终生成一个次优的执行计划,之所以称之为次优,Cascades 模型只会遍历部分计划空间寻找局部最优的执行计划,如果要遍历所有的执行,一来是所有执行计划空间非常庞大,实现难度较大,二来遍历所有执行计划空间得不偿失,可能一句SQL花费在执行计划优化的时候比整个SQL的执行时间还长,考虑到以上因素,Cascades模型通过贪心算法,每次迭代找到局部最优的执行计划,反复执行该步骤一定次数后,最终得到次优的执行计划。

Calcite 代价

Calcite中代价的接口为 RelOptCost, Volcano(Calcite中最新实现的其实是Cascades, 但名称还是叫Volcano)模型中cost代价为 VolcanoCost, 其中Cost主要有以下三个部分组成:

  • RowCount, RelNode代表的数据行数
  • CPU 所消耗的CPU
  • IO 所需要的IO

目前Calcite中Cost模型主要是以上面三个方面的代价,那么问题来了,如何计划一个RelNode的代价呢?

Calcite 代价计算主要交给RelMetadataQuery 计算, 核心代码见下:

 public final JaninoRelMetadataProvider metadataProvider;

 //获取RelNode的数据的行数
 public Double getRowCount(RelNode rel) {
    for (;;) {
      try {
        Double result = rowCountHandler.getRowCount(rel, this);
        return validateResult(result);
      } catch (JaninoRelMetadataProvider.NoHandler e) {
        rowCountHandler = revise(e.relClass, BuiltInMetadata.RowCount.DEF);
      }
    }
  }

// 获取RelNode的累加代价,累加代价即该RelNode与其所有的input代价和
public RelOptCost getCumulativeCost(RelNode rel) {
    for (;;) {
      try {
        return cumulativeCostHandler.getCumulativeCost(rel, this);
      } catch (JaninoRelMetadataProvider.NoHandler e) {
        cumulativeCostHandler =
            revise(e.relClass, BuiltInMetadata.CumulativeCost.DEF);
      }
    }
  }

现在细说一下RelMetadataQuery中相关field和method的作用和工作原理

RelMetadataProvider

RelMetadataProvider 是为Relnode提供元数据的接口,默认实现是JaninoRelMetadataProvider, 该方法是通过CodeGen的方式动态获取元数据据Handler

元数据

什么是元数据,准确来讲是指RelNode所代表的数据的元数据,Collation, Distribution, RowCount等等, 这些元数据定义RelNode所代表数据的特征,根据这些特征进行后续的CBO和RBO优化(RBO也用到了元数据,比如relNode的Collation 是排序好的,那么可以消除显示的Sort)

元数据的原始接口为Metadata, 各种不同的元数据通过扩展该接口实现不同的定义。

元数据只是定义相应的接口,在哪里实现呢?每一个元数据接口中定义一个 获取该元数据值的方法,同时定义Hander接口,Handler接口中也定义了同名的方法,见:

  public interface CumulativeCost extends Metadata {
    MetadataDef<CumulativeCost> DEF = MetadataDef.of(CumulativeCost.class,
        CumulativeCost.Handler.class, BuiltInMethod.CUMULATIVE_COST.method);

    RelOptCost getCumulativeCost();

    /** Handler API. */
    interface Handler extends MetadataHandler<CumulativeCost> {
      RelOptCost getCumulativeCost(RelNode r, RelMetadataQuery mq);
    }
  }

获取元数据值最终会调用Handler中的同名函数的。

Handler

看过Calcite源码的同学或许很其怪, Hanlder接口有默认实现,我刚开始接触这一块也没有弄明白怎么回事,实际上每个Handler是通过动态代码生成技术来实现相应的接口。

现在就以下代码为例:


// Volcano中获取一个RelNode的累加Cost方法,
public RelOptCost getCost(RelNode rel, RelMetadataQuery mq) {
    ...
    //重点,通过RelMetadataQuery获取Cost
    RelOptCost cost = mq.getNonCumulativeCost(rel);
    for (RelNode input : rel.getInputs()) {
      cost = cost.plus(getCost(input, mq));
    }
   ...
    return cost;
  }
  
   public RelOptCost getNonCumulativeCost(RelNode rel) {
    for (;;) {
      try {
        //调定对应的handler来获取Cost
        return nonCumulativeCostHandler.getNonCumulativeCost(rel, this);
      } catch (JaninoRelMetadataProvider.NoHandler e) {
        //注意这个Reivse方法, 具体内容在这里
        nonCumulativeCostHandler =
            revise(e.relClass, BuiltInMetadata.NonCumulativeCost.DEF);
      }
    }
  }

//nonCumulativeCostHandler 初始逻辑
public RelMetadataQuery() {
    ...
    // init Handler 
    this.nonCumulativeCostHandler = initialHandler(BuiltInMetadata.NonCumulativeCost.Handler.class);
    ...
 }
// init handler 主要逻辑,能过Rroxy, 生成实例,该方法没做什么内容,直接抛出异常
 protected static <H> H initialHandler(Class<H> handlerClass) {
    return handlerClass.cast(
        Proxy.newProxyInstance(RelMetadataQuery.class.getClassLoader(),
            new Class[] {handlerClass}, (proxy, method, args) -> {
              final RelNode r = (RelNode) args[0];
              throw new JaninoRelMetadataProvider.NoHandler(r.getClass());
            }));
  }

以上逻辑总结来说:
在初始化时,能过代理实例出一个抛异常的handler, 而在获取对实的元数据时,如果抛了异常,就会调用revise方法来订正,直到得到结果。现在来看一下revise具体做了

 protected <M extends Metadata, H extends MetadataHandler<M>> H
      revise(Class<? extends RelNode> class_, MetadataDef<M> def) {
    return metadataProvider.revise(class_, def);
  }

最终调用HANDLERS.get()

 //Google cache中获取
 private static final LoadingCache<Key, MetadataHandler> HANDLERS =
      maxSize(CacheBuilder.newBuilder(),          CalciteSystemProperty.METADATA_HANDLER_CACHE_MAXIMUM_SIZE.value())
          .build(
              CacheLoader.from(key ->
                  load3(key.def, key.provider.handlers(key.def),
                      key.relClasses)));

load3方法通过代码拼接、内存编译、实例化等过程,生所的类方法如下:

public final class GeneratedMetadataHandler_NonCumulativeCost implements org.apache.calcite.rel.metadata.BuiltInMetadata.NonCumulativeCost.Handler {
  private final java.util.List relClasses;
  public final org.apache.calcite.rel.metadata.RelMdPercentageOriginalRows provider0;
  public GeneratedMetadataHandler_NonCumulativeCost(java.util.List relClasses,
      org.apache.calcite.rel.metadata.RelMdPercentageOriginalRows provider0) {
    this.relClasses = relClasses;
    this.provider0 = provider0;
  }
  public org.apache.calcite.rel.metadata.MetadataDef getDef() {
    return org.apache.calcite.rel.metadata.BuiltInMetadata$NonCumulativeCost.DEF;
  }
  public org.apache.calcite.plan.RelOptCost getNonCumulativeCost(
      org.apache.calcite.rel.RelNode r,
      org.apache.calcite.rel.metadata.RelMetadataQuery mq) {
    final java.util.List key = org.apache.calcite.runtime.FlatLists.of(org.apache.calcite.rel.metadata.BuiltInMetadata$NonCumulativeCost.DEF, r);
    final Object v = mq.map.get(key);
    if (v != null) {
      if (v == org.apache.calcite.rel.metadata.NullSentinel.ACTIVE) {
        throw org.apache.calcite.rel.metadata.CyclicMetadataException.INSTANCE;
      }
      if (v == org.apache.calcite.rel.metadata.NullSentinel.INSTANCE) {
        return null;
      }
      return (org.apache.calcite.plan.RelOptCost) v;
    }
    mq.map.put(key,org.apache.calcite.rel.metadata.NullSentinel.ACTIVE);
    try {
      final org.apache.calcite.plan.RelOptCost x = getNonCumulativeCost_(r, mq);
      mq.map.put(key, org.apache.calcite.rel.metadata.NullSentinel.mask(x));
      return x;
    } catch (java.lang.Exception e) {
      mq.map.remove(key);
      throw e;
    }
  }

 // 最终调用函数
  private org.apache.calcite.plan.RelOptCost getNonCumulativeCost_(
      org.apache.calcite.rel.RelNode r,
      org.apache.calcite.rel.metadata.RelMetadataQuery mq) {
    switch (relClasses.indexOf(r.getClass())) {
    default:
      /*走这里,provider0 指的是RelMdPercentageOriginalRows, getNonCumulativeCost 函数返回结果如下:
     
      public RelOptCost getNonCumulativeCost(RelNode rel, RelMetadataQuery         mq) {
    return rel.computeSelfCost(rel.getCluster().getPlanner(), mq);
  } 

     rel.computeSelfCost() 函数为RelNode中方法,也就是说用户需要自行定义
    computeSelfCost()方法的实现逻辑,如可实现可以参考默认实现
     */
      return provider0.getNonCumulativeCost((org.apache.calcite.rel.RelNode) r, mq);
    case 3:
      return getNonCumulativeCost(((org.apache.calcite.plan.hep.HepRelVertex) r).getCurrentRel(), mq);
    case -1:
      throw new org.apache.calcite.rel.metadata.JaninoRelMetadataProvider$NoHandler(r.getClass());
    }
  }

}

总结来说:
RelMetadataQuery中实现了RelNode的各种元数据计算逻辑,计算逻辑是通过MetadataHandler来实现的,而MetadataHandler是通过CodeGen动态生成代码来实现的。
关于RelNode的computeSelfCost的方法,可以参考TableScancomputeSelfCost 方法,在构造CBO优化器时,需要重写当前Convention
各个RelNode的computeSelfCost方法。TableScan中可以通过查全表的形式获取RelOptCost的rowcount属性,后续的代价计算可以通过TableScan获取,比如说默认Filter的rowcount, 只是默认实现比较原始,统计结果无法直接用于实际场景,故用户需要自行实现computeSelfCost方法

 @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
    //Filter 代价直接设置成其输入的0.1倍
    return super.computeSelfCost(planner, mq).multiplyBy(0.1);
  }

上述是以CumulativeCost这个元数据为例,在RelMetadataQuery中还有许多其它的元数据实现,如DistributionCollation等,感兴趣的同学可以自行看一下。

总结

本文简要说明了一个Calcite CBO 代价计算的基本逻辑及流程,关于CBO中CBO的执行流程,请参考下后续文章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容

  • 不想看文章直接访问mysql-protocal(Java版本的Mysql)、calcite-test,这里有关于C...
    ni_d58f阅读 66,935评论 13 58
  • 本文主要是对数据库查询优化器的一个综述,包括: 查询优化器定义、分类 查询优化器执行过程 CBO框架Calcite...
    chunwei_lei阅读 1,237评论 0 0
  • 感恩金钱方便给我随喜放生 感恩金钱方便给我捐给需要帮助的人 感恩金钱方便给我提供吃,住,穿的 感恩金钱方便给我学习...
    俏予阅读 86评论 0 0
  • 读书,在其韵。 落日楼头,斜阳草树,望断人生无常;闲暇午后,星夜入眠,阅遍历史沧桑。轻手扬起一段文字,带着诗词...
    羽洛轩阅读 266评论 0 1
  • 突然觉得所有的一切都理顺了,不再是一团乱麻了 该去哪个方向有了明确的指引,心里不再是一团忧郁,一切都散开了,各归各...
    桐羽2018阅读 154评论 0 1