作者:王东阳
ANSI-SQL 2011 中提出了Temporal 的概念,Oracle,SQLServer,DB2等大的数据库厂商也先后实现了这个标准。Temporal Table记录了历史上任何时间点所有的数据改动,Temporal Table具有普通table的特性,有具体独特的DDL/DML/QUERY语法,时间是其核心属性。历史意味着时间,意味着快照Snapshot。
Apache Flink遵循ANSI-SQL标准,Apache Flink中Temporal Table的概念也源于ANSI-2011的标准语义,但目前的实现在语法层面和ANSI-SQL略有差别,上面看到ANSI-2011中使用FOR SYSTEM_TIME AS OF
的语法,Apache Flink在早期版本中仅仅支持 LATERAL TABLE(TemporalTableFunction)
的语法,当前flinkv14版本中已经支持FOR SYSTEM_TIME AS OF
语法。
由于Flink中基于eventtime 的 temporal table join 基于flink 的watermark机制实现,为了更好的让读者理解,本文首先介绍flink中的 动态表和时序表,时间概念,***Watermark***等相关知识,最后通过详细的代码用例介绍Flink中基于eventtime 的 temporal table join用法。
动态表 是 Flink 的支持流数据的 Table API 和 SQL 的核心概念。与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们。查询动态表将生成一个 连续查询 。一个连续查询永远不会终止,结果会生成一个动态表。查询不断更新其(动态)结果表,以反映其(动态)输入表上的更改。本质上,动态表上的连续查询非常类似于定义物化视图的查询。
动态表可以像普通数据库表一样通过 INSERT
、UPDATE
和 DELETE
来不断修改。它可能是一个只有一行、不断更新的表,也可能是一个 insert-only 的表,没有 UPDATE
和 DELETE
修改,或者介于两者之间的其他表。
在将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的 Table API 和 SQL 支持三种方式来编码一个动态表的变化:
INSERT
操作修改的动态表可以通过输出插入的行转换为流。INSERT
操作编码为 add message、将 DELETE
操作编码为 retract message、将 UPDATE
操作编码为更新(先前)行的 retract message 和更新(新)行的 add message,将动态表转换为 retract 流。下图显示了将动态表转换为 retract 流的过程。INSERT
和 UPDATE
操作编码为 upsert message,将 DELETE
操作编码为 delete message ,将具有唯一键的动态表转换为流。消费流的算子需要知道唯一键的属性,以便正确地应用 message。与 retract 流的主要区别在于 UPDATE
操作是用单个 message 编码的,因此效率更高。下图显示了将动态表转换为 upsert 流的过程。Flink在将动态表转换为 DataStream
时,只支持 append 流和 retract 流。后面的样例代码中会展示转换的API以及Retract 流和Upsert 流的不同。
时态表(Temporal Table)是一张随时间变化的表, 在 Flink 中称为动态表,时态表中的每条记录都关联了一个或多个时间段,所有的 Flink 表都是时态的(动态的)。也就是说时态表是动态表的特例,时态表一定是动态表,动态表不一定是时态表。
时态表包含表的一个或多个有版本的表快照,时态表可以是一张跟踪所有变更记录的表(例如数据库表的 changelog,包含多个表快照),也可以是物化所有变更之后的表(例如数据库表,只有最新表快照)。
Flink 使用主键约束和事件时间来定义一张版本表和版本视图,在后面介绍temporal join的相关样例中会展示这两种。
首先初始化StreamExecutionEnvironment env 和 StreamTableEnvironment tEnv, 如下:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
RatesHistory
DataStream<Row> ratesStream = env.fromElements(
Row.of(LocalDateTime.parse("2021-08-21T09:02:00"), "US Dollar", 102),
Row.of(LocalDateTime.parse("2021-08-21T09:00:00"), "Euro", 114),
Row.of(LocalDateTime.parse("2021-08-21T09:00:00"), "Yen", 1),
Row.of(LocalDateTime.parse("2021-08-21T10:45:00"), "Euro", 116),
Row.of(LocalDateTime.parse("2021-08-21T11:15:00"), "Euro", 119),
Row.of(LocalDateTime.parse("2021-08-21T11:49:00"), "Pounds", 108))
.returns(
Types.ROW_NAMED(
new String[] {
"currency_time", "currency", "rate"},
Types.LOCAL_DATE_TIME, Types.STRING, Types.INT));
Table rateTable = tEnv.fromDataStream(ratesStream, Schema.newBuilder().build());
tEnv.registerTable("RatesHistory", rateTable);
rateTable.printSchema();
tEnv.from("RatesHistory").execute().print();
得到RatesHistory的schema信息以及表中内容:
(
`currency_time` TIMESTAMP(9),
`currency` STRING,
`rate` INT
)
+----+-------------------------------+--------------------------------+-------------+
| op | currency_time | currency | rate |
+----+-------------------------------+--------------------------------+-------------+
| +I | 2021-08-21 09:02:00.000000000 | US Dollar | 102 |
| +I | 2021-08-21 09:00:00.000000000 | Euro | 114 |
| +I | 2021-08-21 09:00:00.000000000 | Yen | 1 |
| +I | 2021-08-21 10:45:00.000000000 | Euro | 116 |
| +I | 2021-08-21 11:15:00.000000000 | Euro | 119 |
| +I | 2021-08-21 11:49:00.000000000 | Pounds | 108 |
+----+-------------------------------+--------------------------------+-------------+
6 rows in set
在 Flink 中,定义了主键约束和事件时间属性的表就是版本表。相比上面的代码,在使用fromDataStream的第二个参数Schema里面,通过columnByExpression
指定事件时间的时间戳(flink中要求必须是 TIMESTAMP(3)
), 通过 primaryKey("currency")
指定 currency 主键约束。
// version table
Table versionedTable = tEnv.fromDataStream(ratesStream, Schema.newBuilder()
.columnByExpression("rowtime", "CAST(currency_time AS TIMESTAMP(3))")
.primaryKey("currency")
.build());
tEnv.registerTable("versionRate", versionedTable);
System.out.println("versioned table get");
versionedTable.printSchema();
tEnv.from("versionRate").execute().print();
打印versionRate的schema信息以及表中内容:
(
`currency_time` TIMESTAMP(9),
`currency` STRING NOT NULL,
`rate` INT,
`rowtime` TIMESTAMP(3) AS CAST(currency_time AS TIMESTAMP(3)),
CONSTRAINT `PK_currency` PRIMARY KEY (`currency`) NOT ENFORCED
)
+----+-------------------------------+--------------------------------+-------------+-------------------------+
| op | currency_time | currency | rate | rowtime |
+----+-------------------------------+--------------------------------+-------------+-------------------------+
| +I | 2021-08-21 09:02:00.000000000 | US Dollar | 102 | 2021-08-21 09:02:00.000 |
| +I | 2021-08-21 09:00:00.000000000 | Euro | 114 | 2021-08-21 09:00:00.000 |
| +I | 2021-08-21 09:00:00.000000000 | Yen | 1 | 2021-08-21 09:00:00.000 |
| +I | 2021-08-21 10:45:00.000000000 | Euro | 116 | 2021-08-21 10:45:00.000 |
| +I | 2021-08-21 11:15:00.000000000 | Euro | 119 | 2021-08-21 11:15:00.000 |
| +I | 2021-08-21 11:49:00.000000000 | Pounds | 108 | 2021-08-21 11:49:00.000 |
+----+-------------------------------+--------------------------------+-------------+-------------------------+
Flink 也支持定义版本视图只要一个视图包含主键和事件时间便是一个版本视图。为了在 RatesHistory
上定义版本表,Flink 支持通过去重查询定义版本视图, 去重查询可以产出一个有序的 changelog 流,去重查询能够推断主键并保留原始数据流的事件时间属性。
// https://nightlies.apache.org/flink/flink-docs-release-1.14/zh/docs/dev/table/concepts/versioned_tables/#%E5%A3%B0%E6%98%8E%E7%89%88%E6%9C%AC%E8%A7%86%E5%9B%BE
Table versionedRateView = tEnv.sqlQuery(
"select currency, rate, currency_time " + // (1) `currency_time` 保留了事件时间
"from ( " +
"select *, " +
"ROW_NUMBER() OVER (PARTITION BY currency " + //(2) `currency` 是去重query的unique key,作为主键
" ORDER BY currency_time DESC) AS rowNum " +
"FROM RatesHistory ) " +
"WHERE rowNum = 1");
tEnv.createTemporaryView("versioned_rates", versionedRateView);
versionedRateView.printSchema();
tEnv.from("versioned_rates").execute().print();
对于去重语法中的相关参数描述如下
Parameter Specification:
ROW_NUMBER()
: 给每一行分配一个从1开始的递增的唯一的序号。PARTITION BY col1[, col2...]
: 指定分区列.ORDER BY time_attr [asc|desc]
:指定排序所基于的列, 必须是 time attribute. 当前Flink支持 processing time attribute 和event time attribute. Ordering by ASC 意味保留最老的那列, ordering by DESC 意味保留最新的那列.WHERE rownum = 1
: rownum = 1
用于 Flink 获取到去重后的数据。行 (1)
保留了事件时间作为视图 versioned_rates
的事件时间,行 (2)
使得视图 versioned_rates
有了主键, 因此视图 versioned_rates
是一个版本视图。
视图中的去重 query 会被 Flink 优化并高效地产出 changelog stream, 产出的 changelog 保留了主键约束和事件时间。
打印schema和versioned_rates表中内容
(
`currency` STRING,
`rate` INT,
`currency_time` TIMESTAMP(9)
)
+----+--------------------------------+-------------+-------------------------------+
| op | currency | rate | currency_time |
+----+--------------------------------+-------------+-------------------------------+
| +I | US Dollar | 102 | 2021-08-21 09:02:00.000000000 |
| +I | Euro | 114 | 2021-08-21 09:00:00.000000000 |
| +I | Yen | 1 | 2021-08-21 09:00:00.000000000 |
| -U | Euro | 114 | 2021-08-21 09:00:00.000000000 |
| +U | Euro | 116 | 2021-08-21 10:45:00.000000000 |
| -U | Euro | 116 | 2021-08-21 10:45:00.000000000 |
| +U | Euro | 119 | 2021-08-21 11:15:00.000000000 |
| +I | Pounds | 108