Flink TableAPI&SQL(二)
2.6 表和流的转换
一般用于测试时候的数据输出,针对的是 流数据
。由于Table没有提供print()方法,所有要将Table数据类型转换成DataStream数据类型或者DataSet。
2.6.1 将表(Table)转换成流(DataStream)
- 调用
toDataStream()
方法
Table aggResult = tableEnv.sqlQuery("select user, `url` from clickTable");
tableEnv.toDataStream(aggResult).print("agg");
对于简单的select查询表来说,可以直接调用toDataStream()
方法来进行转换输出。但是在进行一些聚合、组合等操作的时候,需要调用toChangelogStream()
方法,否则会报如下错误:
Table aggResult = tableEnv.sqlQuery("select user, COUNT(url) as cnt from clickTable group by user");
tableEnv.toDataStream(aggResult).print("agg");
这表示当前的TableSink 并不支持表的更新(update)操作。
因为数据是一条一条来的,当前SQL语句所表示的是进行一个累加聚合操作,当第一条数据为(zhangsan,1)的时候,后面又来的一个zhansan
(按user分组聚合),此时zhangsan的数据就变成了(zhangsan,2),这其实是更改了表的数据,因此对于这样有更新操作的表,我们不要试图直接把它转换成 DataStream 打印输出,而是记录一下它的“更新日志”(change log)。这样一来,对于表的所有更新操作,就变成了一条更新日志的流,我们就可以转换成流打印输出了。
- 调用
toChangelogStream()
方法
Table aggResult = tableEnv.sqlQuery("select user, COUNT(url) as cnt from clickTable group by user");
tableEnv.toChangelogStream(aggResult).print("agg");
总结:当对有更新操作的表进行流转换的时候应当调用toChangelogStream()
方法。
2.6.2 将流(DataStream)转换成表(Table)
fromDataStream()
方法
想要将一个DataStream 转换成表也很简单,可以通过调用表环境的 fromDataStream()方法来实现,返回的就是一个 Table 对象。例如,我们可以直接将事件流 eventStream 转换成一个表
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 获取表环境
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 读取数据源
SingleOutputStreamOperator<Event> eventStream = env.addSource(...)
// 将数据流转换成表
Table eventTable = tableEnv.fromDataStream(eventStream);
由于流中的数据本身就是定义好的 POJO
类型 Event,所以我们将流转换成表之后,每一行数据就对应着一个Event,而表中的列名就对应着Event 中的属性。
话虽如此,但有时还会出现字段被重命名为f0...的问题。
因此我们还可以在 fromDataStream()方法中增加参数,用来指定提取哪些属性作为表中的字段名,并可以任意指定位置
// $表示提取对应字段
Table eventTable2 = tableEnv.fromDataStream(eventDataStreamSource, $("timestamp").as("ts"),$("url"));
//如果表中只有一个字段,那么可以直接重命名
tableEnv.fromDataStream(stream, $("myLong"))// 将里面的字段重命名为myLong
// 如果不指定字段,语句会按顺序重命名
Table eventTable = tableEnv.fromDataStream(eventDataStreamSource).as("user","url", "timestamp");
createTemporaryView()
方法
如果我们想要在SQL语句中直接引用该表,那就应该用createTemporaryView()
方法,创建一个临时视图。
tableEnv.createTemporaryView("EventTable", eventDataStreamSource, $("timestamp").as("ts"),$("url"));
传入的两个参数,第一个依然是注册的表名,而第二个可以直接就是DataStream。之后仍旧可以传入多个参数,用来指定表中的字段。
fromChangelogStream()
方法
2.6.3 支持的数据类型
整体来看,DataStream 中支持的数据类型,Table 中也是都支持的,只不过在进行转换时需要注意一些细节。
-
原子类型:基础数据类型(Integer、Double、String)和通用数据类型(就是不可再拆分的数据类型)
-
Tuple类型
当原子类型不做重命名的时候,默认字段名就是f0
,字段还可以通过调用表达式的 as()方法来进行重命名。
StreamTableEnvironment tableEnv = ...; DataStream<Tuple2<Long, Integer>> stream = ...;
// 将数据流转换成只包含 f1 字段的表
Table table = tableEnv.fromDataStream(stream, $("f1"));
// 将数据流转换成包含 f0 和 f1 字段的表,在表中 f0 和 f1 位置交换
Table table = tableEnv.fromDataStream(stream, $("f1"), $("f0"));
// 将 f1 字段命名为 myInt,f0 命名为 myLong
Table table = tableEnv.fromDataStream(stream, $("f1").as("myInt"),
$("f0").as("myLong"));
- POJO类型
POJO也可以理解为Java中的Bean。POJO类型中的字段也可以同样被重新排序、提取和重命名。
StreamTableEnvironment tableEnv = ...; DataStream<Event> stream = ...;
Table table = tableEnv.fromDataStream(stream);
Table table = tableEnv.fromDataStream(stream, $("user"));
Table table = tableEnv.fromDataStream(stream, $("user").as("myUser"),$("url").as("myUrl"));
- Row类型
字段按位置,任意数量的字段映射,支持null
值,无类型安全访问。
?????????
3、 流处理中的表
3.1 动态表和持续查询
- 动态表
当流中有新数据到来,初始的表中会插入一行;而基于这个表定义的 SQL 查询,就应该在之前的基础上更新结果。这样得到的表就会不断地动态变化,被称为“动态表”(Dynamic Tables)。
动态表是Flink 在Table API 和SQL 中的核心概念,它为流数据处理提供了表和SQL 支持。我们所熟悉的表一般用来做批处理,面向的是固定的数据集,可以认为是“静态表”;而动态表则完全不同,它里面的数据会随时间变化。
其实动态表的概念,我们在传统的关系型数据库中已经有所接触。数据库中的表,其实是一系列 INSERT、UPDATE 和 DELETE 语句执行的结果;在关系型数据库中,我们一般把它称为更新日志流(changelog stream)。如果我们保存了表在某一时刻的快照(snapshot),那么接下来只要读取更新日志流,就可以得到表之后的变化过程和最终结果了。在很多高级关系型数据库(比如 Oracle、DB2)中都有“物化视图”(Materialized Views)的概念,可以用来缓存 SQL 查询的结果;它的更新其实就是不停地处理更新日志流的过程。
Flink 中的动态表,就借鉴了物化视图的思想。
- 持续查询
动态表可以像静态的批处理表一样进行查询操作。由于数据在不断变化,因此基于它定义的 SQL 查询也不可能执行一次就得到最终结果。这样一来,我们对动态表的查询也就永远不会停止,一直在随着新数据的到来而继续执行。这样的查询就被称作“持续查询”(Continuous Query)。对动态表定义的查询操作,都是持续查询;而持续查询的结果也会是一个动态表。
由于每次数据到来都会触发查询操作,因此可以认为一次查询面对的数据集,就是当前输入动态表中收到的所有数据。这相当于是对输入动态表做了一个“快照”(snapshot),当作有限数据集进行批处理;流式数据的到来会触发连续不断的快照查询,像动画一样连贯起来,就构成了“持续查询”。
如下图所示:描述了持续查询的过程。这里我们也可以清晰地看到流、动态表和持续查询的关系:
持续查询的步骤如下:
(1) 流(stream)被转换为动态表(dynamic table);
(2) 对动态表进行持续查询(continuous query),生成新的动态表;
(3) 生成的动态表被转换成流。
这样,只要API 将流和动态表的转换封装起来,我们就可以直接在数据流上执行 SQL 查询,用处理表的方式来做流处理了。
package com.peng.dynamic_table;
import com.peng.Event;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 动态表-持续查询
* @date 2022/11/22-19:42
*/
public class DynamicTableDemo01 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// source
DataStreamSource<Event> stream = env.fromElements(
new Event("Alice", "./home", 1000L),
new Event("Bob", "./cart", 1000L),
new Event("Alice", "./prod?id=1", 5 * 1000L),
new Event("Cary", "./home", 60 * 1000L),
new Event("Bob", "./prod?id=3", 90 * 1000L),
new Event("Alice", "./prod?id=7", 105 * 1000L)
);
// 将数据转换成表,数据一条一条的来,表一条一条的插入改变
tableEnv.createTemporaryView("eventTable", stream);
// sql持续查询表的状态,进行聚合统计
Table resultTable = tableEnv.sqlQuery("select user, count(url) as cnt from eventTable group by user");
// 表中涉及到更新,用toChangelogStream()方法
tableEnv.toChangelogStream(resultTable).print();
/**打印结果:
* +I[Alice, 1] --- +I表示插入增加
* +I[Bob, 1]
* -U[Alice, 1] --- -U表示撤回上次的更改 撤回/撤销[Alice, 1]数据
* +U[Alice, 2] --- +U表示更新修改 数据更新为[Alice, 2]
* +I[Cary, 1]
* -U[Bob, 1]
* +U[Bob, 2]
* -U[Alice, 2]
* +U[Alice, 3]
*/
env.execute();
}
}
动态表的变化如下图:
而SQL语句就是截取期间变化的状态,持续查询。
因此一个SQL语句可以引申出追加查询和更新查询。
- 追加查询:没有涉及到聚合累加语句,不会更改原表中已有的数据
- 更新查询:涉及到聚合累加语句,会更改原表中已有的数据
3.2 将动态表转换成流
动态表也可以通过插入(Insert)、更新(Update)和删除(Delete)操作,进行持续的更改。
- 追加流
- 只涉及到插入
- 撤回流
- 涉及到add和retract
- 更新流
- 需指定唯一的key值,涉及到update和delete
4、 时间属性和窗口
基于时间的操作(比如时间窗口),需要定义相关的时间语义和时间数据来源的信息。在 Table API 和 SQL 中,会给表单独提供一个逻辑上的时间字段,专门用来在表处理程序中指示时间。
所以所谓的时间属性(time attributes),其实就是每个表模式结构(schema)的一部分。它可以在创建表的DDL 里直接定义为一个字段,也可以在 DataStream 转换成表时定义。一旦定义了时间属性,它就可以作为一个普通字段引用,并且可以在基于时间的操作中使用。
时间属性的数据类型为 TIMESTAMP
,它的行为类似于常规时间戳,可以直接访问并且进行计算。
4.1 事件时间
时间戳的数据类型为 TIMESTAMP
4.1.1 在创建表的DDL 中定义
在创建表的 DDL(CREATE TABLE 语句)中,可以增加一个字段,通过 WATERMARK
语句来定义事件时间属性。WATERMARK 语句主要用来定义水位线(watermark)的生成表达式,这个表达式会将带有事件时间戳的字段标记为事件时间属性,并在它基础上给出水位线的延迟时间。具体定义方式如下:
CREATE TABLE EventTable(
user STRING,
url STRING,
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
...
);
这里我们把 ts 字段定义为事件时间属性,而且基于 ts 设置了 5 秒的水位线延迟。这里的“5 秒”是以“时间间隔”的形式定义的,格式是INTERVAL <数值> <时间单位>:
INTERVAL '5' SECOND
这里的数值必须用单引号引起来,而单位用 SECOND 和 SECONDS 是等效的。
Flink 中支持的事件时间属性数据类型必须为TIMESTAMP 或者TIMESTAMP_LTZ。这里TIMESTAMP_LTZ 是指带有本地时区信息的时间戳(TIMESTAMP WITH LOCAL TIME ZONE);一般情况下如果数据中的时间戳是“年-月-日-时-分-秒”的形式,那就是不带时区信息的,可以将事件时间属性定义为TIMESTAMP 类型。
而如果原始的时间戳就是一个长整型的毫秒数,这时就需要另外定义一个字段来表示事件时间属性,类型定义为TIMESTAMP_LTZ 会更方便:
CREATE TABLE events (
user STRING,
url STRING,
ts BIGINT,
ts_ltz AS TO_TIMESTAMP_LTZ(ts, 3),//将时间转换成TIMESTAMP类型
WATERMARK FOR ts_ltz AS ts_ltz - INTERVAL '5' SECOND//设置 5 秒的水位线延迟
) WITH (
...
);
这里我们另外定义了一个字段ts_ltz
,是把长整型的 ts 转换为TIMESTAMP_LTZ 得到的;进而使用 WATERMARK 语句将它设为事件时间属性,并设置 5 秒的水位线延迟。
可以简单看下TO_TIMESTAMP_LTZ
转换后的结果:
4.1.2 在数据流转换为表时定义
看实例吧
package com.peng.time;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 在流转表中创建事件时间
* @date 2022/11/23-11:16
*/
public class EventTest02 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// source
DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
// 想在流转表中定义事件时间,就要先指定谁是事件时间
SingleOutputStreamOperator<Event> clickStream = streamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
// '$' 进行字段重命名
Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"),
$("et").rowtime());//声明一个额外的逻辑字段作为事件时间,因为上一步已经指定了事件时间,.rowtime()会自动进行转换
tableEnv.toDataStream(clickTable).print();
env.execute();
}
}
运行结果:
4.2 处理时间
4.2.1 在创建表的DDL 中定义
在创建表的 DDL(CREATE TABLE 语句)中,可以增加一个额外的字段,通过调用系统内置的 PROCTIME()
函数来指定当前的处理时间属性,返回的类型是TIMESTAMP_LTZ。
CREATE TABLE events (
user STRING,
url STRING,
ts BIGINT,
ts_ltz AS PROCTIME()
) WITH (
...
);
声明一个而外字段ts_ltz,用来存储处理时间
4.2.2 在流转表时定义
在 fromDataStream()
方法里定义字段名的时候,添加/指定字段,调用 proctime()
方法,进行一个处理时间的定义。
综合展示:
package com.peng.time;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 在DDL中和流中创建处理时间
* @date 2022/11/23-13:15
*/
public class ProcessTimeTest01 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// TODO 在DDL中创建处理时间
// 调用PROCTIME()函数
String createDDL = "CREATE TABLE clickTable ( " +
" user_name STRING, " +
" url STRING, " +
" ts BIGINT, " +
" ts_ltz AS PROCTIME() " +
" ) WITH (" +
" 'connector' = 'filesystem'," +
" 'path' = 'input/clicks.txt'," +
" 'format' = 'csv'" +
" )";
// TODO 在流转表中创建处理时间
DataStreamSource<Event> stream = env.addSource(new ClickSource());
Table clickTable = tableEnv.fromDataStream(stream, $("user"), $("url"), $("timestamp").as("ts"),
$("ps").proctime());// 这里重新声明了一个字段
tableEnv.toDataStream(clickTable).print();
env.execute();
}
}
结果:
5、 窗口
FlinkTable & SQL中进行开窗有下面两种方法
5.1 分组窗口(Group Window)
在 Flink 1.12 之前的版本中,Table API 和 SQL 提供了一组“分组窗口”(Group Window)函数,常用的时间窗口如滚动窗口、滑动窗口、会话窗口都有对应的实现;具体在 SQL 中就是调用 TUMBLE()、HOP()、SESSION(),传入时间属性字段、窗口大小等参数就可以了。以滚动窗口为例:统计一小时内的用户点击数
Table result = tableEnv.sqlQuery(
"SELECT " +
"user, " +
"TUMBLE_END(ts, INTERVAL '1' HOUR) as endT, " + //TUPMBLE_END()函数获取滚动窗口的结束时间,重命名为 endT 提取出来。
"COUNT(url) AS cnt " +
"FROM EventTable " +
"GROUP BY " + // 使用窗口和用户名进行分组
"user, " +
"TUMBLE(ts, INTERVAL '1' HOUR)" // 定义 1 小时滚动窗口
);
可用看出,分组窗口在 GROUP BY
SQL查询语句中定义。
- 官方文档解释:
组窗口函数 | 描述 |
---|---|
TUMBLE(time_attr, interval) |
定义翻滚时间窗口。翻滚时间窗口将行分配给具有固定持续时间(interval )的非重叠连续窗口。例如,5分钟的翻滚窗口以5分钟为间隔对行进行分组。可以在事件时间(流+批处理)或处理时间(流)上定义翻滚窗口。 |
HOP(time_attr, interval, interval) |
定义跳跃时间窗口(在 Table API中称为滑动窗口)。跳跃时间窗口具有固定的持续时间(第二interval 参数)并且按指定的跳跃间隔(第一interval 参数)跳跃。如果跳跃间隔小于窗口大小,则跳跃窗口重叠。因此,可以将行分配给多个窗口。例如,15分钟大小和5分钟跳跃间隔的跳跃窗口将每行分配给3个不同的15分钟大小的窗口,这些窗口以5分钟的间隔进行评估。可以在事件时间(流+批处理)或处理时间(流)上定义跳跃窗口。 |
SESSION(time_attr, interval) |
定义会话时间窗口。会话时间窗口没有固定的持续时间,但它们的界限由interval 不活动时间定义,即如果在定义的间隙期间没有出现事件,则会话窗口关闭。例如,如果在30分钟不活动后观察到一行,则会开始一个30分钟间隙的会话窗口(否则该行将被添加到现有窗口中),如果在30分钟内未添加任何行,则会关闭。会话窗口可以在事件时间(流+批处理)或处理时间(流)上工作。 |
时间属性
对于流表的SQL查询,time_attr
组窗口函数的参数必须引用指定行的处理时间或事件时间的有效时间属性。
对于批处理表上的SQL,time_attr
组窗口函数的参数必须是类型的属性TIMESTAMP
。
选择组窗口开始和结束时间戳
可以使用以下辅助函数选择组窗口的开始和结束时间戳以及时间属性:
辅助函数 | 描述 |
---|---|
TUMBLE_START(time_attr, interval) |
返回相应的翻滚,跳跃或会话窗口的包含下限的时间戳。 |
HOP_START(time_attr, interval, interval) |
|
SESSION_START(time_attr, interval) |
|
TUMBLE_END(time_attr, interval) |
返回相应的翻滚,跳跃或会话窗口的_独占_上限的时间戳。注意:独占上限时间戳_不能_在后续基于时间的 算子操作中用作行时属性,例如时间窗口连接和组窗口或窗口聚合。 |
HOP_END(time_attr, interval, interval) |
|
SESSION_END(time_attr, interval) |
|
TUMBLE_ROWTIME(time_attr, interval) |
返回相应的翻滚,跳跃或会话窗口的_包含_上限的时间戳。结果属性是rowtime属性,可用于后续基于时间的 算子操作,例[时间窗口连接和组窗口或窗口聚合。 |
HOP_ROWTIME(time_attr, interval, interval) |
|
SESSION_ROWTIME(time_attr, interval) |
|
TUMBLE_PROCTIME(time_attr, interval) |
返回proctime属性,该属性可用于后续基于时间的 算子操作,例如时间窗口连接和组窗口或窗口聚合。 |
HOP_PROCTIME(time_attr, interval, interval) |
|
SESSION_PROCTIME(time_attr, interval) |
_注意:_必须使用与GROUP BY
子句中的组窗口函数完全相同的参数调用辅助函数。
下面的示例演示如何在流表上使用组窗口指定SQL查询。
CREATE TABLE Orders (
user BIGINT,
product STIRNG,
amount INT,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '1' MINUTE
) WITH (...);
SELECT
user,
TUMBLE_START(order_time, INTERVAL '1' DAY) AS wStart,
SUM(amount) FROM Orders
GROUP BY
TUMBLE(order_time, INTERVAL '1' DAY),
user;
5.2 窗口表值函数(Windowing TVFs)
6、 聚合查询
6.1 分组聚合
其实就是编写对应的SQL语句。GROUP BY 分组, 然后调用聚合函数SUM()、MAX()、MIN()、AVG()以及 COUNT()...
6.2 窗口聚合
添加数据:
zhangsan, ./home, 1500
zhangsan, ./appliance, 3500
zhangsan, ./food, 4500
zhangsan, ./cart, 5500
lisi, ./shop, 2000
wangwu, ./school, 25000
zhangsan, ./food, 45000
zhangsan, ./cart, 55000
lisi, ./shop, 20000
wangwu, ./school, 25000
6.2.1 基于分组窗口
代码:
package com.peng.time_window;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO
* @date 2022/11/23-10:04
*/
public class TimeAndWindowTest01 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// DDL中创建事件时间
String createDDL = "CREATE TABLE clickTable ( " +
" user_name STRING, " +
" url STRING, " +
" ts BIGINT, " +
" et AS TO_TIMESTAMP_LTZ(ts, 3), " + // 将时间戳转换成TIMESTAMP类型
" WATERMARK FOR et AS et - INTERVAL '5' SECOND" +
" ) WITH (" +
" 'connector' = 'filesystem'," +
" 'path' = 'input/clicks.txt'," +
" 'format' = 'csv'" +
" )";
tableEnv.executeSql(createDDL);
DataStreamSource<Event> clickStream = env.addSource(new ClickSource());
SingleOutputStreamOperator<Event> eventSingleOutputStreamOperator = clickStream.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
Table clickTable = tableEnv.fromDataStream(eventSingleOutputStreamOperator, $("user"), $("url"), $("timestamp").as("ts"),
$("et").rowtime());
// TODO 分组聚合
Table aggTable = tableEnv.sqlQuery("select user_name, count(url) from clickTable group by user_name");
// TODO 分组窗口聚合
Table groupByWindowTable = tableEnv.sqlQuery("select " +
"user_name, count(url) as cnt, " +
"TUMBLE_END(et, INTERVAL '10' SECOND) AS entT " +
"from clickTable " +
"group by " +
"user_name, " +
"TUMBLE(et, INTERVAL '10' SECOND)"//创建一个10秒的滚动窗口
);
//clickTable.printSchema();//打印模板
tableEnv.toChangelogStream(aggTable).print("agg");
tableEnv.toChangelogStream(groupByWindowTable).print("groupByWindowTable");
env.execute();
}
}
结果:
6.2.2 基于窗口表值函数(Windowing TVFs)
窗口本身返回的是就是一个表,所以窗口会出现在 FROM后面,GROUP BY 后面的则是窗口新增的字段 window_start 和window_end。
Table result = tableEnv.sqlQuery(
"SELECT " +
"user, " +
"window_end AS endT, " +
"COUNT(url) AS cnt " +
"FROM TABLE( " +
"TUMBLE( TABLE EventTable, " + // TABLE 要开窗的表名
"DESCRIPTOR(ts), " + // 选定事件时间
"INTERVAL '1' HOUR)) " + // 设置一小时间隔-每隔一小时滚动一次
"GROUP BY user, window_start, window_end "
);
完整代码:
package com.peng.time_window;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO
* @date 2022/11/23-10:04
*/
public class TimeAndWindowTest01 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
String createDDL = "CREATE TABLE clickTable ( " +
" user_name STRING, " +
" url STRING, " +
" ts BIGINT, " +
" et AS TO_TIMESTAMP_LTZ(ts, 3), " + // 将时间戳转换成TIMESTAMP类型
" WATERMARK FOR et AS et - INTERVAL '5' SECOND" +
" ) WITH (" +
" 'connector' = 'filesystem'," +
" 'path' = 'input/clicks.txt'," +
" 'format' = 'csv'" +
" )";
tableEnv.executeSql(createDDL);
DataStreamSource<Event> clickStream = env.addSource(new ClickSource());
SingleOutputStreamOperator<Event> eventSingleOutputStreamOperator = clickStream.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
Table clickTable = tableEnv.fromDataStream(eventSingleOutputStreamOperator, $("user"), $("url"), $("timestamp").as("ts"),
$("et").rowtime());
// TODO 分组聚合
Table aggTable = tableEnv.sqlQuery("select user_name, count(url) from clickTable group by user_name");
// TODO 分组窗口聚合
Table groupByWindowTable = tableEnv.sqlQuery("select " +
"user_name, count(url) as cnt, " +
"TUMBLE_END(et, INTERVAL '10' SECOND) AS entT " +
"from clickTable " +
"group by " +
"user_name, " +
"TUMBLE(et, INTERVAL '10' SECOND)"
);
// 基于窗口表值函数(Windowing TVFs)的窗口聚合
Table tumbleWindowTable = tableEnv.sqlQuery("select user_name, count(url) as cnt, " +
"window_end as endT " +
"from TABLE( " +
"TUMBLE(TABLE clickTable, DESCRIPTOR(et), INTERVAL '10' SECOND) " +
")" +
"group by user_name, window_end, window_start");
//clickTable.printSchema();//打印模板
tableEnv.toChangelogStream(aggTable).print("agg");
tableEnv.toChangelogStream(groupByWindowTable).print("groupByWindowTable");
tableEnv.toChangelogStream(tumbleWindowTable).print("tumbleWindowTable");
env.execute();
}
}
结果一样:
滑动窗口:
Table hopWindowTable = tableEnv.sqlQuery("select user_name, count(url) as cnt, " +
"window_end as endT " +
"from TABLE( " +
"HOP(TABLE clickTable, DESCRIPTOR(et),INTERVAL '5' SECOND, INTERVAL '10' SECOND) " + // 设置每隔5秒,统计10秒内的数据
")" +
"group by user_name, window_end, window_start");
累计窗口:
// 累计窗口
Table cumulateWindowTable = tableEnv.sqlQuery("select user_name, count(url) as cnt, " +
"window_end as endT " +
"from TABLE( " +
"CUMULATE(TABLE clickTable, DESCRIPTOR(et),INTERVAL '5' SECOND, INTERVAL '10' SECOND) " + // 设置每隔5秒,统计10秒内的数据
")" +
"group by user_name, window_end, window_start");
6.3 开窗(Over)聚合
就是以每一行数据为基准,开一个上下的窗口,有点像滑动窗口。只不过之前的滑动窗口是基于时间的,这个有点像DataStreamAPI里基于数据量的滑动窗口思想。
基本语法:
SELECT
<聚合函数> OVER (
[PARTITION BY <字段 1>[, <字段 2>, ...]]
ORDER BY <时间属性字段>
<开窗范围>),
...
FROM ...
Top N基本语法
在 Flink SQL 中,是通过 OVER
聚合和一个条件筛选来实现 Top N 的。具体来说,是通过将一个特殊的聚合函数 ROW_NUMBER()
应用到OVER 窗口上,统计出每一行排序后的行号, 作为一个字段提取出来;然后再用WHERE 子句筛选行号小于等于N 的那些行返回
Flink官方对Top N进行了专门的优化,使得OVER开窗函数后面的 ORDER BY
不在跟时间属性字段了
SELECT ...
FROM (
SELECT ...,
ROW_NUMBER() OVER (# 先将OVER开窗里的数据进行一个排序,并给每行数据赋予一个行号
[PARTITION BY <字段 1>[, <字段 1>...]]
ORDER BY <排序字段 1> [asc|desc][, <排序字段 2> [asc|desc]...]
) AS row_num
FROM ...)
WHERE row_num <= N [AND <其它条件>]
下面是一个具体的示例,我们统计每个用户的访问事件中,按照字符长度排序的前两个url
SELECT user, url, ts, row_num
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user
ORDER BY CHAR_LENGTH(url) desc
) AS row_num
FROM EventTable)
WHERE row_num <= 2
实例:
package com.peng.top_n;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 实现一个简单的TOPN排序
* @date 2022/11/23-20:03
*/
public class TopNStatic {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
SingleOutputStreamOperator<Event> clickSource = streamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
Table clickTable = tableEnv.fromDataStream(clickSource, $("user").as("user_name"), $("url"), $("timestamp").as("ts"), $("et").rowtime());
tableEnv.createTemporaryView("clickTable", clickTable);
// Top N 统计被访问页面次数的前2名
/*Table TopNUrl = tableEnv.sqlQuery("select user_name, count(url) as cnt, " +
"TUMBLE_END(et, INTERVAL '15' SECOND) AS entT " +
"from clickTable " +
"GROUP BY user_name," +
"TUMBLE(et, INTERVAL '15' SECOND) " +
"ORDER BY cnt DESC " +
"LIMIT 2 "
);*/
// TopN 统计前2名 user 访问次数---针对的是全量
Table topNResultTable = tableEnv.sqlQuery("select user_name, cnt, row_num " +
"from (" +
" select *, ROW_NUMBER() OVER (" +
" ORDER BY cnt DESC" +
" ) AS row_num " +
" from (select user_name, count(url) as cnt from clickTable group by user_name )" +
") where row_num <= 2");
//tableEnv.toChangelogStream(TopNUrl).print("TopN Url: ");
tableEnv.toChangelogStream(topNResultTable).print("top 2: ");
env.execute();
}
}
topNResultTable的结果:
基于窗口的TopN
String subQuery = "select user_name, count(url) as cnt, window_start, window_end " +
"from table (" +
" tumble(table clickTable, descriptor(et), interval '15' second)" +
" )" +
"group by user_name, window_start, window_end";
Table windowTopNResultTable = tableEnv.sqlQuery("select user_name, cnt, row_num " +
"from (" +
" select *, row_number() over (" +
" partition by window_start, window_end " +
" order by cnt desc" +
" ) as row_num " +
" from (" + subQuery + " )" +
") where row_num <= 2");
tableEnv.toChangelogStream(windowTopNResultTable).print("windowTop 2: ");
结果:
此时可以看到,只有 +I
操作,因为窗口随时间滑动,每张表都在不停更新,相当于每张表都在执行插入操作
7、 函数
跟SQL
语言一样,Flink TableAPI & SQL里也有相应的函数。目前TableAPI里的函数是少于SQL里的,因此在实际开发中写的最多的还是SQL。
字符串、字段转大小写
----------------转大写--------------
// SQL里
UPPER(str)
// TableAPI 里
str.upperCase()
----------------转小写--------------
LOWER(str)
str.lowerCase()
7.1 系统函数
1) 标量函数
-
比较函数(=、<>、IS NOT)
-
value1 = value2 判断两个值相等;
-
value1 <> value2 判断两个值不相等
-
value IS NOT NULL 判断value 不为空
-
-
逻辑函数(OR、IS、NOT)
- boolean1 OR boolean2 布尔值boolean1 与布尔值 boolean2 取逻辑或
- boolean IS FALSE 判断布尔值 boolean 是否为 false
- NOT boolean 布尔值 boolean 取逻辑非
-
算术函数(+-*/、POWER()、RAND())
- numeric1 + numeric2 两数相加
- POWER(numeric1, numeric2) 幂运算,取数numeric1 的 numeric2 次方
- RAND() 返回(0.0, 1.0)区间内的一个double 类型的伪随机数
-
字符串函数
- string1 || string2 两个字符串的连接
- UPPER(string) 将字符串 string 转为全部大写
- CHAR_LENGTH(string) 计算字符串 string 的长度
-
时间函数
- DATE string 按格式"yyyy-MM-dd"解析字符串 string,返回类型为 SQL Date
- TIMESTAMP string 按格式"yyyy-MM-dd HH:mm:ss[.SSS]"解析,返回类型为 SQL timestamp
- CURRENT_TIME 返回本地时区的当前时间,类型为 SQL time(与 LOCALTIME等价)
- INTERVAL string range 返回一个时间间隔。string 表示数值;range 可以是DAY, MINUTE,DAT TO HOUR 等单位,也可以是YEAR TO MONTH 这样的复合单位。如“2 年10 个月”可以写成:INTERVAL '2-10' YEAR TO MONTH
2) 聚合函数
在SQL语言里面的聚合函数,Flink SQL里都是支持的。具体可查SQL函数。
例如:
-
COUNT(*) 返回所有行的数量,统计个数
-
SUM([ ALL | DISTINCT ] expression) 对某个字段进行求和操作。默认情况下省略了关键字 ALL,表示对所有行求和;如果指定 DISTINCT,则会对数据进行去重,每个值只叠加一次。
-
RANK() 返回当前值在一组值中的排名
-
ROW_NUMBER() 对一组值排序后,返回当前值的行号。与RANK()的功能相似
其中,RANK()和ROW_NUMBER()一般用在 OVER 窗口中,进行一个TopN排序。
7.2 自定义函数
如果需要自定义函数,就需要用自定义函数(UserDefinedFunction)——UDF。
Flink 的Table API 和SQL 提供了多种自定义函数的接口,以抽象类的形式定义。当前 UDF主要有以下几类:
-
标量函数(Scalar Functions):将输入的标量值转换成一个新的标量值;
-
表函数(Table Functions):将标量值转换成一个或多个新的行数据,也就是扩展成一个表;
-
聚合函数(Aggregate Functions):将多行数据里的标量值转换成一个新的标量值;
-
表聚合函数(Table Aggregate Functions):将多行数据里的标量值转换成一个或多个新的行数据。
7.2.1 整体调用流程
要想在代码中使用自定义的函数,我们需要首先自定义对应UDF 抽象类的实现,并在表环境中注册这个函数,然后就可以在 Table API 和SQL 中调用了。
- 注册函数
注册函数时需要调用表环境的 createTemporarySystemFunction()方法,传入注册的函数名以及UDF 类的Class 对象:
// 注册全局函数
tableEnv.createTemporarySystemFunction("MyFunction", MyFunction.class);
// 注册目录函数
tableEnv.createTemporaryFunction("MyFunction2", MyFunction.class);
我们自定义的 UDF 类叫作 MyFunction,它应该是上面四种 UDF 抽象类中某一个的具体实现;在环境中将它注册为名叫 MyFunction 的函数。
- 使用TableAPI调用函数
在 Table API 中,需要使用 call()方法来调用自定义函数:
tableEnv.from("MyTable").select(call("MyFunction", $("myField")));
// 或者
tableEnv.from("MyTable").select(call(MyFunction.class, $("myField")));
这里 call()方法有两个参数,一个是注册好的函数名 MyFunction
,另一个则是 函数调用时本身的参数
。这里我们定义 MyFunction 在调用时,需要传入的参数是 myField 字段。
- 在SQL中调用函数
当我们将函数注册为系统函数之后,在 SQL 中的调用就与内置系统函数完全一样了:
tableEnv.sqlQuery("SELECT MyFunction(myField) FROM MyTable");
7.2.2 标量函数
Flink对标量函数提供了ScalarFunction
抽象类,从下图可以看到ScalarFunction抽象类继承的是UserDefinedFunction抽象类
目前Flink自定义函数还不完善,所以使用起来又点不方便:
想要实现自定义的标量函数,我们需要自定义一个类来继承抽象类 ScalarFunction,并实现叫作eval()
的求值方法。标量函数的行为就取决于求值方法的定义,它必须是公有的(public
),而且名字必须是 eval
。求值方法 eval 可以重载多次,任何数据类型都可作为求值方法的参数和返回值类型。
这里需要特别说明的是,ScalarFunction 抽象类中并没有定义 eval()方法,所以我们不能直接在代码中重写(override);但 Table API 的框架底层又要求了求值方法必须名字为 eval()。
下面来实践下:
package com.peng.defined_function;
import com.peng.top_n.ClickSource;
import com.peng.top_n.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 自定义标量函数
* @date 2022/11/25-10:28
*/
public class DUFTest_ScalarFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
SingleOutputStreamOperator<Event> clickSource = streamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
Table clickTable = tableEnv.fromDataStream(clickSource, $("user").as("user_name"), $("url"), $("timestamp").as("ts"), $("et").rowtime());
tableEnv.createTemporaryView("clickTable", clickTable);
// TODO 2. 注册自定义函数
tableEnv.createTemporarySystemFunction("MyHash", MyHashFunction.class);
// TODO 3. 调用UDF进行查询转换
Table resultTable = tableEnv.sqlQuery("select user_name, MyHash(user_name) from clickTable");
// TODO 4. 转换成流打印输出
tableEnv.toDataStream(resultTable).print();
env.execute();
}
// 自定义标量函数-->求Hash值
public static class MyHashFunction extends ScalarFunction{
public int eval(String str){
return str.hashCode();
}
/*
// 接受任意类型输入,返回 INT 型输出
public int eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o) {
return o.hashCode();
}*/
}
}
结果:
7.2.3 表函数
package com.peng.defined_function;
import com.peng.top_n.ClickSource;
import com.peng.top_n.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.TableFunction;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 自定义表函数
* @date 2022/11/25-11:08
*/
public class DUFTest_TableFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
SingleOutputStreamOperator<Event> clickSource = streamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<com.peng.top_n.Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
Table clickTable = tableEnv.fromDataStream(clickSource, $("user").as("user_name"), $("url"), $("timestamp").as("ts"), $("et").rowtime());
tableEnv.createTemporaryView("clickTable", clickTable);
// TODO 2. 注册自定义函数
tableEnv.createTemporarySystemFunction("MyTableFunction", MyTableFunction.class);
// TODO 3. 调用UDF进行查询转换
Table resultTable = tableEnv.sqlQuery("select url, world, length " +
"from clickTable, LATERAL TABLE(MyTableFunction(user_name)) AS T(world, length)");// 注意:重命名时不能跟原表重名
// TODO 4. 转换成流打印输出
tableEnv.toDataStream(resultTable).print();
env.execute();
}
// 自定义表函数-->统计字段的长度
public static class MyTableFunction extends TableFunction<Tuple2<String, Integer>>{
//该方法自己书写,类型必须是public,方法名必须是eval
public void eval(String str){
collect(Tuple2.of(str, str.length()));//表函数是通过collect()方法进行输出的
}
}
}
结果:
7.2.4 聚合函数
package com.peng.defined_function;
import com.peng.top_n.ClickSource;
import com.peng.top_n.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.AggregateFunction;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
/**
* @author 海绵先生
* @Description TODO 自定义聚合函数
* @date 2022/11/25-14:21
*/
public class UDFTest_AggFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
SingleOutputStreamOperator<Event> clickSource = streamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<com.peng.top_n.Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.getTimestamp();
}
}));
Table clickTable = tableEnv.fromDataStream(clickSource, $("user").as("user_name"), $("url"), $("timestamp").as("ts"), $("et").rowtime());
tableEnv.createTemporaryView("clickTable", clickTable);
// TODO 2. 注册自定义函数
tableEnv.createTemporarySystemFunction("MyAggFunction", MyAggFunction.class);
// TODO 3. 调用UDF进行查询转换
Table resultTable = tableEnv.sqlQuery("select user_name, MyAggFunction(ts, 1) as w_avg " + //MyAggFunction(ts, 1)第二个参数传的是1
"from clickTable group by user_name");
// TODO 4. 转换成流打印输出
tableEnv.toChangelogStream(resultTable).print();
env.execute();
}
// 单独定义个累加器类型
public static class WeightedAvgAccumulator{
public long sum = 0;
public int count = 0;
}
// 实现自定义聚合函数-->计算加权平均值
public static class MyAggFunction extends AggregateFunction<Long,WeightedAvgAccumulator>{//AggregateFunction<IN, ACC>
@Override
public Long getValue(WeightedAvgAccumulator accumulator) {
if (accumulator.count ==0)
return null;
else
return accumulator.sum / accumulator.count;
}
@Override
public WeightedAvgAccumulator createAccumulator() {
return new WeightedAvgAccumulator();
}
// 实现累加计算方法,该方法必须时public属性,方法名必须为accumulate
public void accumulate(WeightedAvgAccumulator accumulator, Long iValue, Integer iWeight){
accumulator.sum += iValue + iWeight;
accumulator.count += iWeight;
}
}
}