3. Flink SQL 基础回顾:动态表、连续查询、时间属性
做实时宽表 Join 之前,有几个基础概念必须掰扯清楚。我见过不少同学,SQL 写得飞起,但一问动态表是什么,就卡住了。说白了,Flink SQL 和传统数据库 SQL 最大的区别,就是它处理的是无界流,而不是有界表。
这一节,我们就来聊聊动态表、连续查询,还有那两个绕不开的时间属性——Processing Time 和 Event Time。这些都是宽表 Join 的基石,绕不过去的。
3.1 动态表:流上的“快照”
传统数据库里,表是静态的。你查一次,数据就在那里,不会变。但 Flink 里不一样,数据源源不断地流进来,表的内容也在不断变化。这就是动态表。
你可以把动态表想象成一张一直在更新的 Excel 表格。每来一条新数据,表格就新增一行,或者更新某一行,甚至删除某一行。Flink SQL 就是在这个不断变化的表上做查询。
核心要点:动态表是流数据在某个时间点的“视图”。流是无限的,动态表是流在逻辑上的映射。
举个例子,一个订单流:
-- 订单流数据 (JSON 格式)
{"order_id": 1, "user_id": "A", "amount": 100, "ts": "2024-01-01 10:00:00"}
{"order_id": 2, "user_id": "B", "amount": 200, "ts": "2024-01-01 10:01:00"}
{"order_id": 3, "user_id": "A", "amount": 150, "ts": "2024-01-01 10:02:00"}
在 Flink 中,这个流被注册成一张动态表。每次查询,你看到的都是截至当前时刻的数据集合。嗯,这里要注意,动态表不是全量数据,它背后是无限的流,你只能通过查询来获取当前“快照”。
3.2 连续查询:一直在跑的 SQL
动态表上执行的查询,叫连续查询。它不会像传统 SQL 那样跑完就结束,而是一直运行,每来一条新数据,查询结果就更新一次。
我个人习惯把连续查询理解成一个“监听器”。你写一条 SQL,Flink 就把它转成一个流式计算任务,持续不断地处理流入的数据,并输出结果。
看个简单的例子:
-- 连续查询:统计每个用户的订单总金额
SELECT
user_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
这条 SQL 在传统数据库里,跑一次就出结果。但在 Flink 里,它会一直运行。每来一条新订单,对应 user_id 的 total_amount 就会更新。你想想看,这就是实时计算的本质——结果随数据流动而不断刷新。
避坑指南:我曾经在项目里遇到一个坑——连续查询的结果是更新流,不是追加流。也就是说,下游如果是一个 Kafka Sink,默认行为是不断发送更新后的结果,而不是只发新增数据。这会导致下游消费者收到大量重复或更新的消息,需要特别注意。
3.3 时间属性:Processing Time vs Event Time
时间属性是 Flink SQL 的灵魂。没有时间,你就没法做窗口聚合,更没法做基于时间的 Join。宽表 Join 里,时间属性直接决定了关联的准确性和延迟。
Flink 提供了两种时间属性:
| 属性 | 来源 | 特点 | 适用场景 |
|---|---|---|---|
| Processing Time | 机器本地时钟 | 简单、低延迟、不依赖数据本身 | 对准确性要求不高、追求吞吐的场景 |
| Event Time | 数据中的时间戳字段 | 准确、能处理乱序、需要 Watermark | 对准确性要求高的场景,如金融、电商 |
3.3.1 Processing Time:简单粗暴
Processing Time 就是 Flink 算子所在机器的当前时间。它最简单,不需要从数据里提取时间戳,也不需要处理乱序。但缺点也很明显——结果不准确。
为什么?因为 Processing Time 依赖的是处理时刻,而不是事件发生的时刻。如果数据因为网络延迟、背压等原因被延迟处理,Processing Time 就会失真。
举个例子:
-- 使用 Processing Time 的滚动窗口
CREATE TABLE orders (
order_id STRING,
user_id STRING,
amount DOUBLE,
proc_time AS PROCTIME() -- 声明 Processing Time
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'properties.bootstrap.servers' = 'localhost:9092',
'format' = 'json'
);
SELECT
TUMBLE_START(proc_time, INTERVAL '5' MINUTE) AS window_start,
user_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY
TUMBLE(proc_time, INTERVAL '5' MINUTE),
user_id;
这条 SQL 每 5 分钟统计一次订单金额。但注意,它用的是 Processing Time。如果一条订单在 10:00 发生,但因为 Kafka 积压,10:03 才被 Flink 处理,那它就会被归到 10:00-10:05 这个窗口里。看起来没问题?但如果积压更严重,10:06 才处理,它就会被归到 10:05-10:10 的窗口里。结果就错了。
警告:Processing Time 只适合对时间准确性不敏感的场景,比如简单的实时监控、日志聚合。做宽表 Join 时,如果两个流的时间基准不一致,用 Processing Time 会导致关联错位。我建议你慎重。
3.3.2 Event Time:准确但复杂
Event Time 是数据本身携带的时间戳,比如订单的创建时间、用户的行为时间。它不依赖机器时钟,所以能真实反映事件发生的顺序。但代价是——你需要处理乱序数据。
乱序是分布式系统的常态。网络延迟、分区不均、上游重试,都可能导致数据到达顺序和发生顺序不一致。Flink 用 Watermark 来解决这个问题。
Watermark 是一个“时间水位线”。它告诉 Flink:在这个时间戳之前的数据,应该都已经到了。如果还有没到的,就当作迟到数据丢弃或侧输出。
看个 Event Time 的例子:
-- 使用 Event Time 的滚动窗口
CREATE TABLE orders (
order_id STRING,
user_id STRING,
amount DOUBLE,
event_ts TIMESTAMP(3), -- 数据中的时间戳
WATERMARK FOR event_ts AS event_ts - INTERVAL '5' SECOND -- 声明 Watermark
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'properties.bootstrap.servers' = 'localhost:9092',
'format' = 'json'
);
SELECT
TUMBLE_START(event_ts, INTERVAL '5' MINUTE) AS window_start,
user_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY
TUMBLE(event_ts, INTERVAL '5' MINUTE),
user_id;
这里的关键是 WATERMARK FOR event_ts AS event_ts - INTERVAL '5' SECOND。它声明了一个 5 秒的延迟容忍度。也就是说,Flink 会等待 5 秒,如果某个时间戳的数据还没到,就认为它迟到了,不再等待。
我记得有一次做实时大屏,用的 Event Time + Watermark。业务方说数据不准,排查了半天,发现是 Watermark 设置得太小,导致大量正常数据被当作迟到数据丢弃了。后来把延迟容忍度从 2 秒改成 10 秒,问题就解决了。嗯,这里要注意,Watermark 不是越大越好,太大会增加延迟,太小会丢数据,需要根据业务场景调优。
3.4 时间属性与宽表 Join 的关系
宽表 Join 的核心,是把多个流的数据按照某个 key 关联起来。但流是无限的,如果不加时间约束,Join 会一直等待,永远出不了结果。所以,宽表 Join 必须依赖时间属性来划定关联范围。
常见的做法有两种:
- 基于 Processing Time 的 Interval Join:简单,但结果可能不准确。适合对延迟敏感、对准确性要求不高的场景。
- 基于 Event Time 的 Interval Join:准确,但需要处理乱序和 Watermark。适合金融、电商等对数据一致性要求高的场景。
下面这张图,展示了时间属性在宽表 Join 中的核心位置:
从这张图可以看出,不同来源的流可能携带不同的时间属性。宽表 Join 之前,必须通过时间属性处理层将它们对齐。否则,订单流用 Event Time,物流流用 Processing Time,两个流的时间基准不同,Join 出来的结果就是乱的。
我的建议:做宽表 Join 时,尽量统一使用 Event Time。虽然复杂一点,但结果可靠。如果某个流实在无法提供时间戳,再考虑用 Processing Time 兜底。但一定要在文档里注明,这个字段的关联结果可能存在偏差。
3.5 小结
这一节我们聊了三个核心概念:
- 动态表:流在逻辑上的映射,内容不断变化。
- 连续查询:一直在运行的 SQL,结果随数据流动而更新。
- 时间属性:Processing Time 简单但可能不准,Event Time 准确但需要处理乱序。