目录
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。也就是列和水位标志、主键,基本也是列相关的一些信息。