一、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 小时, 当前时间] 区间内的所有行;
- 对这些行的
amount做SUM。
适用场景:
- 实时看"最近 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:严格排名,不考虑并列textA: 1 B: 2 C: 3 D: 4 -
RANK:同分同名次,会"跳名次"textA: 1 B: 2 C: 2 D: 4 (中间 3 被空出来) -
DENSE_RANK:同分同名次,不跳名次textA: 1 B: 2 C: 2 D: 3
Flink SQL 里经常用于:
- 分区内排序后选 TopN;
- 对多版本数据按时间排序后选最新一条(配合
ROW_NUMBER = 1)。
六、模式五:基于窗口函数的 TopN 场景
TopN 是窗口函数最典型的使用场景之一。简单分两类:
- 全局 TopN:比如全站销量前 10 的商品;
- 分组 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 的窗口函数做了一次"模式化拆解":
-
OVER 聚合不会压缩行数,而是在明细行上附加一个"窗口统计值",是"明细 + 统计"的天然解法。
-
针对常见需求,总结了几类常用模式:
- 按时间的滑动累计(RANGE + 时间)
- 最近 N 行统计(ROWS)
- 从头到当前的累积值(UNBOUNDED PRECEDING)
- 排名类窗口函数:
ROW_NUMBER / RANK / DENSE_RANK - 基于窗口函数的分组 TopN / 全局 TopN
- 明细去重 / 最新版本抽取
-
在流式语义下,要重点关注:
- 状态大小 & 状态 TTL;
- 多个 OVER 共享窗口定义的限制;
- 时间属性与水位线对结果的影响。