Rel转换

1.概述

  Flink SQL的处理流程,SQL完成Parse解析后进行validate校验,校验完成后就就会进行Rel转换,转换的调用在SqlToOperationConverter.toQueryOperation

private PlannerQueryOperation toQueryOperation(FlinkPlannerImpl planner, SqlNode validated) {
    // transform to a relational tree
    RelRoot relational = planner.rel(validated);
    return new PlannerQueryOperation(relational.project());
}

  实际的转换操作完全依赖于Calcite,由SqlToRelConverter.convertQuery完成

2.convertQuery

2.1.整体流程

  校验:这一步在Flink中不做,因为Flink往calcite时复合类型的兼容性有问题
  RelNode转换:这一步是核心,完成了SqlNode向RelNode的转换
  Stream封装:Calcite支持流式语法,这里数对流式语法进行流式封装
  顺序属性:获取物理顺序属性,不限于order关键字
  类型转换校验:校验SqlNode转换RelNode的类型正确性
  附加项获取:RelHint获取
  最终封装:RelRoot封装

2.2.RelNode转换

  这里完成RelRoot的初步转换,在convertQueryRecursive。根据查询的类型,走不同的分支。这边是Select分支

protected RelRoot convertQueryRecursive(SqlNode query, boolean top, RelDataType targetRowType) {
    SqlKind kind = query.getKind();
    switch (kind) {
        case SELECT:
            return RelRoot.of(this.convertSelect((SqlSelect)query, top), kind);
        case WITH:
            return this.convertWith((SqlWith)query, top);

  可以看到,之后走专门的select转换接口——convertSelect。其中使用了Blackboard完成转换,在convertSelectImpl当中进行

public RelNode convertSelect(SqlSelect select, boolean top) {
    SqlValidatorScope selectScope = this.validator.getWhereScope(select);
    Blackboard bb = this.createBlackboard(selectScope, (Map)null, top);
    this.convertSelectImpl(bb, select);
    return bb.root;
}

2.3.Stream封装

  功能就是对stream类型的SQL封装成LogicalDelta。

if (isStream(query)) {
  result = new LogicalDelta(cluster, result.getTraitSet(), result);
}

  stream流式SQL,不是Flink中的流式的概念,而是Calcite对应的SQL语句:select stream。总共有三种特殊类型,其他还有select all和select distinct

public enum SqlSelectKeyword implements Symbolizable {
  DISTINCT,
  ALL,
  STREAM
}

  LogicalDelte是Calcite中的一种封装,是将关系转换为流的关系运算符。跟Flink的动态表的概念类似。例如,如果Orders是一个表,而TableScan(Orders)是一个返回表当前内容的关系操作符,那么Delta(TableScan(Orders))是一个返回表中所有插入的关系操作符。

3.Select转换

  具体操作在SqlToRelConverter.convertSelectImpl当中

3.1.转换From

  在convertFrom接口,这一步是完成from部分的转换。根据上一章节,from部分的SqlNode是转换成join的,所以走join分支。

case JOIN:
    this.convertJoin(bb, (SqlJoin)from);
    return;

  整个convertJoin的转换过程以及转换的结果如下
在这里插入图片描述

  其中,SqlValidatorScope是Calcite中用来校验的信息上下文。Blackboard相当于是一个临时工作区,为后续Select的relNode产生提供一些信息和流程。

3.1.1.转化左右子树

  转换流程主要就是调用的convertFrom,就是一个递归调用,只是入口参数变更了,这里的左右子树是一个基础的sqlCall
在这里插入图片描述

  可以看到,具体的操作类型是AS,所以在convertFrom时走入AS分支而不再是join分支。AS分支的操作会再次递归convertFrom(但是接口不一样)

case AS:
    call = (SqlCall)from;
    SqlNode firstOperand = call.operand(0);
    List<String> fieldNameList = new ArrayList();
    if (call.operandCount() > 2) {
        Iterator var14 = Util.skip(call.getOperandList(), 2).iterator();

        while(var14.hasNext()) {
            node = (SqlNode)var14.next();
            fieldNameList.add(((SqlIdentifier)node).getSimple());
        }
    }

    this.convertFrom(bb, firstOperand, fieldNameList);
    return;

  这一次进入的参数是一个纯粹的标识符
在这里插入图片描述

  标识符分支

case IDENTIFIER:
    this.convertIdentifier(bb, (SqlIdentifier)from, (SqlNodeList)null, (SqlNodeList)null);
    return;

  convertIdentifier里完成向relNode的转换

RelNode tableRel = this.toRel(table, extendedFields);
bb.setRoot(tableRel, true);

  toRel,后续调用table的toRel进行转换,最终走到Flink定义的DynamicSourceUtils当中进行Table向RelNode的转换。其中进行了一些属性设置(比如最重要的LogicTableScan),然后调用Calcite的relBuilder.build完成最终的转换。tableRel的结果是
在这里插入图片描述

  最终返回的左右子树的结果就是这个tableRel

3.1.2.转换Join

  转换左右子树后,进行join的转换

RelNode joinRel = this.createJoin(fromBlackboard, leftRel, rightRel, (RexNode)condition, convertJoinType(join.getJoinType()));
bb.setRoot(joinRel, false);

  最终在createJoin当中,基于Calcite的RelBuilder完成创建,创建主要就是放入左右子树的RelNode

node = this.relBuilder.push(leftRel).push(rightRel).join(joinType, joinCond).build();

  这里面有一个特殊点就是关联,关联类似Join,但在后续优化规则的应用上有不同。这里createJoin中转换join的时候根据是否相关性有走相关性的分支,最终生成的就不是join,而是LogicalCorrelate

CorrelationUse p = this.getCorrelationUse(bb, rightRel);
return LogicalCorrelate.create(leftRel, node, p.id, requiredCols, joinType);

  最终的joinRel结果如下
在这里插入图片描述

3.2.转换where

  转换接口在convertWhere当中,整体转换逻辑和结果如下,生成的是一个LogicalFilter
在这里插入图片描述

3.2.1.NOT条件下推

  这一步的处理目的是将所有的Not关键字下推到IN当中,样例不包含,暂时构建新样例查看执行情况。修改where的条件为

String querySql = "SELECT \n" +
        "  t1.id, 1 + 2 + t1.score AS v \n" +
        "FROM t1, t2 \n" +
        "WHERE \n" +
        "  t1.id Not IN (1,3) AND \n" +
        "  Not t2.id > 1000 AND \n" +
        "  Not t2.id in (2,3)";

  处理主要分两个过程:1、拆分;2、Not关键字转换
  拆分就是如样例中,where后面跟了两个条件,这种情况需要拆分递归处理。在代码中,根据SqlNode类型,如果是AND和OR类型的,则进行递归调用,继续每个分支做pushDownNotForIn进行处理。第一层的分支除了AND和OR外只有NOT和default,default直接原样返回,所以只对NOT起效。
查看运行结果,对比sqlCall和newCall,这个函数的处理就是把Not t2.id in (2,3)变成t2.id Not in (2,3)这种形式,使关键字更加准确。
在这里插入图片描述

3.2.2.替换子查询

  这一步的作用是替换子查询。这里有一个重要的配置,就是是否扩展子查询,如果不扩展,则不作处理,原样返回后封装成子查询的rel。目前好像Calcite有问题,所以默认就是false,不作扩展处理。
  select from select这种形式的子查询的类型是SCALAR_QUERY

case SCALAR_QUERY:
    if (this.config.isExpand()) {
        call = (SqlBasicCall)subQuery.node;
        query = call.operand(0);
        converted = this.convertExists(query, SubQueryType.SCALAR, subQuery.logic, true, (RelDataType)null);

        assert !converted.indicator;

        if (!this.convertNonCorrelatedSubQuery(subQuery, bb, converted.r, false)) {
            rel = this.convertToSingleValueSubq(query, converted.r);
            subQuery.expr = bb.register(rel, JoinRelType.LEFT);
        }
    }

3.2.3.Expression处理

  Expression表达式,主要包含的是列引用和常量值。这一步转换主要是转成RexCall,具体的操作是:1、And这种操作转换成operator;2、常量保持常量;3、列转换成$0这种引用形式
  如下:t1.id转换成$0
在这里插入图片描述

  常量保持常量
在这里插入图片描述

  where条件整体转换完成如下
在这里插入图片描述

3.2.4.Cast处理

  对可以进行精简的Cast语句进行精简,去除缀余的Cast。比如:CAST(1 = 0 AS BOOLEAN)转换后变成1 = 0

3.2.5.转换Filter

  条件语句最终都是过滤,所以这里转换成Filter语句。Filter的input就是上一节转换的join语句,条件就是本节前面转换出来的where的条件。
在这里插入图片描述

3.3.收集排序

  接下来对order进行处理,样例语句中增加。此步骤是将order排序进行收集

"order by v";

  结果是使用一个List进行保存,并且此处已经将排序列表转为Rel类型了
在这里插入图片描述

  之后再将Rel列表转成RelCollation。从类的描述来看,RelFieldCollation是一个RelNode类型,RelCollation则是物理顺序的描述
在这里插入图片描述

3.4.转换SelectList

  就是转换select后面最终要选取的结果的那一部分,一般是列名或者表达式。整个过程其实跟上面的转换where的流程差不多,就是对内容一步步分解,然后对分解的项进行Rel转换。
  以样例来说,列名被替换为$0这种引用行的,然后表达式变成operator封装的计算过程的一个rel逻辑,如下是原始获取的selectList
在这里插入图片描述

  转化的结果如下,AS后面的内容没有转换,而是单独提取出来成为filed成员
在这里插入图片描述

  Filed被单独提取出来,这一部分其实就是最后SQL上的列的名字
在这里插入图片描述

  最终结果是在Blackboard当中添加进root,root的实际内容如下
在这里插入图片描述

3.5.转换order by

  开头是一些不处理场景的识别,比如无排序等

if (this.removeSortInSubQuery(bb.top) || select.getOrderList() == null || select.getOrderList().getList().isEmpty()) {
    assert this.removeSortInSubQuery(bb.top) || collation.getFieldCollations().isEmpty();

    if ((offset == null || offset instanceof SqlLiteral && ((SqlLiteral)offset).bigDecimalValue().equals(BigDecimal.ZERO)) && fetch == null) {
        return;
    }
}

  之后创建一个排序的RelNode——LogicalSort,直接调用对应的create接口创建

bb.setRoot(LogicalSort.create(bb.root, collation, offset == null ? null : this.convertExpression(offset), fetch == null ? null : this.convertExpression(fetch)), false);

  创建的LogicalSort结构如下,主要是collation这个成员,记录了使用哪个字段做排序,使用的排序方式
在这里插入图片描述

  最后如果排序用的列不在select当中,则进一步添加LogicalProject做root。否则的话,整个转换返回的就是LogicalSort类型。

if (orderExprList.size() > 0 && !bb.top) {
    List<RexNode> exprs = new ArrayList();
    RelDataType rowType = bb.root.getRowType();
    int fieldCount = rowType.getFieldCount() - orderExprList.size();

    for(int i = 0; i < fieldCount; ++i) {
        exprs.add(this.rexBuilder.makeInputRef(bb.root, i));
    }

    bb.setRoot(LogicalProject.create(bb.root, ImmutableList.of(), exprs, rowType.getFieldNames().subList(0, fieldCount)), false);
}

4.PlannerQueryOperation封装

  convertQuery流程走完以后,转换relNode基本完成了,接下来就是封装成Flink的结果式——PlannerQueryOperation
  SqlToOperationConverter.toQueryOperation当中,直接调用构造函数创建。relational就是前面转换的relNode。

new PlannerQueryOperation(relational.project());

  PlannerQueryOperation封装就是把relNode里的列信息提取出来单独做了保存,如下,calciteTree保存的是原始的relNode,剩下三项都是列信息
在这里插入图片描述

  最后还有一个ResolvedSchema,这个主要记录了三项内容:columns、watermarkSpecs、primaryKey。也就是列和水位标志、主键,基本也是列相关的一些信息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值