Flink:table API

目录

Flink API概述

两套方案的区别

两者在编译与执行的区别

两者在优化SQL语句时的区别

如何使用Table API

入门

导入依赖

代码结构

创建TableEnvironment

创建表

从外部关系型数据库中创建表

标识符

查询表

使用table API

使用SQL

Table sink将表写到外部系统

与DataStream和DataSet API集成

将table转换成DataStream或DataSet

Data Types映射Table Schema

Explain


Flink API概述

早期的flink table API十分不完善,功能很少,如果需要使用的话,基本需要二次开发。所以当时各大厂商很少使用,即便有使用的,也都是自成一套体系,导致flink table API一直处于一种尴尬的地位。直到flink 1.09版本以后,阿里贡献出Blink,table API才逐步完善。

flink支持table API可以使用DDL风格进行开发,支持包含select、filter、where等方法,类似spark的DF编程;也可以直接使用sql语法进行开发。

目前Flink Table API还不完善,不是所有操作都支持DDL风格API和SQL语法与DataSet和DataStream相结合的,这在后面两种方案的差别中会提到。但是后续会逐步完善。

两套方案的区别

flink的table API有两套方案,分别是Blink和原生flink table API,用于对代码进行编译与优化。根据官网文档,推荐在生产环境中使用原生的table API而非Blink,因为原生的table API更加稳定。

两者区别:

  1. 批流统一:Blink将批处理作业,视为流式处理的特殊情况。所以,blink不支持表和DataSet之间的转换,批处理作业将不转换为DataSet应用程序,而是跟流处理一样,转换为DataStream程序来处理。
  2. 因为批流统一,Blink planner也不支持BatchTableSource,而使用有界的StreamTableSource代替。
  3. Blink planner只支持全新的目录,不支持已弃用的ExternalCatalog。
  4. 旧planner和Blink planner的FilterableTableSource实现不兼容。旧的planner会把PlannerExpressions下推到filterableTableSource中,而blink planner则会把Expressions下推。
  5. 基于字符串的键值配置选项仅适用于Blink planner。
  6. PlannerConfig在两个planner中的实现不同。
  7. Blink planner会将多个sink优化在一个DAG中(仅在TableEnvironment上受支持,而在StreamTableEnvironment上不受支持)。而旧planner的优化总是将每一个sink放在一个新的DAG中,其中所有DAG彼此独立。
  8. 旧的planner不支持目录统计,而Blink planner支持。

两者在编译与执行的区别

旧方案

根据table API和SQL的输入的是流数据还是批数据,会转换为DataStreamDataSet程序。Flink在内部分两步将sql语句转换为逻辑执行计划:

  1. 优化逻辑计划
  2. 转换为DataStream或DataSet程序

在以下情况下,将转换Table API或SQL转换成DataStreamDataSet

  • 将Table发送到TableSink,即当Table.insertInto()被调用时。
  • 指定SQL更新语句执行,即当TableEnvironment.sqlUpdate()调用。
  • 手动将Table转换为DataStreamDataSet

经过转换,并在调用StreamExecutionEnvironment.execute()或时执行ExecutionEnvironment.execute()后,将以常规DataStream或DataSet程序一样处理Table API或SQL的结果。

Blink方案

无论Table API和SQL的输入是流数据还是批数据,都将转换为DataStream程序。Flink在内部分两步将sql语句转换为逻辑执行计划:

  1. 优化逻辑计划,
  2. 转换为DataStream程序。

将Table API和SQL转换成DataStream时,对于TableEnvironmentStreamTableEnvironment有不同的转换方式

对于TableEnvironment,Table API或SQL在TableEnvironment.execute()被调用时会进行转换,因为TableEnvironment将优化多个multiple-sinks到一个DAG中。

而对于StreamTableEnvironment,在以下情况下转换表API或SQL查询:

  • 将Table发送到TableSink,即当Table.insertInto()被调用时。
  • 指定SQL更新语句执行,即当TableEnvironment.sqlUpdate()调用。
  • 手动将Table转换为DataStream

经过转换,并在调用StreamExecutionEnvironment.execute()或时执行ExecutionEnvironment.execute()后,将以常规DataStream或DataSet程序一样处理Table API或SQL的结果。

两者在优化SQL语句时的区别

旧方案

旧方案利用Apache Calcite来优化和翻译SQL。目前执行的优化包括字段映射、谓词下推、subquery decorrelation(笔者译为子查询分解,是一种避免对 inner 表频繁的做 inner 操作的手段)以及其他类型的优化。旧方案尚未优化join的顺序,而是按照SQL中定义的顺序执行。

可以使用一个CalciteConfig对象,对不同阶段应用的优化方式进行配置。可以通过CalciteConfig.createBuilder()创建该对象,并通过TableEnvironment 的tableEnv.getConfig.setPlannerConfig(calciteConfig)方法将该对象中的配置应用到环境中

Blink

  1. 基于Apache Calcite进行扩展,使之能够进行更复杂的优化操作。例如:
  2. 基于Apache Calcite的subquery decorrelation。
  3. Project 剪枝
  4. Partition 剪枝
  5. 谓词下推
  6. 消除子查询的重复数据,以避免重复计算
  7. 特殊子查询重写,包括两部分:将IN和EXISTS转换成 left semi-joins; 将NOT IN 和 NOT EXISTS 转换成 left anti-join。
  8. 可选的JOIN优化:启用table.optimizer.join-reorder-enabled

注意:目前IN/EXISTS/NOT IN/NOT EXISTS只支持优化子查询中的连接条件。

优化器能够做出智能决策,不仅基于优化方案,还会根据source的数据和每个机器的状态(如io、cpu、网络和内存)进行优化。

高级用户可以通过CalciteConfig对象自定义优化方案。

如何使用Table API

入门

导入依赖

根据使用的编程语言,导入不同的连接器依赖:

<!-- JAVA -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-api-java-bridge_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>
<!-- SCALA -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-api-scala-bridge_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

根据选用的方案(Blink与原生API)导入不同的依赖,官网推荐使用原生API:

<!-- 原生API -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-planner_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>
<!-- Blink  -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-planner-blink_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

其他必要依赖:

<!-- 部分底层代码使用scala实现,务必导入此依赖 -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-common</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

<!-- 如果需要与kafka进行交互,或使用自定义方法,需要导入一下依赖 -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-common</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

代码结构

table API和SQL基于数据流和批的代码结构是一样的,都是创建TableEnvironment,然后通过TableEnvironment获取table,继而对table进行操作。以下是一个大致的代码结构:

// 创建TableEnvironment,根据流数据或批数据创建不同的env
// 具体参考下一节:创建TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// 创建表
tableEnv.connect(...).createTemporaryTable("table1")

tableEnv.connect(...).createTemporaryTable("outputTable")

// 使用API对表进行转换,获取新的表
val tapiResult = tableEnv.from("table1").select(...)
// 使用SQL对表进行转换,获取新的表
val sqlResult  = tableEnv.sqlQuery("SELECT ... FROM table1 ...")

// 将表提交给TableSink,API和SQL的提交方式相同
tapiResult.insertInto("outputTable")

// 执行
tableEnv.execute("scala_job")

创建TableEnvironment

TableEnvironment是table API的核心,它负责:

  • 在内部表目录中注册table
  • 注册表目录
  • 加载 pluggable modules
  • 执行SQL语句
  • 注册自定义方法
  • 将流数据与批数据转换成table
  • 保存ExecutionEnvironment 或StreamExecutionEnvironment,用于将table转换回流数据或批数据

table绑定于TableEnvironment,基于不同的TableEnvironment的table,不能在一起操作,如join、union等。

使用静态方法BatchTableEnvironment.create() 或StreamTableEnvironment.create()创建TableEnvironment。方法参数为

  1. StreamExecutionEnvironment 或ExecutionEnvironment 
  2. TableConfig

创建流环境还是批环境需要根据项目情况选择。如果环境中存在两种方案的jar,则在创建时需注意使用的是哪个jar包中的方法。

以下是使用两种方案创建流/批环境的示例:

// **********************
// FLINK STREAMING QUERY
// **********************
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.EnvironmentSettings
import org.apache.flink.table.api.scala.StreamTableEnvironment

val fsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val fsEnv = StreamExecutionEnvironment.getExecutionEnvironment
val fsTableEnv = StreamTableEnvironment.create(fsEnv, fsSettings)
// or val fsTableEnv = TableEnvironment.create(fsSettings)

// ******************
// FLINK BATCH QUERY
// ******************
import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.table.api.scala.BatchTableEnvironment

val fbEnv = ExecutionEnvironment.getExecutionEnvironment
val fbTableEnv = BatchTableEnvironment.create(fbEnv)

// **********************
// BLINK STREAMING QUERY
// **********************
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.EnvironmentSettings
import org.apache.flink.table.api.scala.StreamTableEnvironment

val bsEnv = StreamExecutionEnvironment.getExecutionEnvironment
val bsSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build()
val bsTableEnv = StreamTableEnvironment.create(bsEnv, bsSettings)
// or val bsTableEnv = TableEnvironment.create(bsSettings)

// ******************
// BLINK BATCH QUERY
// ******************
import org.apache.flink.table.api.{EnvironmentSettings, TableEnvironment}

val bbSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inBatchMode().build()
val bbTableEnv = TableEnvironment.create(bbSettings)

创建表

TableEnvironment 维护了一个k-v结构的目录,用于保存哪些被创建的表的标识。每个标识都包含三个部分:catalog , database  以及object (表名或者视图名)。可以设置当前catalog 和当前database,如果未指定catalog和database ,则会使用当前默认的值。如:

// get a TableEnvironment
val tEnv: TableEnvironment = ...;
tEnv.useCatalog("custom_catalog")
tEnv.useDatabase("custom_database")

视图和表:Tables 分别虚拟的VIEWS(视图)和常规的TABLE,视图可以从一个已存在的表对象中创建,通常是一个查询结果。表则是描绘一个外部的数据,如文件或者数据库等。

临时表与永久表:Table分为临时表与永久表,临时表仅在一次Flink session中有效,永久表则可以在不同的Flink session与集群中有效。永久表需要一个存储目录catalog保存表的元数据信息,类似Hive 的Metastore。一旦永久表被创建,则所有已连接到catalog的Flink Session都可以立即读取到它,重启Flink后也可以继续使用永久表,直到它被删除。

另一方面,临时表总是保存在内存里,且仅存在于创建它的那个Flink Session中。对于其他session是不可见的。在创建临时表时可以指定catalog和database ,但是临时表信息不会保存到catalog和database,所以即使删除了临时表所属的database,临时表也不会被删除。

注意:如果创建了一张临时表的标识符和已存在的永久表相同,则临时表会覆盖永久表(逻辑上的覆盖,不会影响数据),也就是说,对这个表的所有操作都是基于临时表的,而非永久表的。可以利用这个机制进行一些测试。

以下是创建一个视图的代码示例:

// 创建 TableEnvironment
val tableEnv = ... 

// 使用环境对象从外部表中查询,并将结果创建一张表 
val projTable: Table = tableEnv.from("X").select(...)

// 使用表projTable 注册成临时表 "projectedTable"
tableEnv.createTemporaryView("projectedTable", projTable)

从外部关系型数据库中创建表

Flink也可以从外部的关系型数据库中创建表,使用环境对象可以获取一个连接器来连接数据库,这个连接器描述了存储数据的外部系统,如数据库,或者kafka,或其他文件系统等。

tableEnvironment
  .connect(...)
  .withFormat(...)
  .withSchema(...)
  .inAppendMode()
  .createTemporaryTable("MyTable")

标识符

上面说到,标识符包含三个部分:catalog , database  以及object,当设置了当前的catalog和database时,可以省略这两个参数。也可以在设置了当前catalog和database时指定其他的catalog和database。如果表名是关键字或包含特殊字符,可以使用``引起来。如下列代码:

// 创建 TableEnvironment
val tEnv: TableEnvironment = ...;
tEnv.useCatalog("custom_catalog")
tEnv.useDatabase("custom_database")

// 创建表table
val table: Table = ...;

// 在当前默认的catalog 和database 下创建临时视图:exampleView
tableEnv.createTemporaryView("exampleView", table)

// 在当前默认的catalog 下,创建database为other_database的临时视图:exampleView
tableEnv.createTemporaryView("other_database.exampleView", table)

// 在当前默认的catalog 和database 下创建临时视图:'View',View 是关键字,所以要用``引起来
tableEnv.createTemporaryView("`View`", table)

// 在当前默认的catalog 和database 下创建临时视图: 'example.View' ,因为包含特殊符号".",所以要用``引起来
tableEnv.createTemporaryView("`example.View`", table)

// 创建catalog 为other_catalog,database为other_database的临时视图:exampleView
tableEnv.createTemporaryView("other_catalog.other_database.exampleView", table)

查询表

使用table API

Scala和Java可以使用table API进行查询,与SQL不同,table API不是传入一个sql字符串,而是由API一层一层组成的。

Table API底层实际上是根据表的env类别,基于流数据或批数据API实现的。调用table API的方法,实际上会调用DataStream或DataSet的相关操作。table API每次调用,都会返回一个新的table对象,这个对象代表了对原表进行操作后的结果。

以下是使用table API的一个示例:

// 创建TableEnvironment
val tableEnv = ... 

// 创建Orders 表
val orders = tableEnv.from("Orders")

// scala使用table API需要导入隐式转换
import org.apache.flink.api.scala._ 
import org.apache.flink.table.api.scala._

// 对orders进行计算,将结果付给revenue 
val revenue = orders
  .filter('cCountry === "FRANCE")
  .groupBy('cID, 'cName)
  .select('cID, 'cName, 'revenue.sum AS 'revSum)

// 之后可以将表保存到存储系统,后面有例子

// execute query
tableEnv.execute("scala_job")

使用SQL

Flink’s SQL是基于 Apache Calcite的,直接将sql语句定义为一个String即可。

以下代码展示了如何使用sql进行查询,并将结构返回一个table:

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// register Orders table

// compute revenue for all customers from France
val revenue = tableEnv.sqlQuery("""
  |SELECT cID, cName, SUM(revenue) AS revSum
  |FROM Orders
  |WHERE cCountry = 'FRANCE'
  |GROUP BY cID, cName
  """.stripMargin)

// emit or convert Table
// execute query

以下代码展示如何使用sql将查询结果插入到已注册的表中:

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// register "Orders" table
// register "RevenueFrance" output table

// compute revenue for all customers from France and emit to "RevenueFrance"
tableEnv.sqlUpdate("""
  |INSERT INTO RevenueFrance
  |SELECT cID, cName, SUM(revenue) AS revSum
  |FROM Orders
  |WHERE cCountry = 'FRANCE'
  |GROUP BY cID, cName
  """.stripMargin)

// execute query

table API和sql可以混合使用,因为它们返回的都是一个table对象。

Table sink将表写到外部系统

Flink可以使用Table sink将表写到外部系统,table sink是一个通用的接口,它支持将数据写到各种文件系统(CSV, Apache Parquet, Apache Avro)或者各种存储系统(JDBC, Apache HBase, Apache Cassandra, Elasticsearch)或者消息队列( Apache Kafka, RabbitMQ)。

基于批数据的表只能使用BatchTableSink,基于流数据的表可以使用AppendStreamTableSink,或RetractStreamTableSink,或UpsertStreamTableSink。具体可见Table Sources & Sinks

 Table.insertInto(String tableName)方法可以将表插入到另一个已注册的表中。该方法会在catalog中查找对象表是否存在,并验证两张表的schema是否相同。

以下代码展示了如何将表写入到:

// 创建 TableEnvironment
val tableEnv = ... 

// 创建输出的目标表CsvSinkTable的连接
val schema = new Schema()
    .field("a", DataTypes.INT())
    .field("b", DataTypes.STRING())
    .field("c", DataTypes.LONG())

tableEnv.connect(new FileSystem("/path/to/file"))
    .withFormat(new Csv().fieldDelimiter('|').deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("CsvSinkTable")

// 使用api或sql计算出要插入的数据result
val result: Table = ...


// 将result插入到CsvSinkTable
result.insertInto("CsvSinkTable")

// execute the program

与DataStream和DataSet API集成

两套方案都可以和DataStream API集成,也就是说,可以将一个DataStream转换成一个table,也可以将table转换成DataStream。只有原生的原生的方案可以和DataSet API集成。Blink 在基于批数据时,不能与流数据合并处理。 注意:下面关于DataSet的讨论都是对基于批处理的原生方案进行的。

下面是一个DataStream和Table的相互转换的例子:

import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.table.api.scala.StreamTableEnvironment

class Test {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment
    environment.setParallelism(1)

    //创建datastream
    var host = "192.168.68.131"
    var port = 8080
    val textDstream: DataStream[String]  = environment.socketTextStream(host,port,"\n".charAt(0))

    val stream: DataStream[(Long, String)] = textDstream.map(...)

    //创建表执行环境
    val tableEnvironment = StreamTableEnvironment.create(tableEnvironment)


    // 将DataStream转换成临时视图,默认字段名为 "f0", "f1"
    tableEnvironment .createTemporaryView("view", stream)
    
    // 将DataStream转换成临时视图,设置字段名为'field1, 'field2
    tableEnvironment .createTemporaryView("view2", stream, 'field1', 'field2')


    // 将DataStream转换成一张表,默认字段名为 "f0", "f1"
    val table = tableEnvironment.fromDataStream(stream)

    // 将DataStream转换成一张表,设置字段名为'field1, 'field2'
    val table = tableEnvironment.fromDataStream(stream, 'field1', 'field2')

    //将table转换成datastream
    //需导入隐式转换
    import org.apache.flink.api.scala._
    val datastream = tableEnvironment.toAppendStream(table)

    //执行
    tableEnvironment.execute("test")

  }
}

将table转换成DataStream或DataSet

Table可以转换成DataStream或DataSet,也就是说,可以将Table API或SQL的执行结果转换成DataStream或DataSet,然后再用DataStream或DataSet的API进行计算。

当使用Table转换成DataStream或DataSet时,需要为DataStream或DataSet指定数据类型。即将table的每一行转换成指定的数据类型。通常情况下会使用最方便的数据类型:Row。以下是一些可选的数据类型:

  • ROW:table的每一行对应一条数据,每个字段按位置进行转换,支持任意数量的字段,支持字段为null值,但是不是类型安全的。
  • POJO:字段会按照POJO中相应的变量名进行转换(POJO中的变量名必须与Table的字段名相同),支持任意数量的字段,支持字段为null值,是类型安全的。
  • Case Class:字段按照位置进行转换,不支持字段值为null,类型安全。
  • Tuple:字段按照位置进行转换,scala最多支持22个字段,java最多支持25个字段。不支持字段值为null。类型安全。
  • Atomic Type:表只有一个字段,且该字段只包含一个字段(也就是说必须是不能嵌套的数据类型,如IntegerDoubleString),不支持字段值为null,类型安全。

Table作为对DataStream的查询结果,是会被动态更新的,当新的数据到达时,table的内容就会发生改变。也就是说,当将一个动态变化的Table转换成DataStream时,需要指定更新方式。

将Table转换成DataStream时有两种插入方式:

  • Append Mode:当table自身的更新方式只有Insert时,才能使用这种方式,仅追加写入,而不更新原先的数据。
  • Retract Mode: 所有类型的table都可以使用这种方式,它使用一个boolean 类型的flag来标记数据是INSERT 还是DELETE 。

以下是一个table转DataStream的例子:

// 创建基于流处理的TableEnvironment
val tableEnv: StreamTableEnvironment = ... 

// 假设table包含两个字段 (String name, Integer age)
val table: Table = ...

// 将table转换成Append模式的DataStream,数据类型为 Row
val dsRow: DataStream[Row] = tableEnv.toAppendStream[Row](table)

// 将table转换成Append模式的DataStream,数据类型为 Tuple2[String, Int]
val dsTuple: DataStream[(String, Int)] dsTuple = 
  tableEnv.toAppendStream[(String, Int)](table)

// 将table转换成Retract 模式的DataStream,数据类型为 Row
//   假设数据类型为X,那么DataStream的数据类型应该是: DataStream[(Boolean, X)]. 
//   这个boolean类型的字段表示数据修改的类型
//   True :INSERT, false :DELETE.
val retractStream: DataStream[(Boolean, Row)] = tableEnv.toRetractStream[Row](table)

以下是一个table转DataSet的例子:

// 创建基于批处理的TableEnvironment 
val tableEnv = BatchTableEnvironment.create(env)

// 假设table包含两个字段 (String name, Integer age)
val table: Table = ...

// 将table转换成DataSet,数据类型为Row
val dsRow: DataSet[Row] = tableEnv.toDataSet[Row](table)

// 将table转换成DataSet,数据类型为Tuple2[String, Int]
val dsTuple: DataSet[(String, Int)] = tableEnv.toDataSet[(String, Int)](table)

Data Types映射Table Schema

Flink的DataStream 和DataSet 支持多种数据类型。允许使用Tuples ,POJO,Scala的 case classes和Flink的Row等包含嵌套的数据类型,并且可以使用table表达式进行访问,其他的数据类型视为原子类型。下面举例table API是如何将复杂数据结构转换成内部的一行数据,以及DataStream 如何转换成table。

DataStream的数据类型与Table Schema映射有两种方式:基于字段位置或基于字段名。

基于字段位置

基于位置的映射可用于在保持字段顺序的同时为字段提供更有意义的名称。这种映射方式要求数据的字段顺序是固定的,如Tuples 、case classes、ROW。如果是POJO类型的话,则不能使用这种映射方式,必须使用基于字段名的映射方式,这个在下一节会说明。如果使用基于字段位置的映射关系,则不能使用as关键字来定义别名。

当基于字段位置映射数据时,不能指定在DataStream中已存在的字段名,否则API会自动认为是使用基于字段名映射的方式。如果不指定字段名,则字段或复合数据结构中的原子类型数据会使用默认的字段名或f0、f1等。

以下是基于字段位置映射的一个示例:

//创建 TableEnvironment
val tableEnv: StreamTableEnvironment = ... 
val stream: DataStream[(Long, Int)] = ...

// 将DataStream 转换成Table ,使用默认字段名 "_1" and "_2"
val table: Table = tableEnv.fromDataStream(stream)

// 将DataStream 转换成Table,只保留第一个字段,且将字段名命名为 "myLong" 
val table: Table = tableEnv.fromDataStream(stream, 'myLong')

// 将DataStream 转换成Table ,将字段名按顺序定义为 "myLong" 和"myInt"
val table: Table = tableEnv.fromDataStream(stream, 'myLong, 'myInt)

基于字段名

基于字段名的映射方式适用于任意数据类型包括POJO,这是定义table schema最灵活的映射方式。所有的字段都可以根据字段名进行引用,也可以使用as关键字对字段定义别名。字段可以重新排序。如果没有指定字段名。则会使用默认的字段名,原子数据类型则使用f0、f1之类的默认字段名。

以下是基于字段名映射的一个示例:

// 创建 TableEnvironment
val tableEnv: StreamTableEnvironment = ... // see "Create a TableEnvironment" section

val stream: DataStream[(Long, Int)] = ...

// 将DataStream转换成Table,使用默认字段名: "_1" and "_2"
val table: Table = tableEnv.fromDataStream(stream)

// 将DataStream转换成Table,只保留字段 "_2" 
val table: Table = tableEnv.fromDataStream(stream, '_2')

// 将DataStream转换成Table,并且将两个字段交换位置
val table: Table = tableEnv.fromDataStream(stream, '_2', '_1')

// 将DataStream转换成Table,交换字段位置,且为字段定义别名"myInt" "myLong"
val table: Table = tableEnv.fromDataStream(stream, '_2' as 'myInt, '_1' as 'myLong)

以下都是关于不同数据类型在基于字段名进行映射的情况:

原子类型

flink将原语(IntegerDoubleString)或不能再分解的数据类型称之为原子类型。当一个原子类型的DataStream或DataSet转换成只有一个字段的table时,table字段的数据类型会根据原子类型推断出来。并且可以指定字段名。

以下是一个原子类型在基于字段名进行映射的示例:

// 创建 TableEnvironment
val tableEnv: StreamTableEnvironment = ... 

val stream: DataStream[Long] = ...

// 将DataStream 转换成Table ,使用默认字段名 "f0"
val table: Table = tableEnv.fromDataStream(stream)

// 将DataStream 转换成Table ,定义字段名为 "myLong"
val table: Table = tableEnv.fromDataStream(stream, 'myLong')

Tuples 或Case Classes

Flink支持Scala的内置元组,并为Java提供了Flink自己的元组类。两种元组的DataStreams和DataSet都可以转换为表。可以通过提供所有字段的名称来重命名字段(基于字段位置进行映射)。如果未指定任何字段名称,则使用默认字段名称。如果原始字段名(flink元祖:f0f1,...scala元祖:_1_2...)被引用时,API会认为值基于字段名进行映射。基于字段名进行映射时,支持使用as关键字定义别名,支持重新定义字段顺序。以下是一个代码示例:

// 创建 TableEnvironment
val tableEnv: StreamTableEnvironment = ...

val stream: DataStream[(Long, String)] = ...

// 将DataStream 转换成Table 使用默认字段名 '_1', '_2'
val table: Table = tableEnv.fromDataStream(stream)

// 将DataStream 转换成Table ,重命名字段名为 "myLong", "myString" (基于字段位置进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'myLong', 'myString')

//  将DataStream 转换成Table ,将两个字段调换位置 "_2", "_1" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, '_2', '_1')

//  将DataStream 转换成Table ,只保留字段 "_2" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, '_2')

//  将DataStream 转换成Table ,调换字段位置,并重命名字段名为 "myString", "myLong" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, '_2 as myString', '_1 as myLong')

// 定义样例类
case class Person(name: String, age: Int)
val streamCC: DataStream[Person] = ...

//  将DataStream 转换成Table ,使用默认的字段名 'name', 'age'
val table = tableEnv.fromDataStream(streamCC)

//  将DataStream 转换成Table ,重命名字段名为 'myName', 'myAge' (基于字段位置进行映射)
val table = tableEnv.fromDataStream(streamCC, 'myName', 'myAge')

//  将DataStream 转换成Table ,调换字段位置,并重命名字段名为 "myAge", "myName" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'age as myAge', 'name as myName')

POJO(Java和Scala)

当将一个POJO类型的 DataStreamDataSet转换成Table时没有指定字段名,则会使用POJO本身的字段名作为table的字段名。当基于字段名进行映射时需要指定原始名称,并且不能基于字段位置进行映射。可以使用别名as为字段重命名,支持调换字段位置。

// 创建 TableEnvironment
val tableEnv: StreamTableEnvironment = ... 

// Person 是一个包含 "name" 和 "age" 字段的 POJO。
val stream: DataStream[Person] = ...

// 将 DataStream 转换成Table,使用默认的字段名 "age", "name" (字段会按名称排序)
val table: Table = tableEnv.fromDataStream(stream)

// 将 DataStream 转换成Table,并将字段名重定义为 "myAge", "myName" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'age as myAge', 'name as myName')

// 将 DataStream 转换成Table,仅保留字段 "name" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'name')

// 将 DataStream 转换成Table,仅保留name字段,并将其重命名为 "myName" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'name as myName')

ROW

ROW类型支持任意数量的字段,且支持字段为null值,可以通过设置RowTypeInfo 指定字段名,也可以在转换ROW类型的DataStream或DataSet时指定字段名。ROW类型可以通过基于字段位置和基于字段名两种方式进行映射字段。支持为所有字段重新定义字段名(基于字段位置进行映射),也可以指定字段进行保留、排序、重命名(基于字段名进行映射)。

// 创建 TableEnvironment
val tableEnv: StreamTableEnvironment = ... // see "Create a TableEnvironment" section

// ROW 类型的DataStream,RowTypeInfo指定两个字段名 "name" and "age" 
val stream: DataStream[Row] = ...

// 将 DataStream 转换成Table,使用默认字段 "name", "age"
val table: Table = tableEnv.fromDataStream(stream)

// 将 DataStream 转换成Table,并将字段重命名为 "myName", "myAge" (基于字段位置进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'myName', 'myAge')

// 将 DataStream 转换成Table,并将字段重命名为 "myName", "myAge" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'name as myName', 'age as myAge')

// 将 DataStream 转换成Table, 仅保留字段 "name" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'name')

// 将 DataStream 转换成Table, 仅保留字段name,并将其重命名为 "myName" (基于字段名进行映射)
val table: Table = tableEnv.fromDataStream(stream, 'name as myName')

Explain

代码一:

val env = StreamExecutionEnvironment.getExecutionEnvironment
val tEnv = StreamTableEnvironment.create(env)

val table1 = env.fromElements((1, "hello")).toTable(tEnv, 'count', 'word')
val table2 = env.fromElements((1, "hello")).toTable(tEnv, 'count', 'word')
val table = table1
  .where('word'.like("F%"))
  .unionAll(table2)

val explanation: String = tEnv.explain(table)
println(explanation)

执行计划:

== Abstract Syntax Tree ==
LogicalUnion(all=[true])
  LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
    FlinkLogicalDataStreamScan(id=[1], fields=[count, word])
  FlinkLogicalDataStreamScan(id=[2], fields=[count, word])

== Optimized Logical Plan ==
DataStreamUnion(all=[true], union all=[count, word])
  DataStreamCalc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')])
    DataStreamScan(id=[1], fields=[count, word])
  DataStreamScan(id=[2], fields=[count, word])

== 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: (count, word)
		ship_strategy : REBALANCE

		Stage 4 : Operator
			content : where: (LIKE(word, _UTF-16LE'F%')), select: (count, word)
			ship_strategy : FORWARD

			Stage 5 : Operator
				content : from: (count, word)
				ship_strategy : REBALANCE

代码二:

val settings = EnvironmentSettings.newInstance.useBlinkPlanner.inStreamingMode.build
val tEnv = TableEnvironment.create(settings)

val schema = new Schema()
    .field("count", DataTypes.INT())
    .field("word", DataTypes.STRING())

tEnv.connect(new FileSystem("/source/path1"))
    .withFormat(new Csv().deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("MySource1")
tEnv.connect(new FileSystem("/source/path2"))
    .withFormat(new Csv().deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("MySource2")
tEnv.connect(new FileSystem("/sink/path1"))
    .withFormat(new Csv().deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("MySink1")
tEnv.connect(new FileSystem("/sink/path2"))
    .withFormat(new Csv().deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("MySink2")

val table1 = tEnv.from("MySource1").where("LIKE(word, 'F%')")
table1.insertInto("MySink1")

val table2 = table1.unionAll(tEnv.from("MySource2"))
table2.insertInto("MySink2")

val explanation = tEnv.explain(false)
println(explanation)

执行计划:

== Abstract Syntax Tree ==
LogicalSink(name=[MySink1], fields=[count, word])
+- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
   +- LogicalTableScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]])

LogicalSink(name=[MySink2], fields=[count, word])
+- LogicalUnion(all=[true])
   :- LogicalFilter(condition=[LIKE($1, _UTF-16LE'F%')])
   :  +- LogicalTableScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]])
   +- LogicalTableScan(table=[[default_catalog, default_database, MySource2, source: [CsvTableSource(read fields: count, word)]]])

== Optimized Logical Plan ==
Calc(select=[count, word], where=[LIKE(word, _UTF-16LE'F%')], reuse_id=[1])
+- TableSourceScan(table=[[default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])

Sink(name=[MySink1], fields=[count, word])
+- Reused(reference_id=[1])

Sink(name=[MySink2], fields=[count, word])
+- Union(all=[true], union=[count, word])
   :- Reused(reference_id=[1])
   +- TableSourceScan(table=[[default_catalog, default_database, MySource2, source: [CsvTableSource(read fields: count, word)]]], fields=[count, word])

== Physical Execution Plan ==
Stage 1 : Data Source
	content : collect elements with CollectionInputFormat

	Stage 2 : Operator
		content : CsvTableSource(read fields: count, word)
		ship_strategy : REBALANCE

		Stage 3 : Operator
			content : SourceConversion(table:Buffer(default_catalog, default_database, MySource1, source: [CsvTableSource(read fields: count, word)]), fields:(count, word))
			ship_strategy : FORWARD

			Stage 4 : Operator
				content : Calc(where: (word LIKE _UTF-16LE'F%'), select: (count, word))
				ship_strategy : FORWARD

				Stage 5 : Operator
					content : SinkConversionToRow
					ship_strategy : FORWARD

					Stage 6 : Operator
						content : Map
						ship_strategy : FORWARD

Stage 8 : Data Source
	content : collect elements with CollectionInputFormat

	Stage 9 : Operator
		content : CsvTableSource(read fields: count, word)
		ship_strategy : REBALANCE

		Stage 10 : Operator
			content : SourceConversion(table:Buffer(default_catalog, default_database, MySource2, source: [CsvTableSource(read fields: count, word)]), fields:(count, word))
			ship_strategy : FORWARD

			Stage 12 : Operator
				content : SinkConversionToRow
				ship_strategy : FORWARD

				Stage 13 : Operator
					content : Map
					ship_strategy : FORWARD

					Stage 7 : Data Sink
						content : Sink: CsvTableSink(count, word)
						ship_strategy : FORWARD

						Stage 14 : Data Sink
							content : Sink: CsvTableSink(count, word)
							ship_strategy : FORWARD

猜你喜欢

转载自blog.csdn.net/x950913/article/details/106826031