动态表转换成流
更新(Update)查询
在代码中定义一个 SQL 查询
Table urlCountTable = tableEnv.sqlQuery(“SELECT user, COUNT(url) as cnt FROM EventTable GROUP BY user”);
主要是分组聚合统计每个用户的点击次数。我们把原始的动态表注册为EventTable,经过查询转换后得到 urlCountTable;这个结果动态表中包含两个字段,具体定义如下:
[ user: VARCHAR, // 用户名 cnt: BIGINT // 用户访问 url 的次数 ]
当原始动态表不停地插入新的数据时,查询得到的 urlCountTable 会持续地进行更改。由于 count 数量可能会叠加增长,因此这里的更改操作可以是简单的插入(Insert),也可以是对之前数据的更新(Update)。换句话说,用来定义结果表的更新日志(changelog)流中,包含了 INSERT 和UPDATE 两种操作。这种持续查询被称为更新查询(Update Query),更新查询得到的结果表如果想要转换成DataStream,必须调用 toChangelogStream()方法。

具体步骤解释如下:
- 当查询启动时,原始动态表 EventTable 为空;
- 当第一行 Alice 的点击数据插入EventTable 表时,查询开始计算结果表,urlCountTable中插入一行数据[Alice,1]。
- 当第二行 Bob 点击数据插入 EventTable 表时,查询将更新结果表并插入新行[Bob,1]。
- 第三行数据到来,同样是 Alice 的点击事件,这时不会插入新行,而是生成一个针对已有行的更新操作。这样,结果表中第一行[Alice,1]就更新为[Alice,2]。
- 当第四行 Cary 的点击数据插入到 EventTable 表时,查询将第三行[Cary,1]插入到结果表中。
查询过程用到了分组聚合,结果表中就会产生更新操作。如果执行一个简单的条件查询,结果表中就会像原始表EventTable 一样,只有插入(Insert)操作.
Table aliceVisitTable = tableEnv.sqlQuery("SELECT url, user FROM EventTable WHERE user = 'Cary'");
这样的持续查询,就被称为追加查询(Append Query),它定义的结果表的更新日志(changelog)流中只有 INSERT 操作。追加查询得到的结果表,转换成 DataStream 调用方法没有限制,可以直接用 toDataStream(),也可以像更新查询一样调用 toChangelogStream()。
只要用到了聚合,在之前的结果上有叠加,就会产生更新操作,就是一个更新查询。但事实上,更新查询的判断标准是结果表中的数据是否会有 UPDATE 操作,如果聚合的结果不再改变,那么同样也不是更新查询。
什么时候聚合的结果会保持不变呢?一个典型的例子就是窗口聚合。
考虑开一个滚动窗口,统计每一小时内所有用户的点击次数,并在结果表中增加一个endT 字段,表示当前统计窗口的结束时间。这时结果表的字段定义如下:
user: VARCHAR, // 用户名 endT: TIMESTAMP, // 窗口结束时间 cnt: BIGINT // 用户访问 url 的次数 ]
当原始动态表不停地插入新的数据时,查询得到的结果 result 会持续地进行更改。比如时间戳在 12:00:00 到 12:59:59 之间的有四条数据,其中 Alice 三次点击、Bob 一次点击;所以当水位线达到 13:00:00 时窗口关闭,输出到结果表中的就是新增两条数据[Alice, 13:00:00, 3]和[Bob, 13:00:00, 1]。同理,当下一小时的窗口关闭时, 也会将统计结果追加到 result 表后面,而不会更新之前的数据。 
由于窗口的统计结果是一次性写入结果表的,所以结果表的更新日志流中只会包含插入 INSERT 操作,而没有更新 UPDATE 操作。所以这里的持续查询,依然是一个追加(Append)查询。结果表 result 如果转换成 DataStream,可以直接调用 toDataStream()方法。
将动态表转换为流
动态表也可以通过插入(Insert)、更新(Update)和删除(Delete)操作,进行持续的更改。将动态表转换为流或将其写入外部系统时,就需要对这些更改操作进 行编码,通过发送编码消息的方式告诉外部系统要执行的操作。在 Flink 中,Table API 和 SQL 支持三种编码方式:
1. 仅追加(Append-only)流
仅通过插入(Insert)更改来修改的动态表,可以直接转换为"仅追加"流。这个流中发出的数据,其实就是动态表中新增的每一行。只有新增操作,数据告诉第三方
2. 撤回(Retract)流
撤回流是包含两类消息的流,添加(add)消息和撤回(retract)消息。
具体的编码规则是:INSERT 插入操作编码为 add 消息;DELETE 删除操作编码为 retract 消息;而 UPDATE 更新操作则编码为被更改行的 retract 消息和更新后行(新行)的 add 消息。可以通过编码后的消息指明所有的增删改操作,一个动态表就可以转换为撤回流了。
可以看到,更新操作对于撤回流来说,对应着两个消息:之前数据的撤回(删除)和新数据的插入。显示了将动态表转换为撤回流的过程。

用+代表 add 消息(对应插入 INSERT 操作),用-代表 retract 消息(对应删除 DELETE 操作);当 Alice 的第一个点击事件到来时,结果表新增一条数据[Alice, 1];而当 Alice 的第二个点击事件到来时,结果表会将[Alice, 1]更新为[Alice, 2],对应的编码就是删除[Alice, 1]、插入[Alice, 2]。这样当一个外部系统收到这样的两条消息时,就知道是要对Alice 的点击统计次数进行更新了。
更新插入(Upsert)流
更新插入流中只包含两种类型的消息:更新插入(upsert)消息和删除(delete)消息。所谓的"upsert"其实是"update"和"insert"的合成词,所以对于更新插入流来说,INSERT 插入操作和UPDATE 更新操作,统一被编码为upsert 消息;而DELETE 删除操作则被编码为delete消息。
既然更新插入流中不区分插入(insert)和更新(update),那我们自然会想到一个问题:如果希望更新一行数据时,怎么保证最后做的操作不是插入呢?
这就需要动态表中必须有唯一的键(key)。通过这个 key 进行查询,如果存在对应的数据就做更新(update),如果不存在就直接插入(insert)。这是一个动态表可以转换为更新插入流的必要条件。当然,收到这条流中数据的外部系统,也需要知道这唯一的键(key),这样才能正确地处理消息。
显示了将动态表转换为更新插入流的过程。

更新插入流跟撤回流的主要区别在于,更新(update)操作由于有 key 的存在, 只需要用单条消息编码就可以,因此效率更高。
需要注意的是,在代码里将动态表转换为 DataStream 时,只支持仅追加(append-only) 和撤回(retract)流,我们调用 toChangelogStream()得到的其实就是撤回流;这也很好理解,DataStream 中并没有 key 的定义,所以只能通过两条消息一减一增来表示更新操作。而连接到外部系统时,则可以支持不同的编码方法,这取决于外部系统本身的特性。
浙公网安备 33010602011771号