Calcite CBO 分析1
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 Search、The 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获取CostRelOptCost 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来获取Costreturn 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>> Hrevise(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
的方法,可以参考TableScan
的computeSelfCost
方法,在构造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
中还有许多其它的元数据实现,如Distribution
、Collation
等,感兴趣的同学可以自行看一下。
总结
本文简要说明了一个Calcite CBO 代价计算的基本逻辑及流程,关于CBO中CBO的执行流程,请参考下后续文章。
文章来源:https://www.jianshu.com/p/1f00740a924a
Calcite CBO 分析1相关推荐
- RBO 和CBO分析
ORACLE 提供了CBO.RBO两种SQL优化器. Rule based optimizer(RBO) 基于规则 Cost based optimizer (CBO) 基于成本,或者讲统计信息 1: ...
- SQL查询优化器浅析-字节跳动大数据青训营
1.大数据体系和SQL 1.1SQL流行原因 SQL 是用于访问和处理数据库的标准的计算机语言. 1.SQL 指结构化查询语言 2.SQL 使我们有能力访问数据库 3.SQL 是一种 ANSI 的标准 ...
- Oracle优化之SQL
SQL(StructURED Query Language):结构化查询语言 在进行Oracle优化之前,先让我们花时间来聊聊SQL,并了解下SQL的前世今生. 在此先通过3W原则来讲下SQL: wh ...
- ORACLE的CBO及表分析
最近接触到了表分析的几个案例,有一张表经过分析之后,在上面的sql语句的执行时间一下子从50分钟锐减到2分钟,不可思议. 第一部分 什么是表分析?简单的说,就是收集表和索引的信息,CBO根据这些信息决 ...
- mysql cbo_SparkSQL2.4中启用CBO时JoinReorder分析
背景 Spark Join方式 SparkSQL目前支持三种join方式broadcast hash join:将小表广播分发到大表所在的结点上 ,并行在各节点上进行hash join仅适合内表非常小 ...
- 回首2018 | 分析型数据库AnalyticDB:不忘初心 砥砺前行...
导读 分析型数据库AnalyticDB(下文简称"ADB"),是阿里巴巴自主研发.唯一经过超大规模以及核心业务验证的PB级实时数据仓库.截止目前,现有外部支撑客户既包括传统的大中型 ...
- Calcite 原理解析
Apache Calcite 是独立于存储与执行的SQL解析.优化引擎,广泛应用于各种离线.搜索.实时查询引擎,如Drill.Hive.Kylin.Solr.flink.Samza等.本文结合hive ...
- Apache Calcite 处理流程详解(一)
关于 Apache Calcite 的简单介绍可以参考 Apache Calcite:Hadoop 中新型大数据查询引擎 这篇文章,Calcite 一开始设计的目标就是 one size fits a ...
- 如何打造一款极速分析型数据库
一 极速 OLAP 数据库:预计算 VS 现场计算 1.1 Materialized View 1.2 预聚合 1.3 索引 1.4 Cache二 极速 OLAP 数据库:可扩展性三 极速 OLAP ...
最新文章
- RabbitMQ入门之安装配置与简单实例
- 我与 美国作家 21天精通C++ 作者 Rao的对话:
- swift:简单使用翻页控制器UIPageViewController
- C语言的特点是什么?
- 【Linux】Linux中目录结构说明
- Curator实现分布式锁的基本原理-getTheLock
- ModelView矩阵各列含义及说明
- CF1253F Cheap Robot
- Mybatis参数传递及返回类型
- 华科10年计算机考研复试笔试(算法基础)(1)
- CV Code | 计算机视觉开源周报20190904期
- 程序员c语言简历,程序员的简历该怎么写?当然是程序!
- 简书python_python爬虫(以简书为例)
- Matlab K均值图像分割
- wps怎么图片透明_wps中图片怎么样调透明度_word设置图片背景透明的图文教程-爱纯净...
- SAP中计划策略11测试
- 小程序倒计时,已知过期时间实现倒计时
- 网络不稳定 网速忽高忽低,ping值忽高忽低的解决办法 无线网出现问题解决
- CSS实现3D正方体动态旋转效果【源码+GIF图】
- 面试必备之深入理解自旋锁