Table/SQL模块是flink 为支持数仓开发人员,数据分析的人员的需求,基于calcite定义的一套语义规范。
本文以51信用卡
开源的自定义SQL引擎为叙述主线,从旁解释相关源码
这一部分大致分为两小节描述
- Flink SQL解析流程 (本篇)
- 自研SQL引擎的实现方式 (Flink源码阅读(六) 自研SQL引擎的实现方法 )
对于一次Sql查询,一般经过以下流程:
- 先由
Parser
解析生成SqlNode节点树; - 由
Validator
完成节点类型推导以及必要的表达式验证优化; - 由
SqlToRelConverter
将SqlNode节点树转化为代表逻辑计划的RelNode
; - 在查询优化器
(QueryOptimizer)
里内置了100+的转换规则,分别用于逻辑计划优化以及物理计划转换; - 生成的物理计划
RelNode
被代码实现器遍历处理,各子节点由对应的实现器生成执行代码,最后组装成一个执行Class; Calcite
会为每个SQL
动态生成一个实现了Bindable
接口的Class
,然后动态编译创建实例后传入数据集执行计算。这里的数据集可以通过Schema
进行灵活定义,在业务上针对每一次业务请求会创建一份Schema
,存储当前请求处理过程的所有表数据,处理完成后堆内存后面会被回收。
Flink SQL解析流程
平常我们一般会配置TableEnv来执行SQL相关逻辑
TableEnvironment tableEnv = TableEnvironment.create(settings);`
对TableEnvironment的实现类进行展开 对于Flink基于calcite 重写SQL的逻辑,需要重点关注的是以下四个对象
- TableConfig : 当前{@link TableEnvironment}会话的配置,以调整Table&SQL API程序。
- executor 它可以执行由{@link Planner}生成的{@link Transformation}图。也就是通过执行计划翻译成算子链图
- parser: sql的语法解析器
- planner: sql的执行计划
TableEnvironmentImpl.java
/**
* 调用TableEnv的底层实现类
* @param catalogManager 用于处理目录对象(例如表,视图,函数和类型)的管理器。它封装
* *所有可用的目录并存储临时对象。
* @param moduleManager 负责加载/卸载模块,管理模块的生命周期以及解决模块对象。
* @param tableConfig 当前{@link TableEnvironment}会话的配置,以调整Table&SQL API程序。
* @param executor 它可以执行由{@link Planner}生成的{@link Transformation}图。
* @param functionCatalog 简单的函数目录,用于在目录中存储{@link FunctionDefinition}。
* @param planner 该接口有两个作用:
* * <ul>
* *通过{@link #getParser()}的<li> SQL解析器-将SQL字符串转换为Table API特定的对象
* *例如{@link Operation} s的树</ li>
* * <li>关系计划器-提供一种计划,优化和转换以下内容的树的方法
* * {@link ModifyOperation}变成可运行的形式({@link Transformation})</ li>
* @param isStreamingMode 是否是流SQL
***/**
protected TableEnvironmentImpl(
CatalogManager catalogManager,
ModuleManager moduleManager,
TableConfig tableConfig,
Executor executor,
FunctionCatalog functionCatalog,
Planner planner,
boolean isStreamingMode) {
this.catalogManager = catalogManager;
this.moduleManager = moduleManager;
this.execEnv = executor;
this.tableConfig = tableConfig;
this.functionCatalog = functionCatalog;
this.planner = planner;
this.parser = planner.getParser();
this.operationTreeBuilder = OperationTreeBuilder.create(
tableConfig,
functionCatalog.asLookup(parser::parseIdentifier),
catalogManager.getDataTypeFactory(),
path -> {
try {
UnresolvedIdentifier unresolvedIdentifier = parser.parseIdentifier(path);
Optional<CatalogQueryOperation> catalogQueryOperation = scanInternal(unresolvedIdentifier);
return catalogQueryOperation.map(t -> ApiExpressionUtils.tableRef(path, t));
} catch (SqlParserException ex) {
// The TableLookup is used during resolution of expressions and it actually might not be an
// identifier of a table. It might be a reference to some other object such as column, local
// reference etc. This method should return empty optional in such cases to fallback for other
// identifiers resolution.
return Optional.empty();
}
},
isStreamingMode
);
}
Parser的创建
首先我们需要了解calcite相关概念
freemarker&javacc
- freemarker通常用作前端的模板引擎,简单的说,就是通过#{}和一些列语法添数
- javacc主要用于代码的生成,通过编译 后缀名 .jj的文件,生成我们想要的文件
Flink源码中解析器的模块如下所示
根据语法模板生成解析器代码过程如下:
右侧的代码段为ParserImpl#parse
方法,也是flink 一条sql开始解析的入口
每次调用parse时重新创建parser和planner。这么设计的目的是catalog在每次调用时会变化,所以才动态创建对象。
// 我们在这里使用Supplier模式,以便动态创建对象
// 用户可能会在两者之间的TableConfig中更改解析器配置多个语句解析
private final Supplier<FlinkPlannerImpl> validatorSupplier;
private final Supplier<CalciteParser> calciteParserSupplier;
@Override
public List<Operation> parse(String statement) {
//基于calcite创建语法解析器
CalciteParser parser = calciteParserSupplier.get();
//实现执行计划类,即验证阶段
FlinkPlannerImpl planner = validatorSupplier.get();
// 将sql装换为SQLNode,例如:
//select id, cast(score as int), 'hello' from T where id < ?
// 以上SQL中, 1. id, score, T 等为SqlIdentifier
// 2. cast()为SqlCall
// 3. int 为SqlDataTypeSpec
// 4. 'hello' 为SqlLiteral
// 5. '?' 为SqlDynamicParam
// 这些都可以看作为一个SqlNode
SqlNode parsed = parser.parse(statement);
//根据SqlNode的不同类型 转换成不同的SqlNode,比如 cast()从SqlNode这个父级抽象转换为具体的类型sqlCall
Operation operation = SqlToOperationConverter.convert(planner, catalogManager, parsed)
.orElseThrow(() -> new TableException("Unsupported query: " + statement));
return Collections.singletonList(operation);
}
CalciteParser 语法解析(sql转SqlNode)
/**
* {@link SqlParser}的包装器,用于进行异常转换和{@link SqlNode}强制转换。
*/
public class CalciteParser {
private final SqlParser.Config config;
public CalciteParser(SqlParser.Config config) {
this.config = config;
}
/**
* 将SQL语句解析为{@link SqlNode}。 {@link SqlNode}尚未进入验证阶段(Validator)。
*
* @param 待解析的SQL
* @return 一个转换好的SqlNode节点
* @throws SqlParserException 如果在转换SQL的过程中出了异常
*/
public SqlNode parse(String sql) {
try {
SqlParser parser = SqlParser.create(sql, config);
return parser.parseStmt();
} catch (SqlParseException e) {
throw new SqlParserException("SQL parse failed. " + e.getMessage(), e);
}
}
FlinkPlanner 执行器表达式验证优化 ,SqlNode转逻辑算子
1.对SqlNode中的表达式进行验证
2.根据SqlNode的类型转换每个算子为逻辑算子(即一条sql的语法解析树)
/**
* 将SQL语句解析为{@link SqlNode}。 {@link SqlNode}尚未验证。
*
* @param flinkPlanner FlinkPlannerImpl to convert sql node to rel node
* @param sqlNode SqlNode to execute on
*/
public static Optional<Operation> convert(
FlinkPlannerImpl flinkPlanner,
CatalogManager catalogManager,
SqlNode sqlNode) {
// validate the query
final SqlNode validated = flinkPlanner.validate(sqlNode);
SqlToOperationConverter converter = new SqlToOperationConverter(flinkPlanner, catalogManager);
if (validated instanceof SqlCreateTable) {
return Optional.of(converter.convertCreateTable((SqlCreateTable) validated));
} else if (validated instanceof SqlDropTable) {
return Optional.of(converter.convertDropTable((SqlDropTable) validated));
} else if (validated instanceof SqlAlterTable) {
return Optional.of(converter.convertAlterTable((SqlAlterTable) validated));
} else if (validated instanceof SqlCreateFunction) {
return Optional.of(converter.convertCreateFunction((SqlCreateFunction) validated));
} else if (validated instanceof SqlAlterFunction) {
return Optional.of(converter.convertAlterFunction((SqlAlterFunction) validated));
} else if (validated instanceof SqlDropFunction) {
return Optional.of(converter.convertDropFunction((SqlDropFunction) validated));
} else if (validated instanceof RichSqlInsert) {
SqlNodeList targetColumnList = ((RichSqlInsert) validated).getTargetColumnList();
if (targetColumnList != null && targetColumnList.size() != 0) {
throw new ValidationException("Partial inserts are not supported");
}
return Optional.of(converter.convertSqlInsert((RichSqlInsert) validated));
} else if (validated instanceof SqlUseCatalog) {
return Optional.of(converter.convertUseCatalog((SqlUseCatalog) validated));
} else if (validated instanceof SqlUseDatabase) {
return Optional.of(converter.convertUseDatabase((SqlUseDatabase) validated));
} else if (validated instanceof SqlCreateDatabase) {
return Optional.of(converter.convertCreateDatabase((SqlCreateDatabase) validated));
} else if (validated instanceof SqlDropDatabase) {
return Optional.of(converter.convertDropDatabase((SqlDropDatabase) validated));
} else if (validated instanceof SqlAlterDatabase) {
return Optional.of(converter.convertAlterDatabase((SqlAlterDatabase) validated));
} else if (validated instanceof SqlShowCatalogs) {
return Optional.of(converter.convertShowCatalogs((SqlShowCatalogs) validated));
} else if (validated instanceof SqlShowDatabases) {
return Optional.of(converter.convertShowDatabases((SqlShowDatabases) validated));
} else if (validated instanceof SqlShowTables) {
return Optional.of(converter.convertShowTables((SqlShowTables) validated));
} else if (validated instanceof SqlShowFunctions) {
return Optional.of(converter.convertShowFunctions((SqlShowFunctions) validated));
} else if (validated instanceof SqlCreateView) {
return Optional.of(converter.convertCreateView((SqlCreateView) validated));
} else if (validated instanceof SqlDropView) {
return Optional.of(converter.convertDropView((SqlDropView) validated));
} else if (validated.getKind().belongsTo(SqlKind.QUERY)) {
return Optional.of(converter.convertSqlQuery(validated));
} else {
return Optional.empty();
}
}
SqlNode验证表达式
1.基于访问者模式,递归访问每个SqlCall节点
2.如果是标准DDL,则跳过验证
3.每一种sql ddl 例如: select,insert语句都代表着一颗语法解析树,将会对这颗语法解析树进行验证,重写为标准形式(一般自定义SQL引擎不需要改这里)
FlinkPlannerImpl#validate
def validate(sqlNode: SqlNode): SqlNode = {
val validator = getOrCreateSqlValidator()
validateInternal(sqlNode, validator)
}
private def validateInternal(sqlNode: SqlNode, validator: FlinkCalciteSqlValidator): SqlNode = {
try {
//跟Spark相似,基于访问者模式,递归访问每个SqlCall节点
sqlNode.accept(new PreValidateReWriter(
validator.getCatalogReader.unwrap(classOf[CatalogReader]), typeFactory))
// do extended validation.
sqlNode match {
case node: ExtendedSqlNode =>
node.validate()
case _ =>
}
// 如果是标准的DDL,不需要验证
if (sqlNode.getKind.belongsTo(SqlKind.DDL)
|| sqlNode.getKind == SqlKind.INSERT
|| sqlNode.getKind == SqlKind.CREATE_FUNCTION
|| sqlNode.getKind == SqlKind.DROP_FUNCTION
|| sqlNode.getKind == SqlKind.OTHER_DDL
|| sqlNode.isInstanceOf[SqlShowCatalogs]
|| sqlNode.isInstanceOf[SqlShowDatabases]
|| sqlNode.isInstanceOf[SqlShowTables]
|| sqlNode.isInstanceOf[SqlShowFunctions]) {
return sqlNode
}
//每一种sql ddl 例如: select,insert语句都代表着一颗语法解析树,会对这颗语法解析树进行验证
//将消息表达树重写为标准形式,以便其余的验证逻辑可以更简单。
validator.validate(sqlNode)
}
catch {
case e: RuntimeException =>
throw new ValidationException(s"SQL validation failed. ${e.getMessage}", e)
}
}
那么上述1.基于访问者模式,递归访问SqlCall节点的时候,都在做些什么事情呢?
1.如果在建表后,有插入固定分区的sql操作
2.匹配到相关操作,则将静态分区追加到数据源投影列表。列被附加到相应的位置。
/** [[org.apache.calcite.sql.util.SqlVisitor]]的实现类
* 在做sql node 验证之前重写一些方法 */
class PreValidateReWriter(
val catalogReader: CatalogReader,
val typeFactory: RelDataTypeFactory) extends SqlBasicVisitor[Unit] {
override def visit(call: SqlCall): Unit = {
call match {
//如果在建表后 有例如如下语句
//insert into A partition(a='11', c='22')
// select cast('11' as tpe1), b, cast('22' as tpe2) from B
//则将静态分区追加到数据源投影列表。列被附加到相应的位置。
case r: RichSqlInsert if r.getStaticPartitions.nonEmpty
&& r.getSource.isInstanceOf[SqlSelect] =>
//添加project(投影)字段
appendPartitionProjects(r, catalogReader, typeFactory,
r.getSource.asInstanceOf[SqlSelect], r.getStaticPartitions)
case _ =>
}
}
}
SqlNode转逻辑算子的细节(以SQlCreateTable为例)
SqlToOperationConverter#convertCreateTable
1.判断创建表的SqlNode中主键和唯一键是否存在
2.判断当前创建表结构中是否存在源端事件到达时间
3.设置在创建表节点中的表字段属性
4.根据创建表定义schema
5.创建表时设置分区键
6.根据接入的数据源选择对应的catalog进行源数据管理
7.创建对象的标识符,例如目录中的表,视图,函数或类型。
8.返回表路径的全名,可以填充此名称。使用基于{@code标识符的}长度的当前目录/数据库名称。
9.创建用来描述建表的上下文
/**
* 转换{@link SqlCreateTable} 类型的SqlNode
*/
private Operation convertCreateTable(SqlCreateTable sqlCreateTable) {
// 判断创建表的SqlNode中主键和唯一键的字段是否存在
// primary key and unique keys are not supported
if ((sqlCreateTable.getPrimaryKeyList().size() > 0)
|| (sqlCreateTable.getUniqueKeysList().size() > 0)) {
throw new SqlConversionException("Primary key and unique key are not supported yet.");
}
//判断当前创建表结构中是否存在源端事件到达时间
if (sqlCreateTable.getWatermark().isPresent()) {
throw new SqlConversionException(
"Watermark statement is not supported in Old Planner, please use Blink Planner instead.");
}
// set with properties
//设置在创建表节点中的表字段属性
Map<String, String> properties = new HashMap<>();
sqlCreateTable.getPropertyList().getList().forEach(p ->
properties.put(((SqlTableOption) p).getKeyString(), ((SqlTableOption) p).getValueString()));
//根据创建表语句定义schema
TableSchema tableSchema = createTableSchema(sqlCreateTable);
String tableComment = sqlCreateTable.getComment().map(comment ->
comment.getNlsString().getValue()).orElse(null);
// set partition key
//创建表时设置分区键
List<String> partitionKeys = sqlCreateTable.getPartitionKeyList()
.getList()
.stream()
.map(p -> ((SqlIdentifier) p).getSimple())
.collect(Collectors.toList());
//接入不同数据源的catalog
// https://ci.apache.org/projects/flink/flink-docs-stable/dev/table/catalogs.html
CatalogTable catalogTable = new CatalogTableImpl(tableSchema,
partitionKeys,
properties,
tableComment);
// 对象的标识符,例如目录中的表,视图,函数或类型。该标识符
// 不能直接用于访问目录管理器中的对象,而必须首先被
// 完全解析为{@link ObjectIdentifier}。
UnresolvedIdentifier unresolvedIdentifier = UnresolvedIdentifier.of(sqlCreateTable.fullTableName());
//返回给定表路径的全名,可以填充此名称
//使用基于{@code标识符的}长度的当前目录/数据库名称。
ObjectIdentifier identifier = catalogManager.qualifyIdentifier(unresolvedIdentifier);
//创建用来描述建表的上下文
return new CreateTableOperation(
identifier,
catalogTable,
sqlCreateTable.isIfNotExists());
}
自定义SQL工程中的Table Env配置
原本的Flink工程中TableEnv解析流程
public TableEnv(TableConfig tableConfig) {
try {
this.tableConfig = tableConfig;
//Sql解析器 用来解析配置
SqlParser.Config sqlParserConfig = tableConfig.getSqlParserConfig()
!= null ? tableConfig.getSqlParserConfig() : SqlParser
.configBuilder().setCaseSensitive(false)
.build();
//Sql转换算子
SqlOperatorTable sqlStdOperatorTable = tableConfig
.getSqlOperatorTable()
!= null
? tableConfig.getSqlOperatorTable()
: ChainedSqlOperatorTable.of(SqlStdOperatorTable.instance());
//连接源端的配置
CalciteConnectionConfig calciteConnectionConfig = tableConfig
.getCalciteConnectionConfig()
!= null
? tableConfig.getCalciteConnectionConfig()
: createDefaultConnectionConfig(sqlParserConfig);
//SqlNode转换为RelNode的类型
RelDataTypeSystem typeSystem = tableConfig.getRelDataTypeSystem() != null
? tableConfig.getRelDataTypeSystem()
: calciteConnectionConfig.typeSystem(RelDataTypeSystem.class,
RelDataTypeSystem.DEFAULT);
//SqlNode转换为RelNode
SqlRexConvertletTable convertletTable = tableConfig
.getConvertletTable()
!= null
? tableConfig
.getConvertletTable()
: StandardConvertletTable.INSTANCE;
//可以简化RelNode表达式,为每个表达式写一个文字到列表中
RexExecutor rexExecutor = tableConfig.getRexExecutor() != null
? tableConfig.getRexExecutor()
: RexUtil.EXECUTOR;
this.calciteCatalogReader = new CalciteCatalogReader(
CalciteSchema.from(rootSchema),
CalciteSchema.from(rootSchema).path(null),
new JavaTypeFactoryImpl(typeSystem),
calciteConnectionConfig);
this.frameworkConfig = createFrameworkConfig(sqlParserConfig,
ChainedSqlOperatorTable.of(sqlStdOperatorTable,
calciteCatalogReader), convertletTable,
calciteConnectionConfig, typeSystem, rexExecutor);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
通过上述流程,对应到自定义SQL工程中基于Hive做批流一体TableEnv的路径
工程路径: E:\marble-master\marble-table-hive\src\main\java\org\apache\calcite\adapter\hive\HiveTableEnv.java
public static TableEnv getTableEnv() {
TableConfig tableConfig = new TableConfig();
//Sql转换算子 此处重写SqlOperatorTable类
tableConfig.setSqlOperatorTable(
ChainedSqlOperatorTable.of(HiveSqlOperatorTable.instance(),
SqlStdOperatorTable.instance()));
//语法解析器,此处重写ParserImpl类
tableConfig.setSqlParserConfig(SqlParser
.configBuilder()
.setLex(Lex.JAVA).setCaseSensitive(false).setConformance(
SqlConformanceEnum.HIVE)
.setParserFactory(HiveSqlParserImpl.FACTORY)
.build());
// tableConfig.setRelDataTypeSystem(new HiveTypeSystemImpl());
Properties prop = new Properties();
prop.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(),
String.valueOf(tableConfig.getSqlParserConfig().caseSensitive()));
tableConfig.setCalciteConnectionConfig(
new CalciteConnectionConfigImpl(prop));
//将SqlNode转换为RelNode,此处重写ConvertletTable类
tableConfig.setConvertletTable(new HiveConvertletTable());
//简化RelNode表达式,为每个表达式写一个文字到列表中
RexExecutor rexExecutor = new HiveRexExecutorImpl(
Schemas.createDataContext(null, null));
tableConfig.setRexExecutor(rexExecutor);
//设置新的TableEnv
TableEnv tableEnv = new HiveTableEnv(tableConfig);
//add table functions
tableEnv.addFunction("", "explode",
"org.apache.calcite.adapter.hive.udtf.UDTFExplode", "eval");
return tableEnv;
}