Calcite RBO rule 解析和自定義

什么是查詢優(yōu)化器

查詢優(yōu)化器是傳統(tǒng)數(shù)據(jù)庫(kù)的核心模塊,也是大數(shù)據(jù)計(jì)算引擎的核心模塊,開源大數(shù)據(jù)引擎如 Impala、Presto、Drill、HAWQ、 Spark、Hive 等都有自己的查詢優(yōu)化器。Calcite 就是從 Hive 的優(yōu)化器演化而來(lái)的。

優(yōu)化器的作用:將解析器生成的關(guān)系代數(shù)表達(dá)式轉(zhuǎn)換成執(zhí)行計(jì)劃,供執(zhí)行引擎執(zhí)行,在這個(gè)過(guò)程中,會(huì)應(yīng)用一些規(guī)則優(yōu)化,以幫助生成更高效的執(zhí)行計(jì)劃。

基于成本優(yōu)化(CBO)

基于代價(jià)的優(yōu)化器(Cost-Based Optimizer,CBO):根據(jù)優(yōu)化規(guī)則對(duì)關(guān)系表達(dá)式進(jìn)行轉(zhuǎn)換,這里的轉(zhuǎn)換是說(shuō)一個(gè)關(guān)系表達(dá)式經(jīng)過(guò)優(yōu)化規(guī)則后會(huì)生成另外一個(gè)關(guān)系表達(dá)式,同時(shí)原有表達(dá)式也會(huì)保留,經(jīng)過(guò)一系列轉(zhuǎn)換后會(huì)生成多個(gè)執(zhí)行計(jì)劃,然后 CBO 會(huì)根據(jù)統(tǒng)計(jì)信息和代價(jià)模型 (Cost Model) 計(jì)算每個(gè)執(zhí)行計(jì)劃的 Cost,從中挑選 Cost 最小的執(zhí)行計(jì)劃。

由上可知,CBO 中有兩個(gè)依賴:統(tǒng)計(jì)信息和代價(jià)模型。統(tǒng)計(jì)信息的準(zhǔn)確與否、代價(jià)模型的合理與否都會(huì)影響 CBO 選擇最優(yōu)計(jì)劃。 從上述描述可知,CBO 是優(yōu)于 RBO 的,原因是 RBO 是一種只認(rèn)規(guī)則,對(duì)數(shù)據(jù)不敏感的呆板的優(yōu)化器,而在實(shí)際過(guò)程中,數(shù)據(jù)往往是有變化的,通過(guò) RBO 生成的執(zhí)行計(jì)劃很有可能不是最優(yōu)的。事實(shí)上目前各大數(shù)據(jù)庫(kù)和大數(shù)據(jù)計(jì)算引擎都傾向于使用 CBO,但是對(duì)于流式計(jì)算引擎來(lái)說(shuō),使用 CBO 還是有很大難度的,因?yàn)椴⒉荒芴崆邦A(yù)知數(shù)據(jù)量等信息,這會(huì)極大地影響優(yōu)化效果,CBO 主要還是應(yīng)用在離線的場(chǎng)景。

基于規(guī)則優(yōu)化(RBO)

基于規(guī)則的優(yōu)化器(Rule-Based Optimizer,RBO):根據(jù)優(yōu)化規(guī)則對(duì)關(guān)系表達(dá)式進(jìn)行轉(zhuǎn)換,這里的轉(zhuǎn)換是說(shuō)一個(gè)關(guān)系表達(dá)式經(jīng)過(guò)優(yōu)化規(guī)則后會(huì)變成另外一個(gè)關(guān)系表達(dá)式,同時(shí)原有表達(dá)式會(huì)被裁剪掉,經(jīng)過(guò)一系列轉(zhuǎn)換后生成最終的執(zhí)行計(jì)劃。

RBO 中包含了一套有著嚴(yán)格順序的優(yōu)化規(guī)則,同樣一條 SQL,無(wú)論讀取的表中數(shù)據(jù)是怎么樣的,最后生成的執(zhí)行計(jì)劃都是一樣的。同時(shí),在 RBO 中 SQL 寫法的不同很有可能影響最終的執(zhí)行計(jì)劃,從而影響執(zhí)行計(jì)劃的性能。

優(yōu)化規(guī)則

無(wú)論是 RBO,還是 CBO 都包含了一系列優(yōu)化規(guī)則,這些優(yōu)化規(guī)則可以對(duì)關(guān)系表達(dá)式進(jìn)行等價(jià)轉(zhuǎn)換,常見的優(yōu)化規(guī)則包含:

  1. 謂詞下推 Predicate Pushdown
  2. 常量折疊 Constant Folding
  3. 列裁剪
  4. 其他

在 Calcite 的代碼里,有一個(gè)測(cè)試類(org.apache.calcite.test.RelOptRulesTest)匯集了對(duì)目前內(nèi)置所有 Rules 的測(cè)試 case,這個(gè)測(cè)試類可以方便我們了解各個(gè) Rule 的作用。

RBO的規(guī)則在Calite 1.24版本的時(shí)候,集中在org.apache.calcite.rel.rules.CoreRules中

在這里有下面一條 SQL,通過(guò)這條語(yǔ)句來(lái)說(shuō)明一下上面介紹的這些種規(guī)則。

select 10 + 30, users.name, users.age
from users join jobs on users.id= user.id
where users.age > 30 and jobs.id>10

謂詞下推(Predicate Pushdown)

關(guān)于謂詞下推,它主要還是從關(guān)系型數(shù)據(jù)庫(kù)借鑒而來(lái),關(guān)系型數(shù)據(jù)中將謂詞下推到外部數(shù)據(jù)庫(kù)用以減少數(shù)據(jù)傳輸;屬于邏輯優(yōu)化,優(yōu)化器將謂詞過(guò)濾下推到數(shù)據(jù)源,使物理執(zhí)行跳過(guò)無(wú)關(guān)數(shù)據(jù)。最常見的例子就是 join 與 filter 操作一起出現(xiàn)時(shí),提前執(zhí)行 filter 操作以減少處理的數(shù)據(jù)量,將 filter 操作下推,以上面例子為例,示意圖如下

(對(duì)應(yīng) Calcite 中的CoreRules.FILTER_INTO_JOIN):

image

在進(jìn)行 join 前進(jìn)行相應(yīng)的過(guò)濾操作,可以極大地減少參加 join 的數(shù)據(jù)量。

常量折疊(Constant Folding)

常量折疊也是常見的優(yōu)化策略,這個(gè)比較簡(jiǎn)單、也很好理解,可以看下 編譯器優(yōu)化 – 常量折疊 這篇文章,基本不用動(dòng)腦筋就能理解,對(duì)于我們這里的示例,有一個(gè)常量表達(dá)式 10 + 30,如果不進(jìn)行常量折疊,那么每行數(shù)據(jù)都需要進(jìn)行計(jì)算,進(jìn)行常量折疊后的結(jié)果如下圖所示

( 對(duì)應(yīng) Calcite 中的 中的CoreRules.PROJECT_REDUCE_EXPRESSIONS Rule):

image

列裁剪(Column Pruning)

列裁剪也是一個(gè)經(jīng)典的優(yōu)化規(guī)則,在本示例中對(duì)于jobs 表來(lái)說(shuō),并不需要掃描它的所有列值,而只需要列值 id,所以在掃描 jobs 之后需要將其他列進(jìn)行裁剪,只留下列 id。這個(gè)優(yōu)化帶來(lái)的好處很明顯,大幅度減少了網(wǎng)絡(luò) IO、內(nèi)存數(shù)據(jù)量的消耗。裁剪前后的示意圖如下(不過(guò)并沒(méi)有找到 Calcite 對(duì)應(yīng)的 Rule):

image

如何使用RBO

public class CalciteRuleTest {


    public static void main(String[] args) throws SqlParseException, ValidationException, RelConversionException {

        Planner planner = getPlanner();

        SqlNode sqlNode = planner.parse("SELECT DATE_CD,SUM(IB0002001_CN000) FROM \n" +
                "(SELECT IDX_ID,CUBE2L_IB00040010_CN000.DATE_CD,SUM(CUBE2L_IB00040010_CN000.IDX_VAL) AS IB0002001_CN000 FROM " +
                "CUBE2L_IB00040010_CN000" +
                " GROUP BY  CUBE2L_IB00040010_CN000.DATE_CD,IDX_ID) IB0002001_CN000  WHERE IDX_ID IN ('IB0002001_CN000') AND DATE_CD = '2020-05-31' GROUP BY DATE_CD ");



        System.out.println(sqlNode.toString());

        planner.validate(sqlNode);
        RelRoot relRoot = planner.rel(sqlNode);

        RelNode relNode = relRoot.project();

        System.out.println();
        System.out.print(RelOptUtil.toString(relNode));


        HepProgramBuilder builder = new HepProgramBuilder();
//        builder.addRuleInstance(CoreRules.FILTER_MERGE); //note: 添加 rule
//        builder.addRuleInstance(CoreRules.PROJECT_FILTER_TRANSPOSE);
//        builder.addRuleInstance(CoreRules.FILTER_AGGREGATE_TRANSPOSE);
//        builder.addRuleInstance(CoreRules.FILTER_PROJECT_TRANSPOSE);
//        builder.addRuleInstance(CoreRules.FILTER_AGGREGATE_TRANSPOSE);
        builder.addRuleInstance(CoreRules.AGGREGATE_REMOVE);
        builder.addRuleInstance(CoreRules.PROJECT_FILTER_TRANSPOSE);
        HepPlanner hepPlanner = new HepPlanner(builder.build());
        hepPlanner.setRoot(relNode);
        relNode = hepPlanner.findBestExp();

        System.out.println();
        System.out.println("After --------------------");
        System.out.println();
        System.out.print(RelOptUtil.toString(relNode));


        RelToSqlConverter relToSqlConverter = new RelToSqlConverter(MysqlSqlDialect.DEFAULT);
        SqlImplementor.Result result = relToSqlConverter.visitRoot(relNode);


        System.out.println();
        System.out.println(result.asSelect().toString());
    }


    private static Planner getPlanner() {
        SchemaPlus rootSchema = Frameworks.createRootSchema(true);


        // 添加表的schema信息,才可以通過(guò)validate
        rootSchema.add("CUBE2L_IB00040010_CN000", new AbstractTable() {
            public RelDataType getRowType(final RelDataTypeFactory typeFactory) {

                RelDataTypeFactory.Builder builder = typeFactory.builder();
                builder.add("DATE_CD", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.DATE), true));
                builder.add("IDX_VAL", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.BIGINT), true));
                builder.add("test", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.BIGINT), true));
                builder.add("IDX_ID", typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.VARCHAR), true));

                return builder.build();
            }
        });

        SqlParser.ConfigBuilder parserConfig = SqlParser.configBuilder();
        SqlParser.Config build = parserConfig.setCaseSensitive(false).setLex(Lex.MYSQL).build();

        FrameworkConfig frameworkConfig = Frameworks.newConfigBuilder()
                .parserConfig(build)
                .defaultSchema(rootSchema)
                .build();

        Planner planner = Frameworks.getPlanner(frameworkConfig);
        return planner;
    }

上面的代碼總共分為三步:

  1. 初始化 HepProgram 對(duì)象;
  2. 初始化 HepPlanner 對(duì)象,并通過(guò) setRoot() 方法將 RelNode 樹轉(zhuǎn)換成 HepPlanner 內(nèi)部使用的 Graph;
  3. 通過(guò) findBestExp() 找到最優(yōu)的 plan,規(guī)則的匹配都是在這里進(jìn)行。

1. 初始化 HepProgram

這幾步代碼實(shí)現(xiàn)沒(méi)有太多需要介紹的地方,先初始化 HepProgramBuilder 也是為了后面初始化 HepProgram 做準(zhǔn)備,HepProgramBuilder 主要也就是提供了一些配置設(shè)置和添加規(guī)則的方法等,常用的方法如下:

  1. addRuleInstance():注冊(cè)相應(yīng)的規(guī)則;
  2. addRuleCollection():這里是注冊(cè)一個(gè)規(guī)則集合,先把規(guī)則放在一個(gè)集合里,再注冊(cè)整個(gè)集合,如果規(guī)則多的話,一般是這種方式;
  3. addMatchLimit():設(shè)置 MatchLimit,這個(gè) rule match 次數(shù)的最大限制;
  4. addMatchOrder() Rule 匹配的順序

HepProgram 這個(gè)類對(duì)于后面 HepPlanner 的優(yōu)化很重要,它定義 Rule 匹配的順序,默認(rèn)按【深度優(yōu)先】順序,它可以提供以下幾種(見 HepMatchOrder 類):

  1. ARBITRARY:按任意順序匹配(因?yàn)樗怯行У?,而且大部分?Rule 并不關(guān)心匹配順序);
  2. BOTTOM_UP:自下而上,先從子節(jié)點(diǎn)開始匹配;
  3. TOP_DOWN:自上而下,先從父節(jié)點(diǎn)開始匹配;
  4. DEPTH_FIRST:深度優(yōu)先匹配,某些情況下比 ARBITRARY 高效(為了避免新的 vertex 產(chǎn)生后又從 root 節(jié)點(diǎn)開始匹配)。

這個(gè)匹配順序到底是什么呢?對(duì)于規(guī)則集合 rules,HepPlanner 的算法是:從一個(gè)節(jié)點(diǎn)開始,跟 rules 的所有 Rule 進(jìn)行匹配,匹配上就進(jìn)行轉(zhuǎn)換操作,這個(gè)節(jié)點(diǎn)操作完,再進(jìn)行下一個(gè)節(jié)點(diǎn),這里的匹配順序就是指的節(jié)點(diǎn)遍歷順序(這種方式的優(yōu)劣,我們下面再說(shuō))。

分析現(xiàn)有rule

最簡(jiǎn)單的例子,Join結(jié)合律,JoinAssociateRule

首先所有的Rule都繼承RelOptRule類

/**
 * Planner rule that changes a join based on the associativity rule.
 *
 * <p>((a JOIN b) JOIN c) &rarr; (a JOIN (b JOIN c))</p>
 *
 * <p>We do not need a rule to convert (a JOIN (b JOIN c)) &rarr;
 * ((a JOIN b) JOIN c) because we have
 * {@link JoinCommuteRule}.
 *
 * @see JoinCommuteRule
 */
public class JoinAssociateRule extends RelOptRule implements TransformationRule 

對(duì)于Join結(jié)合律,調(diào)用super,即RelOptRule的構(gòu)造函數(shù),匹配的樹狀為

join
   join 
      any
 /**
   * Creates a JoinAssociateRule.
   */
  public JoinAssociateRule(RelBuilderFactory relBuilderFactory) {
    super(
        operand(Join.class,
            operand(Join.class, any()),
            operand(RelSubset.class, any())),
        relBuilderFactory, null);
  }

operand也是一個(gè)樹形結(jié)構(gòu),Top的Operand的類型是Join,他有兩個(gè)children,其中一個(gè)也是join,另一個(gè)是RelSubset

他們的children是any()

public void onMatch(final RelOptRuleCall call) {
    final Join topJoin = call.rel(0);
    final Join bottomJoin = call.rel(1);
    final RelNode relA = bottomJoin.getLeft();
    final RelNode relB = bottomJoin.getRight();
    final RelSubset relC = call.rel(2);
    final RelOptCluster cluster = topJoin.getCluster();
    final RexBuilder rexBuilder = cluster.getRexBuilder();

    if (relC.getConvention() != relA.getConvention()) {
      // relC could have any trait-set. But if we're matching say
      // EnumerableConvention, we're only interested in enumerable subsets.
      return;
    }

    //        topJoin
    //        /     \
    //   bottomJoin  C
    //    /    \
    //   A      B

    final int aCount = relA.getRowType().getFieldCount();
    final int bCount = relB.getRowType().getFieldCount();
    final int cCount = relC.getRowType().getFieldCount();
    final ImmutableBitSet aBitSet = ImmutableBitSet.range(0, aCount);
    final ImmutableBitSet bBitSet =
        ImmutableBitSet.range(aCount, aCount + bCount);

    if (!topJoin.getSystemFieldList().isEmpty()) {
      // FIXME Enable this rule for joins with system fields
      return;
    }

    // If either join is not inner, we cannot proceed.
    // (Is this too strict?)
    if (topJoin.getJoinType() != JoinRelType.INNER
        || bottomJoin.getJoinType() != JoinRelType.INNER) {
      return;
    }

    // Goal is to transform to
    //
    //       newTopJoin
    //        /     \
    //       A   newBottomJoin
    //               /    \
    //              B      C

    // Split the condition of topJoin and bottomJoin into a conjunctions. A
    // condition can be pushed down if it does not use columns from A.
    final List<RexNode> top = new ArrayList<>();
    final List<RexNode> bottom = new ArrayList<>();
    JoinPushThroughJoinRule.split(topJoin.getCondition(), aBitSet, top, bottom);
    JoinPushThroughJoinRule.split(bottomJoin.getCondition(), aBitSet, top,
        bottom);

    // Mapping for moving conditions from topJoin or bottomJoin to
    // newBottomJoin.
    // target: | B | C      |
    // source: | A       | B | C      |
    final Mappings.TargetMapping bottomMapping =
        Mappings.createShiftMapping(
            aCount + bCount + cCount,
            0, aCount, bCount,
            bCount, aCount + bCount, cCount);
    final List<RexNode> newBottomList =
        new RexPermuteInputsShuttle(bottomMapping, relB, relC)
            .visitList(bottom);
    RexNode newBottomCondition =
        RexUtil.composeConjunction(rexBuilder, newBottomList);

    final Join newBottomJoin =
        bottomJoin.copy(bottomJoin.getTraitSet(), newBottomCondition, relB,
            relC, JoinRelType.INNER, false);

    // Condition for newTopJoin consists of pieces from bottomJoin and topJoin.
    // Field ordinals do not need to be changed.
    RexNode newTopCondition = RexUtil.composeConjunction(rexBuilder, top);
    @SuppressWarnings("SuspiciousNameCombination")
    final Join newTopJoin =
        topJoin.copy(topJoin.getTraitSet(), newTopCondition, relA,
            newBottomJoin, JoinRelType.INNER, false);

    call.transformTo(newTopJoin);
  }

自定義rule

補(bǔ)充下基礎(chǔ)概念

RexLiteral表示常量,RexVariable表示變量,RexCall表示操作來(lái)連接Literal和Variable

自定義rule,有三個(gè)主要的方法,一個(gè)是matches,一個(gè)是onMatch,一個(gè)是構(gòu)造函數(shù)

判斷一個(gè)tree是否滿足該rule,先從構(gòu)造函數(shù)開始匹配,滿足構(gòu)造函數(shù)的tree,在滿足matches方法,就可以進(jìn)入到onMatch方法。

matches方法默認(rèn)是返回true,可以進(jìn)行改寫

 @Override
    public boolean matches(RelOptRuleCall call) {
        return super.matches(call);
    }

假設(shè)想要匹配

join
  project

構(gòu)造函數(shù)得這么寫

 super(
        operand(Join.class,
            operand(Project.class, any())),
        relBuilderFactory, null);

在onMatch中可以通過(guò)call.rel(x)獲取構(gòu)造函數(shù)中匹配的relNode

Join join = call.rel(0);
Project project = call.rel(1);

通過(guò)一些強(qiáng)轉(zhuǎn)可以獲取對(duì)應(yīng)的節(jié)點(diǎn)

((HepRelVertex)rel.getInput(0)).getCurrentRel()

創(chuàng)建rebuild,生成node

 RelBuilder relBuilder = call.builder();
 RelNode build = relBuilder.build();

對(duì)relBuilder操作


relBuilder.push 加入數(shù)據(jù)源
relbuilder.project 加入投影 還可以agg filter 等等

通過(guò)call替換掉原來(lái)的tree

call.transformTo(node);

常用函數(shù)

RexUtil.composeDisjunction 以or合并兩個(gè)規(guī)則

RexUtil.composeConjunction 以and合并兩個(gè)規(guī)則

((HepRelVertex)rel.getInput(0)).getCurrentRel() 強(qiáng)轉(zhuǎn)獲取節(jié)點(diǎn)

node.getCluster()可以繼續(xù)get出很多東西getPlanner()、getRexBuilder()、getTypeFactory()

RelDataType sqlType = typeFactory.createSqlType(SqlTypeName.ANY) 創(chuàng)建對(duì)應(yīng)的類型

rexBuilder.makeLiteral 創(chuàng)建常量 makeCall創(chuàng)建操作符等等

SqlStdOperatorTable可以獲取函數(shù)集合 SUM MIN MAX

Reference

Calcite分析 - Rule

Apache Calcite 優(yōu)化器詳解(二)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容