4、动态表与连续查询:流式 SQL 的核心思想
说实话,刚接触 Flink SQL 那会儿,我也挺困惑的。明明是个流,怎么就能用 SQL 查了?SQL 不是查静态表的吗?
后来我才明白,Flink 搞了一套非常巧妙的抽象——动态表。它把无界的数据流,映射成一张会随时间“生长”的表。你在这张表上跑 SQL,结果就是连续查询。
这一章,我就带你把这个核心思想彻底吃透。
4.1 动态表:流上的“快照”视图
先说说动态表是什么。
传统数据库的表,数据是静态的。你查一次,结果固定。但 Flink 里的表,数据会源源不断地追加进来,或者发生更新。这张表的内容,随着时间一直在变。
我习惯把它理解成:流在某个时间点上的“快照”视图。
核心定义:动态表是随时间演变的表。它是对数据流的一种逻辑映射,支持用标准 SQL 进行查询。
举个例子。假设你有一个电商订单流,每条记录包含:order_id、user_id、amount、timestamp。
在 Flink 中,你可以直接把这个流注册成一张表:
CREATE TABLE orders (
order_id BIGINT,
user_id BIGINT,
amount DOUBLE,
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'format' = 'json'
);
这张 orders 表,就是动态表。Kafka 里每来一条消息,表里就多一行。你查询它的时候,看到的是截至当前时刻的所有数据。
嗯,这里要注意:动态表不是把数据全存到内存里。它背后有状态后端(RocksDB 或 Heap)做支撑,Flink 会按需维护必要的状态。
4.2 连续查询:SQL 一直在跑
动态表有了,那怎么查?
传统 SQL 是“查一次,出结果”。Flink SQL 是“查一次,结果持续更新”。这就是连续查询。
你提交一个 SQL,Flink 会把它翻译成一个持续运行的 DataStream 作业。数据源源不断进来,结果也源源不断输出。
举个例子:
SELECT
user_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
这个查询会持续计算每个用户的累计消费金额。每来一条新订单,对应 user_id 的总金额就会更新。
我个人觉得,理解连续查询的关键在于:它维护的是“结果表”的变更日志。每次有数据进来,结果表可能插入新行、更新旧行、甚至删除行。这些变更,最终会以 INSERT、UPDATE、DELETE 的形式输出到下游。
小技巧:调试连续查询时,可以用 PRINT 连接器把变更日志打印到控制台,直观看到每条数据触发了什么操作。
4.3 流式 SQL 与传统 SQL 的区别
这个区别,我当年踩过不少坑。总结下来,主要有三点:
| 维度 | 传统 SQL | 流式 SQL |
|---|---|---|
| 数据范围 | 全量数据,一次查询 | 无界数据,持续查询 |
| 结果时效 | 查询时点快照 | 结果持续更新 |
| 状态管理 | 无状态,依赖存储引擎 | 有状态,依赖 Flink State |
| 语义保障 | ACID 事务 | Exactly-Once / At-Least-Once |
| 查询终止 | 返回结果后结束 | 作业取消前持续运行 |
说白了,传统 SQL 是“拍照片”,流式 SQL 是“录视频”。
为什么会这样?因为数据源的本质不同。传统 SQL 面对的是有界数据集,流式 SQL 面对的是无界数据流。
我曾经在一个项目中,直接把 MySQL 的统计 SQL 搬到 Flink 上,结果发现 GROUP BY 的结果一直在变,下游的报表系统根本没法用。后来才意识到,需要加 窗口 来把无界流切成有界块。
避坑指南:流式 SQL 中的 GROUP BY 不加窗口,状态会无限膨胀。务必配合 TUMBLE、HOP 或 CUMULATE 窗口使用。
4.4 核心流程:从流到动态表再到连续查询
整个链路是这样的:
- 数据流(DataStream)源源不断产生事件。
- Flink 将流转换为动态表(Dynamic Table)。
- 用户在动态表上执行连续查询(Continuous Query)。
- 查询结果生成一个新的结果动态表。
- 结果动态表的变更日志,再转换为数据流输出到 Kafka、数据库或控制台。
下面这张图,把这个流程画清楚了:
4.5 三种结果模式:Append、Retract、Upsert
连续查询的结果,根据 SQL 类型不同,会以三种模式输出:
- Append 模式:只追加,不更新。适用于
SELECT、FILTER等操作。 - Retract 模式:先发送一条
DELETE(旧值),再发送一条INSERT(新值)。适用于GROUP BY等聚合操作。 - Upsert 模式:直接发送
UPDATE消息,包含新值。适用于有主键的聚合或JOIN。
我记得刚开始用 Retract 模式时,下游的 ClickHouse 表老是报错。后来发现,ClickHouse 不支持 DELETE 操作。解决方案是把结果表改成 Upsert 模式,或者用 Kafka 做中间层,先攒成变更日志再写入。
经验之谈:如果下游是 Kafka,建议用 Upsert 模式配合 changelog-json 格式,这样每条消息都携带完整的变更信息,消费端处理起来更简单。
4.6 避坑:状态膨胀与数据延迟
连续查询虽然强大,但有两个常见坑:
- 状态膨胀:无界聚合(比如
COUNT(*)不加窗口)会导致状态无限增长。我建议用idle-state-retention配置来清理过期状态。 - 数据延迟:乱序数据会导致结果“回退”。比如先来一条 10:00 的数据,又来一条 09:59 的数据,聚合结果会先增加再减少。用
WATERMARK和ALLOWED_LATENESS可以控制这个行为。
我曾经在一个实时大屏项目里,没配 Watermark,结果聚合结果忽高忽低,业务方差点投诉。后来加上 Watermark 和 5 秒的 allowed lateness,数据才稳定下来。
避坑指南:连续查询的语义是“最终一致”,不是“强一致”。如果业务要求精确一次,必须配合 WATERMARK 和 EXACTLY_ONCE 语义。
4.7 小结
动态表和连续查询,是 Flink SQL 的基石。理解它们,你就能明白:
- 为什么 Flink SQL 能处理无界数据
- 为什么聚合结果会持续更新
- 为什么需要窗口和 Watermark
说白了,Flink 就是用“动态表”这个抽象,把流式处理包装成了大家熟悉的 SQL 操作。你不需要关心底层是 DataStream 还是 State,只需要写 SQL 就行。