目录
前言🚩
传统的数据库SQL和实时SQL处理的差别还是很大的,这里简单列出一些区别:
尽管存在这些差异,但使用关系查询和SQL处理流并非不可能。高级关系数据库系统提供称为物化视图的功能。物化视图定义为SQL查询,就像常规虚拟视图一样。与虚拟视图相比,物化视图缓存查询的结果,使得在访问视图时不需要执行查询。缓存的一个常见挑战是避免缓存提供过时的结果。物化视图在修改其定义查询的基表时会过时。Eager View Maintenance是一种在更新基表后立即更新实例化视图的技术。
如果我们考虑以下内容,Eager View Maintenance和流上的SQL查询之间的联系就变得很明显:
- 数据库表是INSERT,UPDATE和DELETEDML语句流的结果,通常被称为更新日志流。
- 物化视图定义为SQL查询。为了更新视图,查询需要持续处理视图源表的更改日志流。
- 物化视图是流式SQL查询的结果。
有了上面的基础,下面可以介绍一下动态表的概念了。
动态表和持续不断查询
动态表flink table api和SQL处理流数据的核心概念。与静态表相比,动态表随时间而变化,但可以像静态表一样查询动态表,只不过查询动态表需要产生连续查询。连续查询永远不会终止,会生成动态表作为结果表。查询不断更新其(动态)结果表以反映其(动态)输入表的更改。最终,动态表上的连续查询与定义物化视图的查询非常相似。
值得注意的是,连续查询的结果始终在语义上等同于在输入表的快照上执行批处理的到的相同查询结果。
下图显示了流,动态表和连续查询的关系:
- 数据流被转化为动态表
- 在产生的动态表上执行连续不断的查询,产生一个动态结果表。
- 结果动态表再次被转化为数据流。
注意:动态表最重要的是逻辑概念。在查询执行期间,动态表不一定(完全)物化。
在下文中,会以schema如下的点击事件流来解释动态表和连续不断的查询。
[
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
]
stream转化成表
当然,想要用经典的sql去分析流数据,肯定要先将其转化为表。从概念上讲,流的每个新增记录都被解释为对结果表的Insert操作。最终,可以理解为是在从一个INSERT-only changelog流上构建一个表。
下图显示了click事件流(左侧)如何转换为表(右侧)。随着更多点击流记录的插入,生成的表不断增长。
注意:stream转化的表内部并没有被物化。
连续查询
在动态表上执行连续查询,并生成新的动态表作为结果表。与批处理查询不同,连续查询绝不会终止,而且会根据输入表的更新来更新它的结果表。在任何时间点,连续查询的结果在语义上等同于在输入表的快照上以批处理模式得到的查询的结果。
在下文中,我们将在用点击事件流定义的clicks表上展示两个示例查询。
第一个查询是一个简单的GROUP-BY COUNT聚合查询。主要是对clicks表按照user分组,然后统计url得到访问次数。下图展示了clicks表在数据增加期间查询是如何执行的。
假设当查询启动的事以后,clicks表为空。当第一行数据插入clicks表的时候,查询开始计算产生结果表。当[Mary, ./home]插入的时候,查询会在结果表上产生一行[Mary, 1]。当[Bob, ./cart]插入clicks表之后,查询会再次更新结果表,增加一行[Bob, 1]。当第三行,[Mary, ./prod?id=1]插入clicks表后,查询会更新结果表的[Mary, 1]为[Mary, 2]。最后,二手QQ买卖地图第四行数据插入clicks后,查询会给结果表增加一行[Liz, 1].
第二个查询仅仅是在上个查询的基础上增加了一个1小时的滚动窗口。下图展示了整个流水过程。
这个就类似批处理了,每个小时产生一次计算结果然后更新结果表。cTime的时间范围在12:00:00 ~12:59:59的时候总共有四行数据,查询计算出了两行结果,并将其追加到结果表。Ctime窗口在13:00:00 and 13:59:59的时候,总共有三行数据,查询再次产生两行结果追加到结果表。随着时间的推移,click数据会被追加到clicks表,结果表也会不断有新的结果产生。
Update 和 append 查询
尽管两个示例查询看起来非常相似(都计算了分组计数聚合),但是内部逻辑还是区别较大:
- 第一个查询更新以前发出的结果,即结果表的更改日志流包含INSERT和UPDATE更改。
- 第二个查询仅append到结果表,即结果表的更改日志流仅包含INSERT更改。
查询是生成仅append表还是update表有一些区别:
- 产生update变化的查询通常必须维护更多状态。
- 将仅append表转换为流与将update表的转换为流,方式不同。
查询限制
并不是所有的查询都能以流查询的格式执行的。因为有些查询计算起来成本比较高,要么就是要维护的状态比较大,要么就是计算更新成本高。
状态大小:连续查询在无界流上执行,通常应该运行数周或数月,甚至7*24小时。因此,连续查询处理的数据总量可能非常大。为了更新先前生成的结果,可能需要维护所有输出的行。例如,第一个示例查询需要存储每个用户的URL计数,以便能够增加计数,并在输入表收到新行时发出新结果。如果仅统计注册用户,则要维护的计数可能不会太高。但是,如果未注册的用户分配了唯一的用户名,则要维护的计数数将随着时间的推移而增长,最终可能导致查询失败。
SELECT user, COUNT(url)
FROM clicks
GROUP BY user;
计算更新:有时即使只添加或更新了单个输入记录,某些查询也需要重新计算和更新大部分发出的结果行。显然,这样的查询不适合作为连续查询执行。下面sql是一个示例查询,该查询基于最后一次点击的时间为每个用户计算RANK 。一旦clicks表接收到新增行,用户的lastAction就会更新,并且必须计算新的排名。但是,由于两行不能具有相同的排名,因此所有排名较低的行也需要更新。
SELECT user, RANK() OVER (ORDER BY lastLogin)
FROM (
SELECT user, MAX(cTime) AS lastAction FROM clicks GROUP BY user
);
表转化为流
可以像传统数据库表一样使用INSERT, UPDATE, 和DELETE修改动态表。当将动态表转化为stream或者写入外部系统的时候,需要对修改进行编码。Flink的Table API和SQL支持三种方式来编码动态表的变化。
Append-only stream:假如动态表的更改操作仅仅是insert ,那么变为stream就仅仅需要将插入的行发送出去即可。
Retract stream: retract(回撤)流是包含两种类型的消息的流,增加消息和回撤消息。通过将INSERT编码为增加消息,DELETE编码为回撤消息,将UPDATE编码为对先前行的回撤消息和对新增行的增加消息,来完成将动态表转换为收回流。下图显示了动态表到回收流的转换。
Upsert流: upsert流是一种包含两种消息,upsert消息和删除消息的流。转换为upsert流的动态表需要唯一键。具有唯一键的动态表通过将INSERT和UPDATE编码为upsert消息,DELETE编码为删除消息来完成动态表转化为流。流算符需要知道唯一键属性才能正确处理消息。与回撤流的主要区别在于,UPDATE使用单个消息对update进行编码,因此更有效。下图显示了动态表到upsert流的转换。