flinksql 利用了Apache Calcite的查询优化框架和SQL parser
Calcite的五个阶段 :
Parse:语法解析,把 SQL 语句转换成为一个抽象语法树(AST),在 Calcite 中用 SqlNode
来表示(SQL–>SqlNode)
Validate:语法校验,根据catalog进行验证,例如查询的表、使用的函数是否存在等,校验之后仍然是 SqlNode
构成的语法树(SqlNode–>SqlNode)
Optimize(Logical Plan):查询计划优化,这里包含了两个阶段
1. 首先将 SqlNode
语法树转换成关系表达式 RelNode
构成的逻辑树(SqlNode–>RelNode/RexNode)
2. 然后使用优化器基于规则进行等价变换,例如我们比较熟悉的谓词下推、列裁剪等,经过优化器优化后得到最优的查询计划(RelNode–>RelNode),先基于calcite rules 去优化logical Plan, 再基于Flink定制的一些优化rules去优化logical Plan
Execute:生成ExecutionPlan,生成物理执行计划(DataStream Plan),提交运行。
Calcite 比较复杂,且扩展性很强,这里介绍了Flink 的几个阶段
1. 入口,环境准备
入口在 TableEnvironment 类,代码示例
EnvironmentSettings settings = EnvironmentSettings
.newInstance()
.inStreamingMode()
.build();
TableEnvironment tableEnv = TableEnvironment.create(settings);
//注册source和sink
tableEnv.executeSql("sourceDDL");
tableEnv.executeSql("sinkDDL");
tableEnv.executeSql("sql语句");
首先是环境准备,包括catalog,resource,fucntion,table等
抽象出了 Planner
接口和 Executor
接口,可以支持多个不同的SQL 执行器,用户可以自行选择希望使用的 Runner。不同的 Runner 只需要正确地实现这两个接口即可
简单聊聊catalog------------------------------------------------------------------------------------------------------
Catalog类
不使用外部catalog 的时候,对应是GenericInMemoryCatalog,就是用这个类记录我们的元数据信息
----------------------------------------------------------------------------------------------------------------------------
2. SQL 解析 部分
planner 有几个实现
batch 和 stream 继承于 PlannerBase parser方法去创建 Parse
Parse 的 parse() 做了
- 先用ExtendedParser解析Calcite不支持的SQL语句
- parser.parse 解析得到 SqlNode
- 使用 SqlNodeToOperationConversion 将 SqlNode 转化为 Operation
SqlNodeToOperationConversion 代码有些多,copy一些核心代码 ,主要就是validate,然后把validate后的SqlNode转为Optional<Operation>
public class SqlNodeToOperationConversion {
private final FlinkPlannerImpl flinkPlanner;
private final CatalogManager catalogManager;
private final SqlCreateTableConverter createTableConverter;
private final AlterSchemaConverter alterSchemaConverter;
public static Optional<Operation> convert(
FlinkPlannerImpl flinkPlanner, CatalogManager catalogManager, SqlNode sqlNode) {
// validate the query
final SqlNode validated = flinkPlanner.validate(sqlNode);
return convertValidatedSqlNode(flinkPlanner, catalogManager, validated);
}
/** Convert a validated sql node to Operation. */
private static Optional<Operation> convertValidatedSqlNode(
FlinkPlannerImpl flinkPlanner, CatalogManager catalogManager, SqlNode validated) {
beforeConversion();
// delegate conversion to the registered converters first
SqlNodeConvertContext context = new SqlNodeConvertContext(flinkPlanner, catalogManager);
Optional<Operation> operation = SqlNodeConverters.convertSqlNode(validated, context);
if (operation.isPresent()) {
return operation;
}
// TODO: all the below conversion logic should be migrated to SqlNodeConverters
SqlNodeToOperationConversion converter =
new SqlNodeToOperationConversion(flinkPlanner, catalogManager);
if (validated instanceof SqlDropCatalog) {
return Optional.of(converter.convertDropCatalog((SqlDropCatalog) validated));
} else if (validated instanceof SqlLoadModule) {
return Optional.of(converter.convertLoadModule((SqlLoadModule) validated));
} else if (validated instanceof SqlShowCatalogs) {
return Optional.of(converter.convertShowCatalogs((SqlShowCatalogs) validated));
......................................................
} else if (validated instanceof SqlShowCurrentCatalog) {
return Optional.of(
converter.convertShowCurrentCatalog((SqlShowCurrentCatalog) validated));
} else if (validated instanceof SqlShowModules) {
return Optional.of(converter.convertShowModules((SqlShowModules) validated));
} else if (validated instanceof SqlUnloadModule) {
return Optional.of(converter.convertUnloadModule((SqlUnloadModule) validated));
} else if (validated instanceof SqlUseCatalog) {
} else {
return Optional.empty();
}
}
3. SQL 转换及优化
SQL 语句解析成 Operation 后,为了得到 Flink 运行时的具体操作算子,需要进一步将 ModifyOperation 转换为 Transformation
核心代码在 PlannerBase 的 translate(),如下
ModifyOperation 对应的是一个 DML 的操作。比如,在将查询结果插入到一张结果表或者转换为 DataStream 时,就会得到 ModifyOperation
上面代码主要做了 4 步
- 1 将 Operation 转换为 RelNode
Operation 其实类似于 SQL 语法树,也构成一个树形结构,并实现了访问者模式,支持使用 QueryOperationVisitor 遍历整棵树,QueryOperationConverter 实现了 QueryOperationVisitor 接口。对于 PlannerQueryOperation,其内部封装的就是已经构建好的 RelNode,直接取出即可;对于其它类型的 Operation,则按需转换为对应的 RelNode
- 2 优化 RelNode
得到 RelNode 后, Calcite 使用 CommonSubGraphBasedOptimizer 优化器(针对流式、批次的优化分别实现了子类StreamCommonSubGraphBasedOptimizer、BatchCommonSubGraphBasedOptimizer)进行 RelNode 的优化流程。优化器将拥有共同子树的 RelNode 看作一个 DAG 结构,并将 DAG 划分成 RelNodeBlock,然后在RelNodeBlock 的基础上进行优化操作。这和正常的 Calcite 处理流程还是保持一致的
CommonSubGraphBasedOptimizer extends Optimizer ,核心代码在 optimize() 和 doOptimize()
abstract class CommonSubGraphBasedOptimizer extends Optimizer {
override def optimize(roots: Seq[RelNode]): Seq[RelNode] = {
//以RelNodeBlock为单位进行优化,在子类(StreamCommonSubGraphBasedOptimizer,BatchCommonSubGraphBasedOptimizer)中实现
val sinkBlocks = doOptimize(roots)
//获得优化后的逻辑计划
val optimizedPlan = sinkBlocks.map { block =>
val plan = block.getOptimizedPlan
require(plan != null)
plan
}
//将 RelNodeBlock 使用的中间表展开
expandIntermediateTableScan(optimizedPlan)
}
Caclite 用一套基于规则的框架优化逻辑计划,用户可以通过添加规则进行扩展,Flink 也是基于自定义规则来实现整个优化过程。优化器主要涉及三个方法doOptimize 、optimizeBlock、optimizeTree
实时和离线分别对应 StreamCommonSubGraphBasedOptimizer 和 BatchCommonSubGraphBasedOptimizer,里面做了优化,具体可以看这两个类的上诉3个方法
经过优化器处理后,得到的逻辑树中的所有节点都是FlinkPhysicalRel ,以待生成物理执行计划了,也就是第三步 translateToExecNodeGraph()
- 3 转换成 ExecNodeGraph
首先要将 FlinkPhysicalRel 构成的 DAG 转换成 ExecNodeGraph ,因为可能存在共用子树的情况,这里还会尝试共用相同的子逻辑计划。由于通常 FlinkPhysicalRel 的具体实现类通常也实现了 ExecNode 接口,所以这一步转换较为简单。
- 4 转换为底层的 Transformation 算子
得到 ExecNodeGraph
后,就可以尝试生成物理执行计划(Transformation
算子),基于算子构建 Flink 的 DAG 然后执行。
4. SQL执行
Executor抽象了SQL执行过程,DefaultExecutor是其默认实现。在得到 Transformation 后,利用 Transformation 生成 Pipeline (也就是StreamGraph),然后就可以提交 Flink 任务执行了
接着退回到executeSql() ,看看 return executeInternal(operation) 做了什么
@Override
public TableResultInternal executeInternal(Operation operation) {
// delegate execution to Operation if it implements ExecutableOperation
if (operation instanceof ExecutableOperation) {
return ((ExecutableOperation) operation).execute(operationCtx);
}
// otherwise, fall back to internal implementation
if (operation instanceof ModifyOperation) {
return executeInternal(Collections.singletonList((ModifyOperation) operation));
} else if (operation instanceof StatementSetOperation) {
return executeInternal(((StatementSetOperation) operation).getOperations());
} else if (operation instanceof ExplainOperation) {
ExplainOperation explainOperation = (ExplainOperation) operation;
ExplainDetail[] explainDetails =
explainOperation.getExplainDetails().stream()
.map(ExplainDetail::valueOf)
.toArray(ExplainDetail[]::new);
Operation child = ((ExplainOperation) operation).getChild();
List<Operation> operations;
if (child instanceof StatementSetOperation) {
operations = new ArrayList<>(((StatementSetOperation) child).getOperations());
} else {
operations = Collections.singletonList(child);
}
String explanation = explainInternal(operations, explainDetails);
return TableResultImpl.builder()
.resultKind(ResultKind.SUCCESS_WITH_CONTENT)
.schema(ResolvedSchema.of(Column.physical("result", DataTypes.STRING())))
.data(Collections.singletonList(Row.of(explanation)))
.build();
} else if (operation instanceof QueryOperation) {
return executeQueryOperation((QueryOperation) operation);
} else if (operation instanceof ExecutePlanOperation) {
ExecutePlanOperation executePlanOperation = (ExecutePlanOperation) operation;
try {
return (TableResultInternal)
executePlan(
PlanReference.fromFile(
resourceManager.registerFileResource(
new ResourceUri(
ResourceType.FILE,
executePlanOperation.getFilePath()))));
} catch (IOException e) {
throw new TableException(
String.format(
"Failed to execute %s statement.", operation.asSummaryString()),
e);
}
} else if (operation instanceof CompilePlanOperation) {
CompilePlanOperation compilePlanOperation = (CompilePlanOperation) operation;
compilePlanAndWrite(
compilePlanOperation.getFilePath(),
compilePlanOperation.isIfNotExists(),
compilePlanOperation.getOperation());
return TableResultImpl.TABLE_RESULT_OK;
} else if (operation instanceof CompileAndExecutePlanOperation) {
CompileAndExecutePlanOperation compileAndExecutePlanOperation =
(CompileAndExecutePlanOperation) operation;
CompiledPlan compiledPlan =
compilePlanAndWrite(
compileAndExecutePlanOperation.getFilePath(),
true,
compileAndExecutePlanOperation.getOperation());
return (TableResultInternal) compiledPlan.execute();
} else if (operation instanceof AnalyzeTableOperation) {
if (isStreamingMode) {
throw new TableException("ANALYZE TABLE is not supported for streaming mode now");
}
try {
return AnalyzeTableUtil.analyzeTable(this, (AnalyzeTableOperation) operation);
} catch (Exception e) {
throw new TableException("Failed to execute ANALYZE TABLE command", e);
}
} else if (operation instanceof NopOperation) {
return TableResultImpl.TABLE_RESULT_OK;
} else {
throw new TableException(UNSUPPORTED_QUERY_IN_EXECUTE_SQL_MSG);
}
}
可以重点关注下几个return,会判断你是什么类型的语句,然后执行