3. Flink SQL 聚合基础:GROUP BY 语法、TUMBLE、HOP、SESSION 窗口函数

好,咱们进入正题。DWS 层的核心工作说白了就是「聚合」。你从 DWD 或者 DIM 层把明细数据接进来,然后按照业务维度做汇总,产出实时指标。Flink SQL 里最常用的聚合手段,就是 GROUP BY 配合各种窗口函数。

我个人习惯把窗口函数分成两类:一类是时间窗口,一类是非时间窗口。非时间窗口就是普通的 GROUP BY,按某个字段分组,比如按用户 ID 统计订单数。但实时数仓里,绝大多数场景都是跟时间绑定的——「最近 5 分钟的 GMV」、「过去 1 小时的 UV」等等。所以窗口函数才是重头戏。

这一节,我会把 GROUP BY 基础语法、TUMBLE(滚动窗口)、HOP(滑动窗口)、SESSION(会话窗口)一个一个讲清楚。每个窗口我都会给一个我在项目中用过的真实案例,顺便说说踩过的坑。

3.1 GROUP BY 基础语法

先看最简单的。Flink SQL 里的 GROUP BY 跟标准 SQL 几乎一样,但有一个关键区别:它必须跟时间属性绑定。你想想看,流数据是无限的,如果不加窗口,GROUP BY 会把所有历史数据都攒在一起,内存迟早爆掉。

所以 Flink 要求:要么你 GROUP BY 的字段里包含一个时间窗口,要么你使用处理时间或事件时间的窗口函数。否则,语法检查直接报错。

来看一个基础示例:

-- 最简单的 GROUP BY:按用户分组,统计每个用户的订单数
SELECT
    user_id,
    COUNT(*) AS order_cnt,
    SUM(amount) AS total_amount
FROM dwd_order_detail
GROUP BY user_id;

嗯,这里要注意。上面这个 SQL 在 Flink 里其实跑不起来的——因为没有窗口。正确的写法应该是:

-- 加上窗口,按 1 分钟滚动窗口 + 用户分组
SELECT
    TUMBLE_END(proctime, INTERVAL '1' MINUTE) AS window_end,
    user_id,
    COUNT(*) AS order_cnt,
    SUM(amount) AS total_amount
FROM dwd_order_detail
GROUP BY
    TUMBLE(proctime, INTERVAL '1' MINUTE),
    user_id;

看到区别了吗?GROUP BY 里多了一个 TUMBLE(proctime, INTERVAL '1' MINUTE),这就是 Flink 的窗口函数。它告诉 Flink:「每 1 分钟切一个窗口,在这个窗口内按 user_id 分组聚合」。

核心要点:Flink SQL 的 GROUP BY 必须配合窗口函数使用,否则无法在流上执行。窗口函数决定了「聚合的范围」,GROUP BY 字段决定了「聚合的维度」。

3.2 TUMBLE 窗口函数(滚动窗口)

TUMBLE 窗口,也叫滚动窗口。它的特点是:窗口之间不重叠,每个数据只属于一个窗口。比如你设置 5 分钟的滚动窗口,那么 [00:00, 00:05)、[00:05, 00:10)……每个数据只会落入其中一个区间。

我在项目中遇到过这样一个场景:实时监控每 5 分钟的接口调用量。用 TUMBLE 窗口就非常合适,因为业务方只关心「每 5 分钟有多少次调用」,不需要滑动效果。

语法结构:

TUMBLE(time_attr, window_interval)
  • time_attr:时间属性,可以是处理时间(proctime)或事件时间(event_time)。
  • window_interval:窗口大小,比如 INTERVAL '5' MINUTE。

完整示例:

-- 每 5 分钟统计一次各商品的销量
SELECT
    TUMBLE_START(event_time, INTERVAL '5' MINUTE) AS window_start,
    TUMBLE_END(event_time, INTERVAL '5' MINUTE) AS window_end,
    product_id,
    SUM(quantity) AS total_qty
FROM dwd_order_detail
GROUP BY
    TUMBLE(event_time, INTERVAL '5' MINUTE),
    product_id;

小技巧:TUMBLE_START 和 TUMBLE_END 可以帮你拿到窗口的起止时间,方便下游做时间对齐。我个人习惯在 DWS 层输出时,一定带上 window_end 字段,这样下游做时间分区时直接用它。

3.3 HOP 窗口函数(滑动窗口)

HOP 窗口,也叫滑动窗口。它的特点是:窗口之间可以重叠,一个数据可能属于多个窗口。比如你设置窗口大小 10 分钟,滑动步长 5 分钟,那么 [00:00, 00:10)、[00:05, 00:15)……每个数据会落入两个窗口。

为什么要用 HOP?举个例子:业务方想看「过去 10 分钟的实时销售额」,但希望每 5 秒刷新一次。如果用 TUMBLE,每 10 分钟才出一个结果,刷新太慢。用 HOP 窗口,窗口大小 10 分钟,滑动步长 5 秒,就能做到「每 5 秒输出一次过去 10 分钟的聚合结果」。

语法结构:

HOP(time_attr, slide_interval, window_interval)
  • time_attr:时间属性。
  • slide_interval:滑动步长,即每次向前移动的时间。
  • window_interval:窗口大小。

我曾经踩过一个坑:窗口大小必须是滑动步长的整数倍。有一次我设了窗口 7 分钟,滑动 3 分钟,结果 Flink 直接报错。后来查文档才发现,HOP 窗口要求 window_interval % slide_interval == 0。嗯,这个限制要注意。

正确示例:

-- 每 1 分钟输出一次过去 10 分钟的 UV
SELECT
    HOP_START(event_time, INTERVAL '1' MINUTE, INTERVAL '10' MINUTE) AS window_start,
    HOP_END(event_time, INTERVAL '1' MINUTE, INTERVAL '10' MINUTE) AS window_end,
    COUNT(DISTINCT user_id) AS uv
FROM dwd_page_view
GROUP BY
    HOP(event_time, INTERVAL '1' MINUTE, INTERVAL '10' MINUTE);

避坑指南:HOP 窗口的滑动步长越小,窗口数量越多,计算开销越大。我曾经在一个高吞吐场景下把滑动步长设成了 1 秒,结果状态后端压力飙升。建议滑动步长不要小于 5 秒,除非你的集群资源非常充裕。

3.4 SESSION 窗口函数(会话窗口)

SESSION 窗口,也叫会话窗口。它跟前两种窗口完全不同——窗口的边界不是固定的时间点,而是由数据的活跃程度决定的。你设置一个「会话超时时间」,如果连续两个数据的时间间隔超过了这个超时时间,就切一个新窗口。

这个窗口特别适合用户行为分析。比如你想统计「用户每次访问会话的时长」,用户连续操作算一次会话,中间超过 30 分钟没动作就算新会话。用 SESSION 窗口就非常自然。

语法结构:

SESSION(time_attr, session_gap)
  • time_attr:时间属性。
  • session_gap:会话超时时间,比如 INTERVAL '30' MINUTE。

我在项目中用过 SESSION 窗口做「实时用户留存分析」。当时业务方想知道:用户从第一次访问开始,30 分钟内有没有再次访问。用 SESSION 窗口 + 事件时间,配合 watermark,就能准确判断会话是否结束。

示例:

-- 按用户分组,会话超时 30 分钟
SELECT
    SESSION_START(event_time, INTERVAL '30' MINUTE) AS session_start,
    SESSION_END(event_time, INTERVAL '30' MINUTE) AS session_end,
    user_id,
    COUNT(*) AS event_cnt,
    MAX(event_time) - MIN(event_time) AS session_duration
FROM dwd_user_event
GROUP BY
    SESSION(event_time, INTERVAL '30' MINUTE),
    user_id;

注意:SESSION 窗口的触发依赖于 watermark。如果数据乱序严重,或者 watermark 推进太慢,会话窗口可能迟迟不关闭,导致结果延迟。我建议在 SESSION 窗口场景下,把 idle_timeout 和 watermark_strategy 调得激进一些。

3.5 三种窗口对比

为了方便你快速选择,我整理了一张对比表:

窗口类型 窗口是否重叠 窗口边界 典型场景 注意事项
TUMBLE 不重叠 固定时间点 每 5 分钟统计一次销量 窗口大小固定,数据只属于一个窗口
HOP 重叠 固定时间点 过去 10 分钟的 UV,每 1 分钟刷新 窗口大小必须是滑动步长的整数倍
SESSION 不重叠 由数据活跃度决定 用户会话分析、行为路径 依赖 watermark,乱序场景需谨慎

3.6 知识体系总览

下面这张图,我把三种窗口的核心逻辑画出来了。你可以对照着理解:

Flink SQL 窗口函数知识体系 窗口函数 TUMBLE 滚动窗口 HOP 滑动窗口 SESSION 会话窗口 特性 窗口不重叠 数据只属于一个窗口 特性 窗口可重叠 数据属于多个窗口 特性 窗口由数据活跃度决定 依赖 watermark 典型场景: 每 5 分钟统计销量 典型场景: 过去 10 分钟 UV,每 1 分钟刷新 典型场景: 用户会话分析 核心:GROUP BY + 窗口函数 = DWS 层实时聚合的基石

3.7 实战建议

最后,给你几个我在项目中沉淀下来的建议:

  • 优先用 TUMBLE:如果业务方没有特殊要求,TUMBLE 窗口最简单、性能最好。HOP 和 SESSION 都有额外的状态开销。
  • 事件时间 vs 处理时间:能选事件时间就选事件时间,虽然要处理乱序和延迟,但结果更准确。处理时间只适合对精度要求不高的场景。
  • 窗口大小别太小:1 秒窗口在理论上是可行的,但实际生产中,窗口太小会导致频繁触发 checkpoint,影响吞吐。我一般建议窗口最小 30 秒。
  • 注意状态大小:SESSION 窗口的状态管理最复杂,因为窗口边界不确定。如果用户量大,建议配合 state.ttl 配置,防止状态无限膨胀。