Flink temporal table join研究

作者:王东阳

前言

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 的核心概念。与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们。查询动态表将生成一个 连续查询 。一个连续查询永远不会终止,结果会生成一个动态表。查询不断更新其(动态)结果表,以反映其(动态)输入表上的更改。本质上,动态表上的连续查询非常类似于定义物化视图的查询。

动态表可以像普通数据库表一样通过 INSERTUPDATEDELETE 来不断修改。它可能是一个只有一行、不断更新的表,也可能是一个 insert-only 的表,没有 UPDATEDELETE 修改,或者介于两者之间的其他表。

动态表转换

在将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的 Table API 和 SQL 支持三种方式来编码一个动态表的变化:

  • Append-only 流: 仅通过 INSERT 操作修改的动态表可以通过输出插入的行转换为流。
  • Retract 流: retract 流包含两种类型的 message: add messagesretract messages 。通过将INSERT 操作编码为 add message、将 DELETE 操作编码为 retract message、将 UPDATE 操作编码为更新(先前)行的 retract message 和更新(新)行的 add message,将动态表转换为 retract 流。下图显示了将动态表转换为 retract 流的过程。

Dynamic tables

  • Upsert 流: upsert 流包含两种类型的 message: upsert messagesdelete messages。转换为 upsert 流的动态表需要(可能是组合的)唯一键。通过将 INSERTUPDATE 操作编码为 upsert message,将 DELETE 操作编码为 delete message ,将具有唯一键的动态表转换为流。消费流的算子需要知道唯一键的属性,以便正确地应用 message。与 retract 流的主要区别在于 UPDATE 操作是用单个 message 编码的,因此效率更高。下图显示了将动态表转换为 upsert 流的过程。

Dynamic tables

Flink在将动态表转换为 DataStream 时,只支持 append 流和 retract 流。后面的样例代码中会展示转换的API以及Retract 流和Upsert 流的不同。

时态表(Temporal Tables)

  • 时态表(Temporal Table)是一张随时间变化的表, 在 Flink 中称为动态表,时态表中的每条记录都关联了一个或多个时间段,所有的 Flink 表都是时态的(动态的)。也就是说时态表是动态表的特例,时态表一定是动态表,动态表不一定是时态表。

  • 时态表包含表的一个或多个有版本的表快照,时态表可以是一张跟踪所有变更记录的表(例如数据库表的 changelog,包含多个表快照),也可以是物化所有变更之后的表(例如数据库表,只有最新表快照)。

  • Flink 使用主键约束和事件时间来定义一张版本表和版本视图,在后面介绍temporal join的相关样例中会展示这两种。

样例代码

0. 环境初始化

首先初始化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);
  • env.setRuntimeMode(RuntimeExecutionMode.STREAMING); 设置当前运行模式为 STREAMING模式
  • env.setParallelism(1); 设置并行度是1主要是为测试的目的,便于观察join的结果
  • env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); 设置时间属性是 EventTime

1. 首先创建并注册一个普通表 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

2. 声明版本表

在 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 |
+----+-------------------------------+--------------------------------+-------------+-------------------------+

3. 声明版本视图

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 attributeevent 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 |                         Poun
  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值