Flink SQL 窗口函数从 OVER 到 TopN 的完整套路

一、OVER 窗口函数快速回顾

先用一个统一的例子。假设有订单表:

sql 复制代码
CREATE TABLE Orders (
  order_id    BIGINT,
  user_id     BIGINT,
  product_id  STRING,
  amount      DOUBLE,
  order_time  TIMESTAMP(3),
  WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (...);

一个典型的 OVER 聚合语法大致长这样:

sql 复制代码
SELECT
  order_id,
  user_id,
  product_id,
  amount,
  -- 在当前行之前一小时内,该商品的累计金额
  SUM(amount) OVER (
    PARTITION BY product_id
    ORDER BY order_time
    RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
  ) AS one_hour_prod_amount
FROM Orders;

核心要素:

  • PARTITION BY:按什么维度隔离计算(比如按用户、按商品、按租户);
  • ORDER BY:在分区内用什么字段做时间 / 顺序排序;
  • RANGE BETWEEN ...:以值(通常是时间)定义窗口范围;
  • ROWS BETWEEN ...:以"行数"定义窗口范围。

和 GROUP BY 最大的区别:

  • GROUP BY多行 → 一行(压缩行数)
  • OVER一行 → 一行 + 统计字段(不压缩行数)

所以当你想要"明细 + 统计"同时输出时,OVER 是天然解法。

二、模式一:按时间做滑动累计(RANGE + 时间)

最常见的需求之一:

"对每条订单,计算最近 1 小时内该用户/商品的总成交金额。"

SQL:

sql 复制代码
SELECT
  order_id,
  user_id,
  product_id,
  order_time,
  amount,
  -- 最近一小时该商品的成交金额
  SUM(amount) OVER (
    PARTITION BY product_id
    ORDER BY order_time
    RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
  ) AS one_hour_prod_amount
FROM Orders;

含义拆解:

  • product_id 分区:不同商品之间互不影响;

  • 在每个分区内,按 order_time 升序;

  • 对于每一行,以当前时间为"锚点":

    • 查找 [当前时间 - 1 小时, 当前时间] 区间内的所有行;
    • 对这些行的 amountSUM

适用场景:

  • 实时看"最近 30 分钟 / 1 小时 / 24 小时"的指标;
  • 实时 DAU、最近一小时活跃用户行为强度;
  • 近 N 分钟内的充值 / 消费金额统计。

相比 Window TVF 的 TUMBLE/HOP:

  • Window TVF:以"窗口"为单位输出一行,适合做报表 / 汇总;
  • OVER RANGE:以"行"为单位输出一行,适合在明细里附带一个滑动统计字段。

三、模式二:按行数做"最近 N 条"的滑动窗口(ROWS)

有些场景不在意时间,而只在意"最近 N 条数据"的统计,比如:

"对每条行情,计算最近 10 条 tick 的平均价格。"

假设行情表:

sql 复制代码
CREATE TABLE Ticks (
  symbol     STRING,
  ts         TIMESTAMP(3),
  price      DOUBLE,
  WATERMARK FOR ts AS ts - INTERVAL '1' SECOND
) WITH (...);

SQL:

sql 复制代码
SELECT
  symbol,
  ts,
  price,
  AVG(price) OVER (
    PARTITION BY symbol
    ORDER BY ts
    ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
  ) AS last_10_avg_price
FROM Ticks;

解释:

  • ROWS BETWEEN 9 PRECEDING AND CURRENT ROW

    • 当前行之前 9 行 + 当前行;
    • 总共 10 行的数据做 AVG

适用场景:

  • 最近 N 条日志的错误比例;
  • 最近 N 条交易的平均价格 / 最大波动;
  • 机器最近 N 采样点的平均 CPU 使用率。

和 RANGE 的区别:

  • RANGE:按"值"定义范围(通常是时间),窗口内行数不固定;
  • ROWS:按"行数"定义范围,不看实际时间跨度。

四、模式三:从头到当前的"运行累计值"(UNBOUNDED PRECEDING)

需求:

"我想看到每条订单所在用户,从注册到当前为止的累计消费金额。"

SQL:

sql 复制代码
SELECT
  order_id,
  user_id,
  order_time,
  amount,
  SUM(amount) OVER (
    PARTITION BY user_id
    ORDER BY order_time
    RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS user_lifetime_amount
FROM Orders;

这里的关键是:

sql 复制代码
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

代表:

  • 从该分区内最早的一条记录开始;
  • 到当前行;
  • 全部纳入窗口。

这就是典型的 "running total" / "累积值" 模式。

适用场景:

  • 用户生命周期累计消费;
  • 用户累计登录次数 / 累计任务完成次数;
  • 账号累计转账金额(风控场景很常见)。

五、模式四:排名窗口函数 ROW_NUMBER / RANK / DENSE_RANK

除了聚合函数(SUM/AVG/MAX/MIN/COUNT)之外,Flink SQL 也支持常见的排名类窗口函数,比如:

  • ROW_NUMBER()
  • RANK()
  • DENSE_RANK()

这些函数通常和 OVER 搭配,用于:

  • 明细去重(只保留某个"最新/最早"版本);
  • TopN / 分组 TopN;
  • 需要"排序 + 打标"的场景。

5.1 三者的区别(复习一下)

假设有这样一组数据(按得分从高到低排序):

text 复制代码
user  score
A     100
B     90
C     90
D     80

如果我们写:

sql 复制代码
ROW_NUMBER() OVER (ORDER BY score DESC)
RANK()       OVER (ORDER BY score DESC)
DENSE_RANK() OVER (ORDER BY score DESC)

则:

  • ROW_NUMBER:严格排名,不考虑并列

    text 复制代码
    A: 1
    B: 2
    C: 3
    D: 4
  • RANK:同分同名次,会"跳名次"

    text 复制代码
    A: 1
    B: 2
    C: 2
    D: 4  (中间 3 被空出来)
  • DENSE_RANK:同分同名次,不跳名次

    text 复制代码
    A: 1
    B: 2
    C: 2
    D: 3

Flink SQL 里经常用于:

  • 分区内排序后选 TopN;
  • 对多版本数据按时间排序后选最新一条(配合 ROW_NUMBER = 1)。

六、模式五:基于窗口函数的 TopN 场景

TopN 是窗口函数最典型的使用场景之一。简单分两类:

  1. 全局 TopN:比如全站销量前 10 的商品;
  2. 分组 TopN:比如每个品类下销量前 3 的商品。

6.1 全局 TopN

假设我们已经通过窗口聚合算出了每小时各商品的销量:

sql 复制代码
CREATE VIEW hourly_product_sales AS
SELECT
  window_start,
  window_end,
  product_id,
  SUM(amount) AS total_amount
FROM TUMBLE(TABLE Orders, DESCRIPTOR(order_time), INTERVAL '1' HOUR)
GROUP BY window_start, window_end, product_id;

现在想要每小时全局销量 Top3 的商品,可以用 ROW_NUMBER

sql 复制代码
SELECT *
FROM (
  SELECT
    window_start,
    window_end,
    product_id,
    total_amount,
    ROW_NUMBER() OVER (
      PARTITION BY window_start, window_end
      ORDER BY total_amount DESC
    ) AS rn
  FROM hourly_product_sales
)
WHERE rn <= 3;

说明:

  • 这里 PARTITION BY window_start, window_end

    • 每个小时作为一个分区;
    • 在每个小时内部按 total_amount 排序;
  • 再用 ROW_NUMBER 标号,取前 3 即可。

6.2 分组 TopN:比如每个品类 Top3

假设 Orders 表里多了 category_id

sql 复制代码
CREATE VIEW hourly_category_product_sales AS
SELECT
  window_start,
  window_end,
  category_id,
  product_id,
  SUM(amount) AS total_amount
FROM TUMBLE(TABLE Orders, DESCRIPTOR(order_time), INTERVAL '1' HOUR)
GROUP BY window_start, window_end, category_id, product_id;

每个品类在每个小时内的 Top3:

sql 复制代码
SELECT *
FROM (
  SELECT
    window_start,
    window_end,
    category_id,
    product_id,
    total_amount,
    ROW_NUMBER() OVER (
      PARTITION BY window_start, window_end, category_id
      ORDER BY total_amount DESC
    ) AS rn
  FROM hourly_category_product_sales
)
WHERE rn <= 3;

模式很统一:

Window TVF 做窗口聚合 → View → 窗口结果上再用窗口函数打 TopN 标号。

这也是官方推荐的"窗口 TopN"玩法。

七、模式六:明细去重 / 最新版本抽取

有时候你并不是要 TopN,而是要从多版本记录里抽出一个"最新版本"或"最早版本",典型场景:

  • 订单表中同一个 order_id 有多次更新记录,只保留最后一次;
  • 用户属性表每次变更写一条新记录,只保留最新那条作为当前状态。

这时可以结合 ROW_NUMBER()

sql 复制代码
SELECT *
FROM (
  SELECT
    order_id,
    user_id,
    product_id,
    status,
    update_time,
    ROW_NUMBER() OVER (
      PARTITION BY order_id
      ORDER BY update_time DESC
    ) AS rn
  FROM OrderChanges
)
WHERE rn = 1;

解释:

  • order_id 分区,在分区内按 update_time 降序(最新在前);
  • 对每个 order_id,最新的一条记录 rn = 1
  • WHERE 里只保留 rn = 1 即可。

这个模式是非常通用的:

  • 只要你能确定排序表达"哪个版本更新";
  • 就可以用 ROW_NUMBER = 1 选出"当前版本"。

八、在流式语义下,OVER 窗口需要注意什么?

8.1 状态与资源

OVER 窗口本质上就是一个 per-row 的窗口聚合,会在流式场景中持有状态,例如:

  • RANGE 需要存"窗口内行"的聚合状态;
  • ROWS 需要知道前 N 行的信息。

所以:

  • 高并发 + 高基数分区 + 大范围窗口 → 状态可能非常大;

  • 要结合业务设置合理的:

    • 窗口范围(不要太大);
    • 状态 TTL 或其他清理策略。

8.2 多个 OVER 必须共享同一窗口定义(流模式限制)

在流模式下有个重要限制:

一个 SELECT 里多个 OVER 聚合,窗口定义必须完全一致

例子(✅ 合法):

sql 复制代码
SELECT
  order_id,
  SUM(amount) OVER w AS sum_amount,
  AVG(amount) OVER w AS avg_amount
FROM Orders
WINDOW w AS (
  PARTITION BY product_id
  ORDER BY order_time
  RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
);

例子(❌ 流模式下不支持):

sql 复制代码
SELECT
  SUM(amount) OVER (
    PARTITION BY product_id
    ORDER BY order_time
    RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
  ) AS sum_1h,
  SUM(amount) OVER (
    PARTITION BY product_id
    ORDER BY order_time
    RANGE BETWEEN INTERVAL '2' HOUR PRECEDING AND CURRENT ROW
  ) AS sum_2h
FROM Orders;

解决方案:

  • 拆成两个 job / 两条 SQL;
  • 或者用 Window TVF 做不同窗口范围的聚合。

8.3 时间属性与水位线(Watermark)

对基于时间的 RANGE OVER 窗口,水位线会影响:

  • 窗口何时认为"可以稳定输出结果";
  • 如何处理迟到数据。

如果迟到数据很多,滑动窗口的结果也会被持续修正。

实践中要注意:

  • 保证水位线策略与业务延迟匹配;
  • 尤其是风控、计费等场景,是否能接受迟到更新。

九、OVER vs Window TVF:如何选择?

可以用一个简单对比表来记:

特性 OVER 窗口 Window TVF(TUMBLE/HOP/...)
输出粒度 行级(明细不减少) 窗口级(每窗口 1 行或多行)
典型用途 明细 + 统计、TopN、去重、排名 报表、窗口汇总、窗口 TopN 前置聚合
是否容易做"完整报表" 不自然 非常自然
多个不同窗口在同一条 SQL 中 流模式下受限 可以自由定义多个窗口视图
常见函数 SUM/AVG/MAX/MIN/COUNT/ROW_NUMBER/RANK... 各种聚合 + 再接 TopN

简单经验:

  • 想要"报表类窗口统计" → Window TVF
  • 想要"行级统计 / 明细附加字段 / 排名 / TopN / 去重" → OVER

十、总结

这一篇我们从业务视角,把 Flink SQL 的窗口函数做了一次"模式化拆解":

  1. OVER 聚合不会压缩行数,而是在明细行上附加一个"窗口统计值",是"明细 + 统计"的天然解法。

  2. 针对常见需求,总结了几类常用模式:

    • 按时间的滑动累计(RANGE + 时间)
    • 最近 N 行统计(ROWS)
    • 从头到当前的累积值(UNBOUNDED PRECEDING)
    • 排名类窗口函数:ROW_NUMBER / RANK / DENSE_RANK
    • 基于窗口函数的分组 TopN / 全局 TopN
    • 明细去重 / 最新版本抽取
  3. 在流式语义下,要重点关注:

    • 状态大小 & 状态 TTL;
    • 多个 OVER 共享窗口定义的限制;
    • 时间属性与水位线对结果的影响。
相关推荐
她说彩礼65万1 小时前
C# ConcurrentDictionary详解
java·服务器·c#
Han.miracle1 小时前
Maven 基础与 Spring Boot 入门:环境搭建、项目开发及常见问题排查
java·spring boot·后端
特拉熊1 小时前
23种设计模式之桥接模式
java·架构
半瓶榴莲奶^_^1 小时前
后端Web进阶(AOP)
java·开发语言
麻辣烫不加辣1 小时前
跑批调额系统说明文档
java·后端
一只乔哇噻1 小时前
java后端工程师+AI大模型开发进修ing(研一版‖day61)
java·开发语言·学习·算法·语言模型
拾忆,想起1 小时前
Dubbo服务降级全攻略:构建韧性微服务系统的守护盾
java·前端·网络·微服务·架构·dubbo
蝈蝈(GuoGuo)1 小时前
FireDAC][Phys][ODBC][SQLSRV32.DLL] SQL_NO_DATA FDquery
数据库·sql·oracle
zlpzlpzyd1 小时前
jetbrains系工具idea和webstorm默认编辑器设置
java·intellij-idea·webstorm