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 核心流程:从流到动态表再到连续查询

整个链路是这样的:

  1. 数据流(DataStream)源源不断产生事件。
  2. Flink 将流转换为动态表(Dynamic Table)。
  3. 用户在动态表上执行连续查询(Continuous Query)。
  4. 查询结果生成一个新的结果动态表。
  5. 结果动态表的变更日志,再转换为数据流输出到 Kafka、数据库或控制台。

下面这张图,把这个流程画清楚了:

流 → 动态表 → 连续查询 → 结果流 数据流 Kafka / Pulsar 动态表 逻辑映射 连续查询 SQL 持续执行 结果流 INSERT / UPDATE / DELETE 关键理解 • 动态表是流在逻辑上的“表”视图 • 连续查询的结果也是一张动态表,它的变更日志就是结果流 • 整个过程是“流进 → 表查 → 流出”的闭环

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 就行。