Flink源码阅读(五) FLink SQL之解析流程

3,767 阅读9分钟

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 例如: selectinsert语句都代表着一颗语法解析树,将会对这颗语法解析树进行验证,重写为标准形式(一般自定义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;
  }