4. Flink SQL 核心概念:动态表、连续查询与时间属性
好,咱们进入 Flink SQL 最核心的部分。说实话,刚接触 Flink SQL 那会儿,我也被这几个概念绕得有点晕——动态表、连续查询、时间属性,听起来都挺玄乎。但搞懂了它们,你才能真正理解 Flink 是怎么做到“流批一体”的。
我习惯把这三者放在一起讲,因为它们是一个有机整体。动态表是 Flink 对流数据的抽象,连续查询是操作动态表的引擎,而时间属性则是驱动这一切的“时钟”。咱们一个一个来拆。
4.1 动态表(Dynamic Table)
先问一个问题:传统数据库里的表,数据是静态的,对吧?你查一次,结果固定。但流数据是源源不断进来的,怎么用 SQL 去查?
Flink 的答案是:动态表。
动态表,说白了就是一张会随时间“生长”的表。每来一条新数据,表的内容就更新一次。你查询这张表,得到的是当前时刻的快照。
核心要点:动态表是 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'
);
注意,这里没有“数据存哪里”的概念。这张表背后就是 Kafka 的 topic,数据源源不断地流进来。每次你执行 SELECT,看到的是当前已经到达的数据。
我在项目中遇到过一种误解:有人以为动态表会把所有历史数据都存下来。其实不是。动态表可以只保留“当前状态”,比如最近 1 小时的聚合结果。这取决于你用的 更新模式。
4.1.1 三种更新模式
动态表的结果输出,有三种模式。我整理了一张表,方便你对比:
| 更新模式 | 说明 | 典型场景 |
|---|---|---|
| Append-only | 只追加,不更新、不删除 | 简单的过滤、投影、ETL |
| Retract | 通过 +I(插入)、-D(删除)消息表示变更 | 无窗口的聚合、去重 |
| Upsert | 通过 +I(插入)、+U(更新前)、-U(更新后)消息表示变更 | 有主键的聚合、维表关联 |
我的经验:刚开始写 Flink SQL 时,我经常搞混 Retract 和 Upsert。后来我记住一个诀窍:Retract 是先删后插,Upsert 是直接覆盖。如果你要输出到 Kafka,Retract 模式会产生两条消息(一条删除、一条插入),而 Upsert 模式只产生一条(更新后的值)。选哪个,取决于下游能不能处理删除消息。
4.2 连续查询(Continuous Query)
动态表有了,怎么查?用 连续查询。
连续查询,就是 Flink SQL 里你写的那些 SELECT、JOIN、GROUP BY。但它不是跑一次就结束,而是一直运行。每来一条新数据,查询结果就自动更新。
你想想看,这跟传统数据库的“一次查询”完全不同。传统 SQL 是“你给我数据,我算完给你结果,然后结束”。Flink 的连续查询是“你给我数据,我算完给你结果,然后继续等下一批”。
举个例子:
SELECT
user_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
这条查询在传统数据库里,跑一次就完了。但在 Flink 里,它会一直运行。每来一条新订单,total_amount 就更新一次。结果也是一张动态表,不断变化。
注意:连续查询的结果是“有状态”的。Flink 需要维护每个 user_id 的累计金额。如果用户量很大,状态也会很大。我曾经遇到过一个案例,因为没做状态清理,导致 TaskManager 内存爆了。后面我会讲怎么用 TTL(Time-to-Live)来管理状态大小。
4.2.1 连续查询的两种执行方式
连续查询怎么执行?Flink 有两种方式:
- Batch 模式:一次处理所有数据,输出最终结果。适合离线批处理。
- Streaming 模式:数据一来就处理,持续输出结果。适合实时流处理。
Flink 的厉害之处在于:同一套 SQL,两种模式都能跑。你只需要在提交作业时指定 execution.runtime-mode: BATCH 或 STREAMING。这就是“流批一体”的核心。
我个人的习惯是:开发阶段先用 Batch 模式跑历史数据,验证逻辑正确性;上线时切到 Streaming 模式,处理实时数据。这样既快又稳。
4.3 时间属性(Processing Time & Event Time)
时间属性,是 Flink SQL 里最容易踩坑的地方。我刚开始做实时数仓时,就因为时间没选对,导致聚合结果对不上。
Flink 支持两种时间:
- Processing Time:数据到达 Flink 算子的系统时间。
- Event Time:数据本身携带的时间戳,比如订单的创建时间。
区别在哪?我画了一张图,你看完就明白了。
4.3.1 Processing Time
Processing Time 是最简单的时间属性。Flink 直接用机器的系统时间。你不需要从数据里提取时间戳,也不需要处理乱序。
但它的缺点也很明显:结果不确定。同样的数据,跑两次,结果可能不一样。因为数据到达的时间受网络、负载等因素影响。
什么时候用 Processing Time?我建议:
- 对准确性要求不高的实时监控(比如“最近 5 分钟的请求量”)
- ETL 场景,只需要简单过滤、映射
- 测试环境,快速验证逻辑
4.3.2 Event Time
Event Time 才是生产环境的主力。它基于数据本身的时间戳,结果可复现。你跑历史数据,跟当时跑实时数据,结果完全一样。
但 Event Time 有个大问题:乱序。数据可能迟到。比如订单 A 在 10:00:00 创建,但因为网络延迟,10:00:05 才到 Flink。如果窗口是 10:00:00 ~ 10:00:01,那订单 A 就错过了。
Flink 怎么解决?用 Watermark(水位线)。
Watermark 是什么?它是一个“时间标记”,告诉 Flink:“在这个时间戳之前的数据,应该都到了。如果还有没到的,就当它迟到了,可以丢弃或侧输出。”
举个例子。你设置 WATERMARK FOR ts AS ts - INTERVAL '5' SECOND,意思是:允许数据最多迟到 5 秒。Flink 会等 5 秒,然后触发窗口计算。
我曾经在一个金融项目中,因为没设 Watermark,导致窗口一直不触发。后来排查发现,数据流里有一条时间戳是 1970-01-01 的脏数据,把 Watermark 卡住了。从那以后,我养成了一个习惯:在定义 Watermark 时,一定要加一个最大乱序容忍度,并且对数据做时间戳合法性校验。
4.3.3 两种时间的对比
| 维度 | Processing Time | Event Time |
|---|---|---|
| 时间来源 | Flink 机器系统时间 | 数据自身携带的时间戳 |
| 结果确定性 | 不确定,每次运行可能不同 | 确定,可复现 |
| 乱序处理 | 不需要 | 需要 Watermark |
| 性能 | 高,无额外开销 | 中等,需要维护 Watermark 状态 |
| 适用场景 | 实时监控、ETL、测试 | 精确聚合、报表、历史回放 |
我的建议:能用 Event Time 就用 Event Time。虽然它复杂一点,但结果可靠。Processing Time 只适合那些“看一眼就行,错了也无所谓”的场景。记住一句话:Event Time 是流批一体的基石。没有它,批处理和流处理的结果就对不上。
4.4 三者关系总结
最后,我用一张图把这三个概念串起来:
嗯,到这里,Flink SQL 的三个核心概念就讲完了。动态表让你用 SQL 操作流数据,连续查询让 SQL 持续运行,时间属性让结果准确可靠。这三者加起来,就是 Flink 实现“流批一体”的底层逻辑。