本文翻译自官网:Flink Table Api & SQL 动态表 https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/table/streaming/dynamic_tables.html

SQL和关系代数在设计时并未考虑流数据。所以,关系代数(和SQL)与流处理之间在概念上有一些差距。

本页讨论了这些差异,并说明了Flink如何在无界数据上实现与常规数据库引擎在有界数据上相同的语义。

数据流上的关系查询

动态表和连续查询

在流上定义表

  • 连续查询

  • 更新和追加查询

  • 查询限制

表到流的转换

数据流上的关系查询

下表针对输入数据、执行和输出结果,比较了传统的关系代数和流处理之间的差异。

尽管存在这些差异,使用关系查询和SQL处理流并不是不可能的。先进的关系数据库系统提供了称为“ 物化视图”的功能(将视图的结果作为表存储起来,并定时更新,Oracle 有,百度 Oracle 物化视图)。物化视图被定义为SQL查询,就像常规虚拟视图一样。与虚拟视图相反,物化视图缓存查询结果,以便在访问视图时无需评估查询。缓存的一个常见挑战是防止缓存提供过时的结果。修改其定义查询的基表时,物化视图将过时。 Eager View Maintenance 是一种在其基表更新后立即更新物化视图的技术。

如果考虑以下因素,那么Eager View Maintenance 和对流进行 SQL查询之间的联系将变得显而易见:

数据库表是一个结果流的INSERT,UPDATE和DELETE DML语句,通常被称为更新日志流。

物化视图定义为SQL查询。为了更新视图,查询会连续处理视图基本关系的变更日志流。

实例化视图是流式SQL查询的结果。

考虑到这些要点,我们将在下一节介绍动态表的以下概念。

动态表和持续查询

动态表是 Flink 的 Table API 和 SQL 对流数据支持的核心概念。与代表批处理数据的静态表相反,动态表随时间而变化。可以像静态批处理表一样查询它们。查询动态表会产生一个持续查询。持续查询永远不会终止并产生动态表作为结果。查询不断更新其(动态)结果表以反映其(动态)输入表上的更改。本质上,对动态表的持续查询与定义物化视图的查询非常相似。

重要的是要注意,持续查询的结果在语义上始终等效于在输入表的快照上以批处理方式执行的同一查询的结果。

下图显示了流,动态表和持续查询之间的关系:

1.流将转换为动态表。

2.在动态表上评估持续查询,生成新的动态表。

3.生成的动态表将转换回流。

注意:动态表首先是一个逻辑概念。在查询执行过程中不一定(完全)实现动态表。

在下文中,我们将通过具有以下模式的单击事件流来解释动态表和持续查询的概念:


[
  user:  VARCHAR,   // the name of the user
  cTime: TIMESTAMP, // the time when the URL was accessed
  url:   VARCHAR    // the URL that was accessed by the user
]

在流上定义表

为了使用关系查询处理流,必须将其转换为Table。从概念上讲,流的每个记录都被解释为INSERT对结果表的修改。本质上,我们是从仅 INSERT的 changelog 流中构建表。

下图可视化了点击事件流(左侧)如何转换为表格(右侧)。随着插入更多点击流记录,结果表将持续增长。

注意:在流上定义的表在内部未实现。

持续查询


在动态表上评估持续查询,并生成一个新的动态表作为结果。与批处理查询相反,持续查询永远不会终止并根据其输入表的更新来更新其结果表。在任何时间点,持续查询的结果在语义上均等同于在输入表的快照上以批处理模式执行同一查询的结果。

在下面的示例中,我们显示了对单击事件流中定义的表的两个示例查询。

第一个查询是一个简单的GROUP-BY COUNT 聚合查询。它将clicks表中的字段 user 分组,并计算访问的URL数量。下图显示了随着clicks表中其他行的更新,随着时间的推移如何评估查询。

启动查询后,clicks表(左侧)为空。当第一行插入到clicks表中时,查询开始计算结果表。插入第一行[Mary,./home]后,结果表(右侧,顶部)由单行[Mary,1]组成。将第二行[Bob,./cart]插入到clicks表中时,查询将更新结果表并插入新行[Bob,1]。第三行[Mary,./prod?id=1]产生已计算结果行的更新,从而将[Mary,1]更新为[Mary,2]。最后,当第四行附加到clicks表时,查询将第三行[Liz,1]插入结果表。

第二个查询与第一个查询类似,但是在对clicks表进行计数之前,除了将user属性归类之外,表还在小时滚动的窗口中进行分组(在基于URL的计数之前,基于时间的计算(例如,窗口基于特殊的时间属性),稍后将进行讨论) )。同样,该图显示了在不同时间点的输入和输出,以可视化动态表的变化过程。

和以前一样,输入表clicks显示在左侧。该查询每小时持续计算结果并更新结果表。clicks 表包含四行,其时间戳(cTime)在 12:00:00 和 12:59:59 之间。该查询从输入计算出两个结果行(每个用户一个),并将它们附加到结果表中。对于 13:00:00 和 13:59:59 之间的下一个窗口,该clicks表包含三行,这将导致另外两行追加到结果表中。结果表将更新,因为clicks随着时间的推移会添加更多行。

更新和 Append 查询

尽管两个示例查询看起来非常相似(都计算分组计数汇总),但是它们在一个重要方面有所不同:

第一个查询更新先前发出的结果,即结果表包含INSERT和UPDATE更改的变更日志流。

第二个查询仅附加到结果表,即结果表的changelog流仅包含INSERT更改。

查询是生成仅追加表还是更新表具有一些含义:

产生更新更改的查询通常必须维护更多状态(请参阅以下部分)。

仅追加表到流的转换与更新表的转换不同(请参阅表到流转换部分)。

查询限制

可以将许多但不是全部的语义有效查询评估为流中的持续查询。某些查询由于需要维护的状态大小或计算更新过于昂贵(注:计算成本)而无法计算。

状态大小:持续查询是在无限制的流上评估的,通常应该运行数周或数月。因此,持续查询处理的数据总量可能非常大。必须更新先前发出的结果的查询需要维护所有发出的行,以便能够更新它们。例如,第一个示例查询需要存储每个用户的URL计数,以便能够增加计数并在输入表接收到新行时发出新结果。如果仅跟踪注册用户,则要维护的计数数量可能不会太多。但是,如果未注册的用户获得分配的唯一用户名,则要维护的计数数量将随着时间的推移而增加,并最终可能导致查询失败。

SELECT user, COUNT(url)
FROM clicks
GROUP BY user;

计算更新:即使只添加或更新一条输入记录,某些查询也需要重新计算和更新很大一部分发射结果行。显然,这样的查询不太适合作为持续查询执行。下面的查询是一个示例,该查询根据最终点击的时间为每个用户计算 排名。clicks表格收到新行后,lastAction用户的身份将更新,并且必须计算新排名。但是,由于两行不能具有相同的排名,因此所有排名较低的行也需要更新。

SELECT user, RANK() OVER (ORDER BY lastLogin)
FROM (
  SELECT user, MAX(cTime) AS lastAction FROM clicks GROUP BY user
);

“ 查询配置”章节讨论了用于控制持续查询的执行的参数。某些参数可用于权衡维护状态的大小以提高结果的准确性。

表到流的转换

动态表可以通过INSERT,UPDATE以及DELETE不断修改,就像一个普通的数据库表。它可能是具有单行的表,该表会不断更新;可能是一个仅插入的表,没有UPDATE和DELETE修改,或者介于两者之间。

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

Append-only流:可以通过发出插入的行将仅通过INSERT更改修改的动态表转换为流。

Retract 流:撤回流是具有两种消息类型的流,添加消息和撤回消息。通过将INSERT更改编码为添加消息,将DELETE更改编码为撤回消息,将UPDATE更改编码为更新(先前)行的更新消息,并将更新消息编码为更新(新)行,将动态表转换为撤回流。下图可视化了动态表到撤回流的转换。

Upsert 流:Upsert流是具有两种消息类型的流,Upsert 消息和Delete消息。转换为upsert流的动态表需要一个(可能是复合的)唯一键。通过将INSERT和UPDATE更改编码为upsert消息并将DELETE更改编码为delete消息,将具有唯一键的动态表转换为流。流消耗操作员需要知道唯一键属性,以便正确应用消息。流消费算子需要知道唯一键属性,以便正确应用消息。撤回流的主要区别在于UPDATE更改使用单个消息进行编码,因此效率更高。下图可视化了动态表到 upsert流的转换。

在Common Concepts页面上讨论了将动态表转换为DataStream的API。请注意,将动态表转换为DataStream时仅支持添加和撤消流。在TableSources和TableSinks页面上讨论了向外部系统发送动态表的TableSink接口。