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:数据本身携带的时间戳,比如订单的创建时间。

区别在哪?我画了一张图,你看完就明白了。

Processing Time vs Event Time 数据源 网络延迟 Flink 算子 输出 事件时间: 10:00:00 处理时间: 10:00:05 处理时间: 10:00:06 处理时间: 10:00:07 关键区别 • Processing Time:数据到达 Flink 的系统时间。简单,但结果不确定。 • Event Time:数据本身携带的时间。复杂,但结果可复现。 • 上图中,事件时间是 10:00:00,但经过网络延迟,处理时间变成了 10:00:05。 • 如果按 Processing Time 做窗口,结果会偏移;按 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 三者关系总结

最后,我用一张图把这三个概念串起来:

动态表 + 连续查询 + 时间属性 = 流批一体 动态表 流数据的表化抽象 连续查询 持续运行的 SQL 逻辑 结果表 持续更新的输出 时间属性(Processing Time / Event Time) 驱动动态表更新和连续查询触发的“时钟” 💡 动态表是“数据容器”,连续查询是“处理逻辑”,时间属性是“调度器”。三者缺一不可。

嗯,到这里,Flink SQL 的三个核心概念就讲完了。动态表让你用 SQL 操作流数据,连续查询让 SQL 持续运行,时间属性让结果准确可靠。这三者加起来,就是 Flink 实现“流批一体”的底层逻辑。