本文主要内容如下:
对比 Spark SQL 的执行流程:https://blog.csdn.net/super_wj0820/article/details/100981862
下面是 Calcite 概念梳理:
Calcite 概念表格展示:
类型 | 描述 | 特点 |
---|---|---|
RelOptRule | transforms an expression into another。对 expression 做等价转换 | 根据传递给它的 RelOptRuleOperand 来对目标 RelNode 树进行规则匹配,匹配成功后,会再次调用 matches() 方法(默认返回真)进行进一步检查。如果 mathes() 结果为真,则调用 onMatch() 进行转换。 |
ConverterRule | Abstract base class for a rule which converts from one calling convention to another without changing semantics. | 它是 RelOptRule 的子类,专门用来做数据源之间的转换(Calling convention),ConverterRule 一般会调用对应的 Converter 来完成工作,比如说:JdbcToSparkConverterRule 调用 JdbcToSparkConverter 来完成对 JDBC Table 到 Spark RDD 的转换。 |
RelNode | relational expression,RelNode 会标识其 input RelNode 信息,这样就构成了一棵 RelNode 树 | 代表了对数据的一个处理操作,常见的操作有 Sort、Join、Project、Filter、Scan 等。它蕴含的是对整个 Relation 的操作,而不是对具体数据的处理逻辑。 |
Converter | A relational expression implements the interface Converter to indicate that it converts a physical attribute, or RelTrait of a relational expression from one value to another. | 用来把一种 RelTrait 转换为另一种 RelTrait 的 RelNode。如 JdbcToSparkConverter 可以把 JDBC 里的 table 转换为 Spark RDD。如果需要在一个 RelNode 中处理来源于异构系统的逻辑表,Calcite 要求先用 Converter 把异构系统的逻辑表转换为同一种 Convention。 |
RexNode | Row-level expression | 行表达式(标量表达式),蕴含的是对一行数据的处理逻辑。每个行表达式都有数据的类型。这是因为在 Valdiation 的过程中,编译器会推导出表达式的结果类型。常见的行表达式包括字面量 RexLiteral, 变量 RexVariable, 函数或操作符调用 RexCall 等。 RexNode 通过 RexBuilder 进行构建。 |
RelTrait | RelTrait represents the manifestation of a relational expression trait within a trait definition. | 用来定义逻辑表的物理相关属性(physical property),三种主要的 trait 类型是:Convention、RelCollation、RelDistribution; |
Convention | Calling convention used to repressent a single data source, inputs must be in the same convention | 继承自 RelTrait,类型很少,代表一个单一的数据源,一个 relational expression 必须在同一个 convention 中; |
RelTraitDef | 主要有三种:ConventionTraitDef:用来代表数据源 RelCollationTraitDef:用来定义参与排序的字段;RelDistributionTraitDef:用来定义数据在物理存储上的分布方式(比如:single、hash、range、random 等); | |
RelOptCluster | An environment for related relational expressions during the optimization of a query. | palnner 运行时的环境,保存上下文信息; |
RelOptPlanner | A RelOptPlanner is a query optimizer: it transforms a relational expression into a semantically equivalent relational expression, according to a given set of rules and a cost model. | 也就是优化器,Calcite 支持RBO(Rule-Based Optimizer) 和 CBO(Cost-Based Optimizer)。Calcite 的 RBO (HepPlanner)称为启发式优化器(heuristic implementation ),它简单地按 AST 树结构匹配所有已知规则,直到没有规则能够匹配为止;Calcite 的 CBO 称为火山式优化器(VolcanoPlanner)成本优化器也会匹配并应用规则,当整棵树的成本降低趋于稳定后,优化完成,成本优化器依赖于比较准确的成本估算。RelOptCost 和 Statistic 与成本估算相关; |
RelOptCost | defines an interface for optimizer cost in terms of number of rows processed, CPU cost, and I/O cost. | 优化器成本模型会依赖; |
Sql 的执行过程一般可以分为下图中的四个阶段,Calcite 同样也是这样:
这里为了讲述方便,把 SQL 的执行分为下面五个阶段(跟上面比比又独立出了一个阶段):
Calcite 使用 JavaCC 做 SQL 解析,JavaCC 根据 Calcite 中定义的 Parser.jj 文件,生成一系列的 java 代码,生成的 Java 代码会把 SQL 转换成 AST 的数据结构(这里是 SqlNode 类型)。
Javacc 实现一个 SQL Parser,它的功能有以下两个,这里都是需要在 jj 文件中定义的。
即:把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示;
经过上面的第一步,会生成一个 SqlNode 对象,它是一个未经验证的抽象语法树,下面就进入了一个语法检查阶段,语法检查前需要知道元数据信息,这个检查会包括表名、字段名、函数名、数据类型的检查。
即:语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
经过第二步之后,这里的 SqlNode 就是经过语法校验的 SqlNode 树,接下来这一步就是将 SqlNode 转换成 RelNode/RexNode,也就是生成相应的逻辑计划(Logical Plan)
即:语义分析,根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan);
第四阶段,也就是 Calcite 的核心所在,优化器进行优化的地方,如过滤条件的下压(push down),在进行 join 操作前,先进行 filter 操作,这样的话就不需要在 join 时进行全量 join,减少参与 join 的数据量等。
在 Calcite 中,提供了两种 planner:HepPlanner 和 VolcanoPlanner,详细可参考下文。
即:逻辑计划优化,优化器的核心,根据前面生成的逻辑计划按照相应的规则(Rule)进行优化;
针对不同的大数据组件,将优化后的plan映射到最终的大数据引擎,如折射成Flink图。
优化器的作用:将解析器生成的关系代数表达式转换成执行计划,供执行引擎执行,在这个过程中,会应用一些规则优化,以帮助生成更高效的执行计划。
Calcite 中 RelOptPlanner 是 Calcite 中优化器的基类:
Calcite 中关于优化器提供了两种实现:
Calcite 参考文章:
https://matt33.com/2019/03/07/apache-calcite-process-flow/
https://matt33.com/2019/03/17/apache-calcite-planner/
Flink Table API&SQL 为流式数据和静态数据的关系查询保留统一的接口,而且利用了Calcite的查询优化框架和SQL parser。
该设计是基于Flink已构建好的API构建的,Flink的 core API 和引擎的所有改进都会自动应用到Table API和SQL上。
一条stream sql从提交到calcite解析、优化最后到flink引擎执行,一般分为以下几个阶段:
而如果是通过table api来提交任务的话,也会经过calcite优化等阶段,基本流程和直接运行sql类似:
可以看出来,Table API 与 SQL 在获取 RelNode 之后是一样的流程,只是获取 RelNode 的方式有所区别:
在flink提供两种API进行关系型查询,Table API 和 SQL。这两种API的查询都会用包含注册过的Table的catalog进行验证,除了在开始阶段从计算逻辑转成logical plan有点差别以外,之后都差不多。同时在stream和batch的查询看起来也是完全一样。只不过flink会根据数据源的性质(流式和静态)使用不同的规则进行优化, 最终优化后的plan转传成常规的Flink DataSet 或 DataStream 程序。
参考官网 StreamSQLExample Demo,Demo SQL 如下:
SELECT
*
FROM
(
(
SELECT
*
FROM
OrderA
WHERE
user < 3
)
UNION ALL
(
SELECT
*
FROM
OrderB
WHERE
product <> 'rubber'
)
) OrderAll
WHERE
amount > 2
表OrderA定义三个字段:user, product, amount,先分别做select查询,再将查询结果 union,最后做select,最外层加了一个Filter,以便触发Filter下推及合并。
以下代码修改自官网 StreamSQLExample Demo,可直接运行:
/**
* Simple example for demonstrating the use of SQL on a Stream Table in Java.
*
* This example shows how to:
* - Convert DataStreams to Tables
* - Register a Table under a name
* - Run a StreamSQL query on the registered Table
*
*/
public class StreamSQLExample {
// *************************************************************************
// PROGRAM
// *************************************************************************
public static void main(String[] args) throws Exception {
// set up execution environment
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tEnv = StreamTableEnvironment.getTableEnvironment(env);
DataStream orderA = env.fromCollection(Arrays.asList(
new Order(1L, "beer", 3),
new Order(1L, "diaper", 4),
new Order(3L, "rubber", 2)));
DataStream orderB = env.fromCollection(Arrays.asList(
new Order(2L, "pen", 3),
new Order(2L, "rubber", 3),
new Order(4L, "beer", 1)));
// register DataStream as Table
tEnv.registerDataStream("OrderA", orderA, "user, product, amount");
tEnv.registerDataStream("OrderB", orderB, "user, product, amount");
// union the two tables
Table result = tEnv.sqlQuery("SELECT " +
"* " +
"FROM " +
"( " +
"SELECT " +
"* " +
"FROM " +
"OrderA " +
"WHERE " +
"user < 3 " +
"UNION ALL " +
"SELECT " +
"* " +
"FROM " +
"OrderB " +
"WHERE " +
"product <> 'rubber' " +
") OrderAll " +
"WHERE " +
"amount > 2");
System.out.println(tEnv.explain(result));
tEnv.toAppendStream(result, Order.class).print();
env.execute();
}
// *************************************************************************
// USER DATA TYPES
// *************************************************************************
/**
* Simple POJO.
*/
public static class Order {
public Long user;
public String product;
public int amount;
public Order() {
}
public Order(Long user, String product, int amount) {
this.user = user;
this.product = product;
this.amount = amount;
}
@Override
public String toString() {
return "Order{" +
"user=" + user +
", product='" + product + '\'' +
", amount=" + amount +
'}';
}
}
}
上述代码中,通过 System.out.println(tEnv.explain(result)); 方法可以打出待执行Sql的抽象语法树(Abstract Syntax Tree)、优化后的逻辑计划以及物理计划:
== Abstract Syntax Tree ==
LogicalProject(user=[$0], product=[$1], amount=[$2])
LogicalFilter(condition=[>($2, 2)])
LogicalUnion(all=[true])
LogicalProject(user=[$0], product=[$1], amount=[$2])
LogicalFilter(condition=[<($0, 3)])
LogicalTableScan(table=[[OrderA]])
LogicalProject(user=[$0], product=[$1], amount=[$2])
LogicalFilter(condition=[<>($1, _UTF-16LE'rubber')])
LogicalTableScan(table=[[OrderB]])
== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
DataStreamScan(table=[[OrderA]])
DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
DataStreamScan(table=[[OrderB]])
== Physical Execution Plan ==
Stage 1 : Data Source
content : collect elements with CollectionInputFormat
Stage 2 : Data Source
content : collect elements with CollectionInputFormat
Stage 3 : Operator
content : from: (user, product, amount)
ship_strategy : FORWARD
Stage 4 : Operator
content : where: (AND(<(user, 3), >(amount, 2))), select: (user, product, amount)
ship_strategy : FORWARD
Stage 5 : Operator
content : from: (user, product, amount)
ship_strategy : FORWARD
Stage 6 : Operator
content : where: (AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))), select: (user, product, amount)
ship_strategy : FORWARD
和前面介绍的 Calcite 处理流程一致,此处Flink解析Flink SQL 的语法和词法解析 完全依赖Calcite提供的SqlParser。
在 tEnv.sqlQuery() 方法中,下面的 Step-1 即为SQL解析过程,入参为 待解析的SQL,返回解析后的 SqlNode 对象。
*TableEnvironment.scala*
def sqlQuery(query: String): Table = {
val planner = new FlinkPlannerImpl(getFrameworkConfig, getPlanner, getTypeFactory)
// Step-1: SQL 解析阶段(SQL–>SqlNode), 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示
val parsed = planner.parse(query)
if (null != parsed && parsed.getKind.belongsTo(SqlKind.QUERY)) {
// Step-2: SqlNode 验证(SqlNode–>SqlNode),语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
val validated = planner.validate(parsed)
// Step-3: 语义分析(SqlNode–>RelNode/RexNode),根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan)
val relational = planner.rel(validated)
new Table(this, LogicalRelNode(relational.rel))
} else {
...
}
}
被解析后的SqlNode AST,每个SQL组成会翻译成一个节点:
SQL在被SqlParser解析后,得到SqlNode组成的 抽象语法树(AST),此后还要根据注册的Catalog对该 SqlNode AST 进行验证。
以下语句注册表OrderA和OrderB:
tEnv.registerDataStream(“OrderA”, orderA, “user, product, amount”);
tEnv.registerDataStream(“OrderB”, orderB, “user, product, amount”);
在 tEnv.sqlQuery() 方法中,下面的 Step-2 即为SQL解析过程,入参为 待验证的SqlNode AST,返回验证后的 SqlNode 对象。
**TableEnvironment.scala**
def sqlQuery(query: String): Table = {
val planner = new FlinkPlannerImpl(getFrameworkConfig, getPlanner, getTypeFactory)
// Step-1: SQL 解析阶段(SQL–>SqlNode), 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示
val parsed = planner.parse(query)
if (null != parsed && parsed.getKind.belongsTo(SqlKind.QUERY)) {
// Step-2: SqlNode 验证(SqlNode–>SqlNode),语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
val validated = planner.validate(parsed)
// Step-3: 语义分析(SqlNode–>RelNode/RexNode),根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan)
val relational = planner.rel(validated)
new Table(this, LogicalRelNode(relational.rel))
} else {
...
}
}
相对于Calcite原生的SQL校验,Flink拓展了语法校验范围,如Flink支持自定义的FunctionCatalog,用于校验SQL Function的入参个数及类型的相关校验,具体用法和细节后续补充。
下面为SQL校验的过程:
**FlinkPlannerImpl.scala**
def validate(sqlNode: SqlNode): SqlNode = {
validator = new FlinkCalciteSqlValidator(
operatorTable,
createCatalogReader(false),
typeFactory)
validator.setIdentifierExpansion(true)
try {
validator.validate(sqlNode)
}
catch {
case e: RuntimeException =>
throw new ValidationException(s"SQL validation failed. ${e.getMessage}", e)
}
}
至此,Flink引擎已将 用户业务 转化成 如下抽象语法树(AST),此AST并未应用任何优化策略,只是Sql节点的原生映射 :
== Abstract Syntax Tree ==
LogicalProject(user=[$0], product=[$1], amount=[$2])
LogicalFilter(condition=[>($2, 2)])
LogicalUnion(all=[true])
LogicalProject(user=[$0], product=[$1], amount=[$2])
LogicalFilter(condition=[<($0, 3)])
LogicalTableScan(table=[[OrderA]])
LogicalProject(user=[$0], product=[$1], amount=[$2])
LogicalFilter(condition=[<>($1, _UTF-16LE'rubber')])
LogicalTableScan(table=[[OrderB]])
前面经过的SQL解析和SQL验证之后得到的SqlNode,仅仅是将SQL解析到java数据结构的固定节点上,并没有给出相关节点之间的关联关系以及每个节点的类型等信息,因此还需要将SqlNode转换为逻辑计划(RelNode)。
在 tEnv.sqlQuery() 方法中,下面的 Step-3 即为SQL解析过程,入参为 验证后的SqlNode,返回的是包含RelNode信息的RelRoot对象。
**TableEnvironment.scala**
def sqlQuery(query: String): Table = {
val planner = new FlinkPlannerImpl(getFrameworkConfig, getPlanner, getTypeFactory)
// Step-1: SQL 解析阶段(SQL–>SqlNode), 把 SQL 转换成为 AST (抽象语法树),在 Calcite 中用 SqlNode 来表示
val parsed = planner.parse(query)
if (null != parsed && parsed.getKind.belongsTo(SqlKind.QUERY)) {
// Step-2: SqlNode 验证(SqlNode–>SqlNode),语法检查,根据元数据信息进行语法验证,验证之后还是用 SqlNode 表示 AST 语法树;
val validated = planner.validate(parsed)
// Step-3: 语义分析(SqlNode–>RelNode/RexNode),根据 SqlNode及元信息构建 RelNode 树,也就是最初版本的逻辑计划(Logical Plan)
val relational = planner.rel(validated)
new Table(this, LogicalRelNode(relational.rel))
} else {
...
}
}
下面为构建逻辑计划的过程:
**FlinkPlannerImpl.scala**
def rel(validatedSqlNode: SqlNode): RelRoot = {
try {
assert(validatedSqlNode != null)
val rexBuilder: RexBuilder = createRexBuilder
val cluster: RelOptCluster = FlinkRelOptClusterFactory.create(planner, rexBuilder)
val sqlToRelConverter: SqlToRelConverter = new SqlToRelConverter(
new ViewExpanderImpl,
validator,
createCatalogReader(false),
cluster,
convertletTable,
sqlToRelConverterConfig)
root = sqlToRelConverter.convertQuery(validatedSqlNode, false, true)
root
} catch {
case e: RelConversionException => throw new TableException(e.getMessage)
}
}
至此,用户通过 StreamTableEnvironment 对象 注册的Calatlog信息 和 业务Sql 都 转化成了 逻辑计划(Logical Plan),同时,TableApi和SqlApi 也在 Logical Plan 这里达成一致,后续进行的优化阶段、生成物理计划和生成DataStream,都是相同的过程。
tEnv.sqlQuery() 返回 Table 对象,在Flink中,Table对象既可通过TableApi生成,也可以通过SqlApi生成,TableApi和SqlApi至此达成一致。
在业务代码中,toAppendStream方法会进行 Logical Plan 的优化、生成物理计划以及生成DataStream的过程:
tEnv.toAppendStream(result, Order.class).print();
跟踪代码,会进入 StreamTableEnvironment.scala 的 translate 方法:
**StreamTableEnvironment.scala**
protected def translate[A](
table: Table,
queryConfig: StreamQueryConfig,
updatesAsRetraction: Boolean,
withChangeFlag: Boolean)(implicit tpe: TypeInformation[A]): DataStream[A] = {
// 获取 逻辑计划(Logical Plan)
val relNode = table.getRelNode
// Step-4: 优化阶段 + Step-5: 生成物理计划
val dataStreamPlan = optimize(relNode, updatesAsRetraction)
val rowType = getResultType(relNode, dataStreamPlan)
// Step-6: 转成DataStream
translate(dataStreamPlan, rowType, queryConfig, withChangeFlag)
}
Calcite框架允许我们使用规则来优化逻辑计划,Flink在Optimize过程中,使用 FlinkRuleSets 定义优化规则进行优化:
此处,简单描述下各RuleSet的作用:
针对批/流应用,采用不同的Rule进行优化,下面是各规则的优化过程:
**StreamTableEnvironment.scala**
private[flink] def optimize(relNode: RelNode, updatesAsRetraction: Boolean): RelNode = {
// 优化子查询,根据 TABLE_SUBQUERY_RULES 应用 HepPlanner 规则优化
val convSubQueryPlan = optimizeConvertSubQueries(relNode)
// 扩展计划优化,根据 EXPAND_PLAN_RULES 和 POST_EXPAND_CLEAN_UP_RULES 应用 HepPlanner 规则优化
val expandedPlan = optimizeExpandPlan(convSubQueryPlan)
val decorPlan = RelDecorrelator.decorrelateQuery(expandedPlan)
val planWithMaterializedTimeAttributes =
RelTimeIndicatorConverter.convert(decorPlan, getRelBuilder.getRexBuilder)
// 正常化流式计算,根据 DATASTREAM_NORM_RULES 应用 HepPlanner 规则优化
val normalizedPlan = optimizeNormalizeLogicalPlan(planWithMaterializedTimeAttributes)
// 逻辑计划优化,根据 LOGICAL_OPT_RULES 应用 VolcanoPlanner 规则优化
val logicalPlan = optimizeLogicalPlan(normalizedPlan)
// 优化流式计算,根据 DATASTREAM_OPT_RULES 应用 Volcano 规则优化
val physicalPlan = optimizePhysicalPlan(logicalPlan, FlinkConventions.DATASTREAM)
// 装饰流式计算,根据 DATASTREAM_DECO_RULES 应用 HepPlanner 规则优化
optimizeDecoratePlan(physicalPlan, updatesAsRetraction)
}
由上述过程也可以看出,Flink基于FlinkRuleSets的rule进行转换的过程中,既包含了 优化 logical Plan 的过程,也包括了生成 Flink PhysicalPlan 的过程。
从 3.3.5.1 节的优化过程可看出,Flink在进行 logical Plan 优化之前,会应用 HepPlanner 针对 TABLE_SUBQUERY_RULES、EXPAND_PLAN_RULES、POST_EXPAND_CLEAN_UP_RULES、DATASTREAM_NORM_RULES 这些规则进行预处理,处理完之后 才会应用 VolcanoPlanner 针对 LOGICAL_OPT_RULES 中罗列的优化规则,尝试使用不同的规则优化,试图计算出最优的一种优化plan返回。
应用 HepPlanner 针对 预处理规则 进行预处理后,会得到 Logic RelNode :
对比 Sql解析之后得到的 SqlNode 发现, Logic RelNode 同样持有 Sql 各组成的 映射信息,除此之外,相比SqlNode,Logic RelNode 加入了各节点的 rowType 类型信息。
VolcanoPlanner 根据 FlinkRuleSets.LOGICAL_OPT_RULES 找到最优的执行Planner,并转换为 Flink Logical RelNode 返回:
应用 VolcanoPlanner 针对 FlinkRuleSets.DATASTREAM_OPT_RULES,将 Optimized Logical RelNode 转换为 Flink Physic Plan (Flink Logical RelNode -> DataStream RelNode)。
== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
DataStreamScan(table=[[OrderA]])
DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
DataStreamScan(table=[[OrderB]])
如果是 RetractStream 则还会使用 FlinkRuleSets.DATASTREAM_DECO_RULES 进行 Retract特征 的一个包装:
至此,Step-4: 优化阶段 + Step-5: 生成物理计划 已完成。
StreamTableEnvironment.scala 的 translate 方法中最后一步,Step-6:转成DataStream,此处将用户的业务Sql最终转成 Stream Api 执行。
**StreamTableEnvironment.scala**
protected def translate[A](
table: Table,
queryConfig: StreamQueryConfig,
updatesAsRetraction: Boolean,
withChangeFlag: Boolean)(implicit tpe: TypeInformation[A]): DataStream[A] = {
// 获取 逻辑计划(Logical Plan)
val relNode = table.getRelNode
// Step-4: 优化阶段 + Step-5: 生成物理计划
val dataStreamPlan = optimize(relNode, updatesAsRetraction)
val rowType = getResultType(relNode, dataStreamPlan)
// Step-6: 转成DataStream
translate(dataStreamPlan, rowType, queryConfig, withChangeFlag)
}
跟踪代码,查看 translate 方法的具体实现:
**StreamTableEnvironment.scala**
protected def translate[A](
logicalPlan: RelNode,
logicalType: RelDataType,
queryConfig: StreamQueryConfig,
withChangeFlag: Boolean)
(implicit tpe: TypeInformation[A]): DataStream[A] = {
// ...
// get CRow plan :关键方法
val plan: DataStream[CRow] = translateToCRow(logicalPlan, queryConfig)
// ...
}
protected def translateToCRow(
logicalPlan: RelNode,
queryConfig: StreamQueryConfig): DataStream[CRow] = {
logicalPlan match {
case node: DataStreamRel =>
// 依次递归调用每个节点的 translateToPlan 方法,将 DataStreamRelNode 转化为 DataStream,最终生成 DataStreamGraph
node.translateToPlan(this, queryConfig)
case _ =>
throw new TableException("Cannot generate DataStream due to an invalid logical plan. " +
"This is a bug and should not happen. Please file an issue.")
}
}
针对优化后得到的逻辑计划(实际已转成物理计划 DataStreamRel),由外到内遍历各节点,将 DataStreamRel Node 转化为 DataStream,以下面物理计划为例:
== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
DataStreamScan(table=[[OrderA]])
DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
DataStreamScan(table=[[OrderB]])
依次递归调用 DataStreamUnion、DataStreamCalc、DataStreamScan 类中 重写的 translateToPlan 方法,将各节点的 DataStreamRel 实现 转化为 DataStream 执行计划的实现。
== Physical Execution Plan ==
Stage 1 : Data Source
content : collect elements with CollectionInputFormat
Stage 2 : Data Source
content : collect elements with CollectionInputFormat
Stage 3 : Operator
content : from: (user, product, amount)
ship_strategy : FORWARD
Stage 4 : Operator
content : where: (AND(<(user, 3), >(amount, 2))), select: (user, product, amount)
ship_strategy : FORWARD
Stage 5 : Operator
content : from: (user, product, amount)
ship_strategy : FORWARD
Stage 6 : Operator
content : where: (AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))), select: (user, product, amount)
ship_strategy : FORWARD
备注:在生成 DataStream 的过程中,使用到CodeGen生成成Flink的各种算子。后面会详细说明
补充:
关于 DataStreamRel 的类继承关系如下图所示,RelNode 是 Calcite 定义的 Sql节点关系 数据结构,FlinkRelNode 继承自 RelNode,其有三个实现,分别是FlinkLogicalRel、DataStreamRel、DataSetRel,分别对应Flink内部 对 Sql 表达式的 逻辑计划的描述以及物理计划的描述。
在递归调用各个节点 DataStreamRel 的 translateToPlan 方法时,会利用CodeGen元编程成Flink的各种算子,就相当于我们直接利用Flink的DataSet或DataStream API开发的程序。
== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[user, product, amount])
DataStreamCalc(select=[user, product, amount], where=[AND(<(user, 3), >(amount, 2))])
DataStreamScan(table=[[OrderA]])
DataStreamCalc(select=[user, product, amount], where=[AND(<>(product, _UTF-16LE'rubber'), >(amount, 2))])
DataStreamScan(table=[[OrderB]])
还是以上面的Demo为例,跟踪进 DataStreamScan 的 translateToPlan 方法中,会发现相关逻辑:
后续在 扩展 flink语法(如join维表)时,需要针对上述步骤,拼接生成 function 的字符串形式。
在 FunctionCodeGenerator.scala 中,可调试至图处,查看拼接成的 Function String形式,以方便调试。
了解完 Flink Sql 的执行流程之后,就可以针对 Flink Sql 做语法、功能上的扩展。
在Flink老版本上,Flink不支持 COUNT(DISTINCT aaa) 语法,但是如果需要对 Flink 做此功能拓展,需要结合 前面说到的 Flink Sql 执行流程,做相应修改。
修改点:
在 DATASTREAM_OPT_RULES.DataStreamGroupWindowAggregateRule 中放开对 Distinct 的限制:
内部实现…