Table API和SQL接口被整合成一个联合API,其主要概念是围绕Table对象进行输入和输出查询操作。另外,由于其和DataStream API 很容易被整合在一起,所以,在开发过程中是可以随意相互转换操作的。
Table API & SQL接口和DataStream API无缝衔接,他们之间可以很容易的相互转换。使用这些接口构建程序需要如下依赖:
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-api-scala-bridge_2.11artifactId>
<version>1.14.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-planner_2.11artifactId>
<version>1.14.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-streaming-scala_2.11artifactId>
<version>1.14.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-commonartifactId>
<version>1.14.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-clients_2.12artifactId>
<version>1.14.4version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-planner_2.11artifactId>
<version>1.14.4version>
<scope>providedscope>
dependency>
常见的Table API and SQL程序结构如下所示。
import org.apache.flink.table.api._
import org.apache.flink.connector.datagen.table.DataGenOptions
// Create a TableEnvironment for batch or streaming execution.
// See the "Create a TableEnvironment" section for details.
val tableEnv = TableEnvironment.create(/*…*/)
// Create a source table 创建数据源表,这里是连接数据源创建了一个临时表SourceTable
tableEnv.createTemporaryTable("SourceTable", TableDescriptor.forConnector("datagen")
.schema(Schema.newBuilder() // 定义表Schema
.column("f0", DataTypes.STRING()) // 只有一个字段,字段名为f0
.build())
.option(DataGenOptions.ROWS_PER_SECOND, 100)
.build())
// Create a sink table (using SQL DDL) 用标准sql创建临时的sink表
tableEnv.executeSql("CREATE TEMPORARY TABLE SinkTable WITH ('connector' = 'blackhole') LIKE SourceTable");
// Create a Table object from a Table API query 创建表对象
val table1 = tableEnv.from("SourceTable");
// Create a Table object from a SQL query 用标准sql的方式创建表对象
val table2 = tableEnv.sqlQuery("SELECT * FROM SourceTable");
// Emit a Table API result Table to a TableSink, same for SQL result 把数据输入sink表
val tableResult = table1.executeInsert("SinkTable");
TableEnvironment是Table API和SQL程序的入口,主要负责:
在内部目录中创建表
创建目录
加载可插拔模块
执行sql查询操作
注册用户自定义函数
DataStream和Table之间的转换
Table对象总是绑定到特定的TableEnvironment,TableEnvironment对象是由TableEnvironment.create()静态方法创建的,所以不可能对来自不同TableEnvironment的表做相关query操作,例如,join,union等。
import org.apache.flink.table.api.{EnvironmentSettings, TableEnvironment}
val settings = EnvironmentSettings
.newInstance()
.inStreamingMode()
//.inBatchMode()
.build()
val tEnv = TableEnvironment.create(settings)
从已经创建的StreamExecutionEnvironment 创建StreamTableEnvironment对象:
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.EnvironmentSettings
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
val env = StreamExecutionEnvironment.getExecutionEnvironment
val tEnv = StreamTableEnvironment.create(env)
TableEnvironment对象维护一个用标识符创建的表目录对象,每个标识符由3部分组成:目录名,数据库名和表名。如果目录或数据库名未指定,将会默认未当前值。
表可以是虚拟的(views)或真是存在的物理表。views可以从已经存在的Table对象创建,通常是Table API 或SQL查询结果。物理表可以是外部数据,例如,文件、数据库表或消息队列。
临时表和单个Flink会话生命周期相关,永久表对所有集群的Flink会话可见。
永久表需要维护其元数据目录,例如,hive元数据。
临时表被存储在内存中,其生命周期也绑定在创建它的Flink会话上,这些表不能被其他的Flink会话访问。他们不依赖于任何一个目录或数据库,但是可以被其中任何一个命名空间创建。临时表不会因为其中的一个数据库被删除而被删除。
可能在创建临时表时,临时表的标识符和永久表的一样,临时表会掩盖永久表,使永久表在临时表存在期间不能被访问。所有的查询都会对临时表进行执行。这可能对测试程序很有用,根据真正数据集创建一个子集临时表,或者其数据是测试数据。一旦验证正确再执行真正的生产表。
在SQL术语中,Table API对象是一个视图(view,虚拟表),它封装了一个逻辑查询计划,可以在一个目录中被创建:
// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section
// table is the result of a simple projection query
val projTable: Table = tableEnv.from("X").select(...) // X是一个已经创建的视图名(虚拟表)
// register the Table projTable as table "projectedTable"
tableEnv.createTemporaryView("projectedTable", projTable) // 根据表对象projTable,创建临时表projectedTable
表对象和关系型数据库中创建的view相似,这个表对象是没有被优化的,但是当其他查询也引用到这个表对象时,会被内联在一起的。如果多个查询都引用同一个注册的表对象,它将分别内联到查询,而且会执行多次,也就是说表对象是不被共享的。
利用连接器声明可以在关系型数据库的基础上建立表对象。连接器描述了存储表数据的外部系统,如:kafka或标准的文件系统。
表对象可以用Table API或转换成SQL DDL创建。
// Using table descriptors
final TableDescriptor sourceDescriptor = TableDescriptor.forConnector("datagen")
.schema(Schema.newBuilder()
.column("f0", DataTypes.STRING())
.build())
.option(DataGenOptions.ROWS_PER_SECOND, 100)
.build();
tableEnv.createTable("SourceTableA", sourceDescriptor);
tableEnv.createTemporaryTable("SourceTableB", sourceDescriptor);
// Using SQL DDL
tableEnv.executeSql("CREATE [TEMPORARY] TABLE MyTable (...) WITH (...)")
表对象总是标识符注册,标识符由三部分组成,目录、数据库名和表名。
用户可以将一个目录和其中的一个数据库设置为“当前目录”和“当前数据库”。在当前目录、数据库下,标识符的前两部分是可选的,这样就会默认未当前目录和数据库。用户可以自己转换当前目录和当前数据库。
标识符符合SQL要求,这意味着它们可以用反引号(')进行转义。
// get a TableEnvironment
val tEnv: TableEnvironment = ...;
tEnv.useCatalog("custom_catalog") // 设定使用的目录
tEnv.useDatabase("custom_database") // 设定使用的数据库
val table: Table = ...;
// register the view named 'exampleView' in the catalog named 'custom_catalog'
// in the database named 'custom_database'
tableEnv.createTemporaryView("exampleView", table) // 未指定目录和数据库,则默认为当前目录和数据库
// register the view named 'exampleView' in the catalog named 'custom_catalog'
// in the database named 'other_database'
tableEnv.createTemporaryView("other_database.exampleView", table) // 只指定了数据库,则目录默认为当前目录
// register the view named 'example.View' in the catalog named 'custom_catalog'
// in the database named 'custom_database'
tableEnv.createTemporaryView("`example.View`", table) // 反引号表明其是一体的,只代表表名,目录及数据库名默认为当前值
// register the view named 'exampleView' in the catalog named 'other_catalog'
// in the database named 'other_database'
tableEnv.createTemporaryView("other_catalog.other_database.exampleView", table) // 直接指定目录和数据库
Table API是一个用于Scala和Java的语言集成查询API。与SQL相反,查询不是指定为string,而是在宿主语言中逐步组合的。
这个API基于Table类,它代表一个表(流或批),并提供了应用关系操作的方法。这些方法返回一个新的Table对象,该对象表示对输入Table应用关系操作的结果。一些关系操作由多个方法调用组成,例如table.groupBy(…).select(),其中groupBy(…)指定表的分组,select(…)表示对表分组的投影。
Table API 文档描述了所有支持流表和批处理表的Table API操作。
下面的例子展示了一个简单的Table API聚合查询:
// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section
// register Orders table
// scan registered Orders table
val orders = tableEnv.from("Orders")
// compute revenue for all customers from France
val revenue = orders
.filter($"cCountry" === "FRANCE")
.groupBy($"cID", $"cName")
.select($"cID", $"cName", $"revenue".sum AS "revSum")
// emit or convert Table
// execute query
注意: Scala Table API使用以美元符号开头的Scala String插值来引用Table的属性。Table API使用Scala隐式,需要导入下面这些隐式转换:
org.apache.flink.table.api._ - for implicit expression conversions
org.apache.flink.api.scala._ and org.apache.flink.table.api.bridge.scala._ if you want to convert from/to DataStream.
Flink的 SQL 集成基于 Apache Calcite ,后者实现了SQL标准。SQL查询被指定为常规字符串。
SQL 文档描述了Flink对流表和批处理表的SQL支持。
下面的示例显示如何指定查询并将结果作为表返回。
// 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
下面的例子是如何将一个更新查询结果写入一个注册的目标表中。
// 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.executeSql("""
|INSERT INTO RevenueFrance
|SELECT cID, cName, SUM(revenue) AS revSum
|FROM Orders
|WHERE cCountry = 'FRANCE'
|GROUP BY cID, cName
""".stripMargin)
Table API & SQL查询可以很容易混用,因为他们都返回表对象:
Table API 查询可以基于一个由SQL查询返回的表对象
可以在Table API查询的结果上定义SQL查询,方法是将结果表注册到TableEnvironment中,并在SQL查询的FROM子句中引用它。
可以通过TableSink将表对象写入目标表。TableSink 是一个通用接口,支持各种文件格式(例如,CSV,Apache Parquet, Apache Avro),存储系统(例如,JDBC,Apache HBase, Apache Cassandra,Elasticsearch), 或消息系统(例如,Apache Kafka, RabbitMQ)。
批数据表仅能写入BatchTableSink,而流数据表需要AppendStreamTableSink,RetractStreamTableSink或UpsertStreamTableSink。
Table.executeInsert(String tableName) 方法将表对象数据写入已经注册的TableSink。该方法通过名称从目录中查询TableSink,确认表对象的Schema是否和TableSink的一致。
下面是一个简单的示例:
// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section
// create an output Table
val schema = Schema.newBuilder()
.column("a", DataTypes.INT())
.column("b", DataTypes.STRING())
.column("c", DataTypes.BIGINT())
.build()
tableEnv.createTemporaryTable("CsvSinkTable", TableDescriptor.forConnector("filesystem")
.schema(schema)
.option("path", "/path/to/file")
.format(FormatDescriptor.forFormat("csv")
.option("field-delimiter", "|")
.build())
.build())
// compute a result Table using Table API operators and/or SQL queries
val result: Table = ...
// emit the result Table to the registered TableSink
result.executeInsert("CsvSinkTable")
表API和SQL查询被转换成DataStream程序,无论它们的输入是流的还是批处理的。查询在内部被表示为逻辑查询计划,并被转换为两个阶段:
优化逻辑计划,
转换为DataStream程序。
这些方法被调用时,Table API & SQL 查询被转化为DataStream程序:
TableEnvironment.executeSql()
Table.executeInsert()
Table.execute()
StatementSet.execute()
Table对象被转换成DataStream,一旦被转换,它就是一个标准DataStream程序,当 StreamExecutionEnvironment.execute() 方法调用时被执行
Apache Flink利用和扩展Apache Calcite来执行复杂的查询优化。这包括一系列基于规则和成本的优化,例如:
Subquery decorrelation based on Apache Calcite
Project pruning
Partition pruning
Filter push-down
Sub-plan deduplication to avoid duplicate computation
Special subquery rewriting, including two parts:
Converts IN and EXISTS into left semi-joins
Converts NOT IN and NOT EXISTS into left anti-join
Optional join reordering
Enabled via table.optimizer.join-reorder-enabled
IN/EXISTS/NOT IN/NOT EXISTS目前只支持在子查询重写的连接条件下使用。
优化器不仅根据计划,还根据来自数据源的丰富统计信息和每个操作符的细粒度成本(如io、cpu、网络和内存)做出智能决策。
高级用户可以通过CalciteConfig对象提供自定义优化,该对象可以通过调用TableEnvironment#getConfig#setPlannerConfig提供给表环境。
Table API提供了一种机制来解释用于计算Table的逻辑和优化的查询计划。这是通过Table.explain()方法或StatementSet.explain()方法完成的。explain()返回一个Table的计划。statement .explain()返回多个接收器的计划。它返回一个描述三个计划的String:
关系查询的抽象语法树,即未经优化的逻辑查询计划,
优化的逻辑查询计划和
物理执行计划。
TableEnvironment.explainSql()和TableEnvironment.executeSql()支持执行一个EXPLAIN