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 的关系 时间属性在宽表 Join 中的核心位置 订单流 (Event Time) 支付流 (Event Time) 物流流 (Processing Time) 时间属性处理层 Watermark 生成 乱序数据处理 窗口分配 迟到数据策略 时间语义对齐 宽表 Join Interval Join Temporal Join Lookup Join 说明 实线:Event Time 流 虚线:Processing Time 流 所有流需对齐时间语义

从这张图可以看出,不同来源的流可能携带不同的时间属性。宽表 Join 之前,必须通过时间属性处理层将它们对齐。否则,订单流用 Event Time,物流流用 Processing Time,两个流的时间基准不同,Join 出来的结果就是乱的。

我的建议:做宽表 Join 时,尽量统一使用 Event Time。虽然复杂一点,但结果可靠。如果某个流实在无法提供时间戳,再考虑用 Processing Time 兜底。但一定要在文档里注明,这个字段的关联结果可能存在偏差。

3.5 小结

这一节我们聊了三个核心概念:

  • 动态表:流在逻辑上的映射,内容不断变化。
  • 连续查询:一直在运行的 SQL,结果随数据流动而更新。
  • 时间属性:Processing Time 简单但可能不准,Event Time 准确但需要处理乱序。