对于像DataFrame
这样的关系型编程接口,因其强大且灵活的表达能力,能够让用户通过非常丰富的接口对数据进行处理,有效降低了用户的使用成本,近年来逐渐成为主流大数据处理框架主要接口形式之一。Flink也提供了关系型编程接口Table API
以及基于TableAPI
的SQL API
,让用户能够通过使用结构化编程接口高效的构建Flink应用。同时Table API
以及SQL
能够统一处理批量和实时计算业务,无需切换到修改任何应用代码就能够基于同一套API编写流式应用和批量应用,从而达到真正意义的批流统一。
Apache Flink
具有两个关系API
- 表API
和SQL
- 用于统一流和批处理。Table API
是Scala
和Java
的语言集成查询API
,允许以非常直观的方式组合来自关系运算符的查询,Table API
和SQL
接口彼此紧密集成,以及Flink
的DataStream
和DataSet API
。您可以轻松地在基于API
构建的所有API和库之间切换。例如,您可以使用CEP
库从DataStream
中提取模式,然后使用Table API
分析模式,或者可以在预处理上运行Gelly
图算法之前使用SQL查询扫描,过滤和聚合批处理表数据。
Blink
将批处理作业视为流式传输的特例。因此,也不支持Table和DataSet
之间的转换,批处理作业不会转换为Dataset
程序,而是转化为DataStream
程序,与流作业相同。Blink planner
不支持BatchTableSource
,使用有界StreamTableSource
而不是它。Blink
规划器仅支持全新Catalog
,不支持ExternalCatalog
已弃用。- 旧计划程序和Blink计划程序的
FilterableTableSource
的实现是不兼容的。 旧计划者将PlannerExpressions
推向FilterableTableSource
,而Blink计划者将推下表达式。- 基于字符串的键值配置选项(有关详细信息,请参阅有关配置的文档)仅用于Blink规划器。
PlannerConfig
在两个规划者中的实现(CalciteConfig
)是不同的。- Blink规划器将多个接收器优化为一个DAG(仅在
TableEnvironment
上支持,而不在StreamTableEnvironment
上支持)。 旧规划器将始终将每个接收器优化为新的DAG,其中所有DAG彼此独立。- 现在,旧规划器不支持目录统计,而Blink规划器则支持。
和DataStream
一样,Table API
和SQL
中具有相同的基本编程模型。首先需要构建对应的TableEnvironment
创建关系型编程环境,才能够在程序中使用Table API
和SQL
来编写程序,另外Table API
和SQL
接口可以在应用中同时使用。Flink SQL
基于Apache Cacite
框架实现SQL
协议,是构建在Table API
之上的更高级接口。
// JAVA
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section
// register a Table
tableEnv.registerTable("table1", ...) // or
tableEnv.registerTableSource("table2", ...); // or
tableEnv.registerExternalCatalog("extCat", ...);
// register an output Table
tableEnv.registerTableSink("outputTable", ...);
// create a Table from a Table API query
Table tapiResult = tableEnv.scan("table1").select(...);
// create a Table from a SQL query
Table sqlResult = tableEnv.sqlQuery("SELECT ... FROM table2 ... ");
// emit a Table API result Table to a TableSink, same for SQL result
tapiResult.insertInto("outputTable");
// execute
tableEnv.execute("java_job");
这TableEnvironment
是Table API
和SQL
集成的核心概念,他主要负责:
Table
内部目录中注册表SQL
查询DataStream
或者DataSet
转换为Table
ExecutionEnvironment
或的引用StreamExecutionEnvironment
表始终绑定到特定的TableEnvironment
。 不可能在同一查询中组合不同TableEnvironments
的表,例如,加入或联合它们。
通过使用StreamExecutionEnvironment
或ExecutionEnvironment
和可选的TableConfig
调用静态BatchTableEnvironment.create()
或StreamTableEnvironment.create()
方法来创建TableEnvironment
。 TableConfig
可用于配置TableEnvironment
或自定义查询优化和转换过程.
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-javaartifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-coreartifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-streaming-java_${scala.binary.version}artifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-api-java-bridge_${scala.binary.version}artifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-planner_${scala.binary.version}artifactId>
<version>${flink.version}version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-commonartifactId>
<version>${flink.version}version>
dependency>
Note 1 : 由于Flink Table接口中引入了Apache Calcite第三方库,会阻止Java虚拟机对用户的Classloaders进行垃圾回收,因此不建议用户在构建Flink应用时将flink table依赖包打包进fat-jar中,可以在集群环境中将{FLINK_HOME}/opt的对应的flink-table jar复制到{FLINK_HOME}/lib中解决此类问题。
Note 2 : If there is only one planner jar in /lib directory, you can use useAnyPlanner (use_any_planner for python) to create specific EnvironmentSettings.
使用Table API & SQL
创建Flink
应用程序,需要在环境中创建TableEnvironment
对象,TableEnvironment
提供了注册内部表,执行Flink SQL
语句、注册自定义函数等功能。
// 批处理环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 获取表操作环境对象
BatchTableEnvironment tableEnvironment = BatchTableEnvironment.create(env);
// 流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 获取表操作环境对象
StreamTableEnvironment tableEnvironment = StreamTableEnvironment.create(env);
在获取TableEnvironment
对象后,可以使用TableEnvironment
提供的方法来注册相应的数据源和数据表信息。所有对数据库和数据表的元数据信息存放在Flink CataLog
内部目录中,其存放了Flink
内部所有与Table
相关的元数据信息,包括表的结构信息,数据源信息等。
Table nameSumDeptid = tableEnvironment.scan("emp").select("name,deptid,email");
// 将 nameSumDeptid 在CataLog中注册成内部表 nameTable
tableEnvironment.registerTable("nameTable" , nameSumDeptid)
在使用Table API
,可以将外部数据源直接注册成Table
数据结构。
// 批处理环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 获取表操作环境对象
BatchTableEnvironment tableEnvironment = BatchTableEnvironment.create(env);
// 创建csv文件类型的TableSource
TableSource csvSource = new CsvTable("/file.csv" , ",");
// 将创建好的TableSource注册到BatchTableEnvironment
tableEnvironment.registerTableSource("CsvTable" , csvSource);
扫描表Orders
,并按照a
字段进行分组聚合,查找a
和b
的数量并将b
的计数取名为cnt
Table orders = tEnv.scan("Orders"); // schema (a, b, c, rowtime)
Table counts = orders
.groupBy("a")
.select("a, b.count as cnt");
// conversion to DataSet
DataSet<Row> result = tEnv.toDataSet(counts, Row.class);
result.print();
下一个示例显示了一个更复杂的Table API程序。程序再次扫描Orders
表格。它过滤空值,规范化a
String类型的字段,并计算每小时和产品a
的平均计费金额b
。
Table orders = tEnv.scan("Orders"); // schema (a, b, c, rowtime)
Table result = orders
.filter("a.isNotNull && b.isNotNull && c.isNotNull")
.select("a.lowerCase() as a, b, rowtime")
.window(Tumble.over("1.hour").on("rowtime").as("hourlyWindow"))
.groupBy("hourlyWindow, a")
.select("a, hourlyWindow.end as hour, b.avg as avgBillingAmount");
Operators | Description |
---|---|
Scan |
与SQL查询中的FROM子句类似。 执行已注册表的扫描.Table orders = tableEnv.scan("Orders"); |
Select | 与SQL SELECT语句类似。 执行选择操作。Table orders = tableEnv.scan("Orders"); Table result = orders.select("a, c as d"); You can use star ( * ) to act as a wild card, selecting all of the columns in the table.Table result = orders.select("*"); |
As | Renames fields.Table orders = tableEnv.scan("Orders"); Table result = orders.as("x, y, z, t"); |
Where / Filter | 与SQL WHERE子句类似。 过滤掉未通过过滤谓词的行.Table orders = tableEnv.scan("Orders"); Table result = orders.where("b === 'red'"); Table orders = tableEnv.scan("Orders"); Table result = orders.filter("a % 2 === 0"); |
Operators | Description |
---|---|
AddColumns | 执行字段添加操作。 如果添加的字段已存在,它将抛出异常。Table orders = tableEnv.scan("Orders"); Table result = orders.addColumns("concat(c, 'sunny')"); |
AddOrReplaceColumns | 执行字段添加操作。 如果添加列名称与现有列名称相同,则将替换现有字段。 此外,如果添加的字段具有重复的字段名称,则使用最后一个字段。Table orders = tableEnv.scan("Orders"); Table result = orders.addOrReplaceColumns("concat(c, 'sunny') as desc"); |
DropColumns | 执行字段放置操作。 字段表达式应该是字段引用表达式,并且只能删除现有字段.Table orders = tableEnv.scan("Orders"); Table result = orders.dropColumns("b, c"); |
RenameColumns | 执行字段重命名操作。 字段表达式应该是别名表达式,并且只能重命名现有字段。Table orders = tableEnv.scan("Orders"); Table result = orders.renameColumns("b as b2, c as c2"); |
Operators | Description |
---|---|
Order By | 与SQL ORDER BY子句类似。 返回跨所有并行分区全局排序的记录。![]() |
Offset & Fetch | 类似于SQL OFFSET和FETCH子句。 偏移和提取限制从排序结果返回的记录数。 Offset和Fetch在技术上是Order By运算符的一部分,因此必须以它为前缀。![]() |
Operators | Description |
---|---|
Insert Into | 类似于SQL查询中的INSERT INTO子句。 执行插入已注册的输出表。![]() |
Group Window
和DataStream API
、DataSet API
中提供的窗口一致,都是将流式数据集根据窗口;类型切分为有界数据集,然后在有界数据集上进行聚合类运算。
tableEnv.scan("Sensors")
.winwods([w : Window] as "window") // 指定窗口类型,并命名为window
.groupBy("window") // 根据窗口进行聚合,窗口数据会分配到单个Task算子中
.select("varl1.sum") // 指定对var字段进行Sum求和
在流式计算中,GroupBy
聚合条件可以以上实例选择Window名称,也可以是一个或多个Key
值与Window
的组合。
Global Window
相似,窗口中的数据都会被汇总到一个Task线程中处理,统计窗口全局的结果;Key
和Window
名称组合,则窗口中的数据分布到并行计算的算子实例中去计算结果。tableEnv.scan("Sensors")
.window([w:Window] as "window") //
.groupBy("window" , "id") // 根据窗口聚合,窗口数据分配到每单个Task算子
.select("id" , "var1.sum") // 指定val字段求和
在select
语句中,我们除了可以获取到数据元素以外,还可以获取到窗口的元数据信息。
tableEnv.scan("Sensors")
.window([w:Window] as "window") //
.groupBy("window" , "id") // 根据窗口聚合,窗口数据分配到每单个Task算子
.select("id" , "var1.sum","window.start","window.end","window.rowtime") // 指定val字段求和
**Note : ** 在以上window()
方法中需要指定的是不同的窗口类型,已确定数据元素被分配到窗口的逻辑。在Table API
中支持Tumble , Sliding , Session Window
三种窗口类型,并分别通过不同的Window
对象来完成定义。
前面提到滚动窗口的窗口长度是固定的,窗口之间的数据不会重合。滚动窗口可以基于Evenet Time
、Process Time
以及Row-Count
来定义。如下实例:Table API
中滚动窗口使用Tumble Class
来创建,且分别基于Evenet Time
、Process Time
以及Row-Count
来定义窗口。
// 通过scan方法在CataLog中查询Sensors表
tableEnv.scan("Sensors")
// Tumbling Event-time Window
.window(Tumble.over("10.minutes").on("rowtime").as("w"));
// Tumbling Processing-time Window (assuming a processing-time attribute "proctime")
.window(Tumble.over("10.minutes").on("proctime").as("w"));
// Tumbling Row-count Window (assuming a processing-time attribute "proctime")
.window(Tumble.over("10.rows").on("proctime").as("w"));
over
: 指定窗口的长度on
: 定义了窗口基于的时间概念类型为EventTime
还是ProcessTime
,EventTime
对应着rowtime
,ProcessTime
对应着proctime
as
: 将创建的窗口重命名,同时窗口名称需要在后续的孙子中使用。滑动窗口的长度也是固定的,但窗口与窗口之间的数据能够重合。滑动窗口可以基于Evenet Time
、Process Time
以及Row-Count
来定义。如下实例:Table API
中滑动窗口使用Slide Class
来创建,且分别基于Evenet Time
、Process Time
以及Row-Count
来定义窗口。
// 通过scan方法在CataLog中查询Sensors表
tableEnv.scan("Sensors")
// Sliding Event-time Window
.window(Slide.over("10.minutes").every("5.minutes").on("rowtime").as("w"));
// Sliding Processing-time window (assuming a processing-time attribute "proctime")
.window(Slide.over("10.minutes").every("5.minutes").on("proctime").as("w"));
// Sliding Row-count window (assuming a processing-time attribute "proctime")
.window(Slide.over("10.rows").every("5.rows").on("proctime").as("w"));
over
: 定义窗口的长度,可以是时间或行计数间隔。every
: 定义滑动间隔,可以是时间间隔也可以是行数。滑动间隔必须与大小间隔的类型相同。on
: 定义了窗口基于的时间概念类型为EventTime
还是ProcessTime
,EventTime
对应着rowtime
,ProcessTime
对应着proctime
as
: 将创建的窗口重命名,同时窗口名称需要在后续的孙子中使用。与Tumbling
、Sliding
窗口不同的是,Session
窗口不需要指定固定的窗口时间,而是通过判断固定时间内数据的活跃性来切分窗口。例如 10 min内数据不接入则切分窗口并触发计算。Session
窗口只能基于EventTime
和ProcessTime
时间概念来定义,通过withGrap
操作符指定数据不活跃的时间Grap
,表示超过该时间数据不接入,则切分窗口并触发计算。
// 通过scan方法在CataLog中查询Sensors表
tableEnv.scan("Sensors")
// Session Event-time Window
.window(Session.withGap("10.minutes").on("rowtime").as("w"));
// Session Processing-time Window (assuming a processing-time attribute "proctime")
.window(Session.withGap("10.minutes").on("proctime").as("w"));
Over Window
和标准SQL
中提供的Over
语法功能类似,也是一种数据聚合计算的方式,但和Group Window
不同的是,Over Window
不需要对输入数据按照窗口大小进行堆叠。Over Window
是基于当前数据和其周围邻近范围内数据进行聚合统计的,例如基于当前记录前面的20条数据,然后基于这些数据统计某一指标的聚合结果。
在Table API
中,Over Window
也是在window方法中指定,但后面不需要和groupBy
操作符绑定,后面直接接SELECT
操作符,并在select
操作符中指定需要查询字段和聚合指标。
Table table = input
.window([OverWindow w].as("w")) // define over window with alias w
.select("a, b.sum over w, c.min over w"); // aggregate over the over window w
方法 | 需要 | 描述 |
---|---|---|
partitionBy |
可 选的 | 定义一个或多个属性上的输入分区。每个分区都是单独排序的,聚合函数分别应用于每个分区。 **注意:**在流式环境中,如果窗口包含partition by子句,则只能并行计算窗口聚合。没有 partitionBy(...) 流由单个非并行任务处理。 |
orderBy |
需要 | 定义每个分区中行的顺序,从而定义聚合函数应用于行的顺序。 **注意:**对于流式查询,这必须是声明的事件时间或处理时间属性。目前,仅支持单个排序属性。 |
preceding |
可选的 | 定义窗口中包含的行的间隔,并在当前行之前。间隔可以指定为时间或行计数间隔。在窗口上限定具有间隔的大小,例如,10.minutes 时间间隔或10.rows 行计数间隔。使用常量(即,UNBOUNDED_RANGE 时间间隔或UNBOUNDED_ROW 行计数间隔)指定在窗口上无界限。在Windows上无限制地从分区的第一行开始。如果preceding 条款被省略,UNBOUNDED_RANGE 并且CURRENT_RANGE 被用作默认preceding 和following 用于该窗口。 |
following |
可选的 | 定义窗口中包含的行的窗口间隔,并跟随当前行。必须在与前一个间隔(时间或行计数)相同的单位中指定间隔。目前,不支持在当前行之后包含行的窗口。相反,您可以指定两个常量之一:CURRENT_ROW 将窗口的上限设置为当前行。CURRENT_RANGE 设置窗口的上限以对当前行的排序键进行排序,即窗口中包含与当前行具有相同排序键的所有行。如果following 省略该子句,则将时间间隔窗口CURRENT_RANGE 的上限定义为,并将行计数间隔窗口的上限定义为CURRENT_ROW 。 |
as |
需要 | 为覆盖窗口指定别名。别名用于引用以下select() 子句中的over window 。 |
Note : 目前,同一select()
调用中的所有聚合函数必须计算相同的窗口。
// Unbounded Event-time over window (assuming an event-time attribute "rowtime")
.window(Over.partitionBy("a").orderBy("rowtime").preceding("unbounded_range").as("w"));
// Unbounded Processing-time over window (assuming a processing-time attribute "proctime")
.window(Over.partitionBy("a").orderBy("proctime").preceding("unbounded_range").as("w"));
// Unbounded Event-time Row-count over window (assuming an event-time attribute "rowtime")
.window(Over.partitionBy("a").orderBy("rowtime").preceding("unbounded_row").as("w"));
// Unbounded Processing-time Row-count over window (assuming a processing-time attribute "proctime")
.window(Over.partitionBy("a").orderBy("proctime").preceding("unbounded_row").as("w"));
// Bounded Event-time over window (assuming an event-time attribute "rowtime")
.window(Over.partitionBy("a").orderBy("rowtime").preceding("1.minutes").as("w"))
// Bounded Processing-time over window (assuming a processing-time attribute "proctime")
.window(Over.partitionBy("a").orderBy("proctime").preceding("1.minutes").as("w"))
// Bounded Event-time Row-count over window (assuming an event-time attribute "rowtime")
.window(Over.partitionBy("a").orderBy("rowtime").preceding("10.rows").as("w"))
// Bounded Processing-time Row-count over window (assuming a processing-time attribute "proctime")
.window(Over.partitionBy("a").orderBy("proctime").preceding("10.rows").as("w"))
The row-based operations generate outputs with multiple columns.
Operators | Describtion |
---|---|
Map | 使用用户定义的标量函数或内置标量函数执行映射操作。如果输出类型是复合类型,则输出将被展平。![]() |
FlatMap | ![]() |
Aggregate | ![]() |
FlatAggregate | ![]() |
Group Window FlatAggregate | ![]() |