1. OVER 聚合和 GROUP BY 聚合到底有什么不一样?
在 Flink SQL 里,最常见的聚合其实有两大类:
-
GROUP BY 聚合
- 一组输入行 → 一行聚合结果
- 典型场景:按用户汇总、按订单分组统计
-
OVER 聚合(窗口函数聚合)
- 一行输入 → 一行输出 + 一个"附加指标"
- 每一行都能得到一个"基于窗口的统计值"
简单说:
GROUP BY:压缩行数(很多行 → 一行)OVER:保留行数(每行都保留,只是在行上多了一些"统计字段")
举个简单例子:
"我想知道每一条订单,过去一小时内同一个商品的总成交金额是多少?"
用 OVER 写起来刚好就很自然:
sql
SELECT
order_id,
order_time,
product,
amount,
SUM(amount) OVER (
PARTITION BY product
ORDER BY order_time
RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
) AS one_hour_prod_amount_sum
FROM Orders;
这里:
- 每一行订单仍然只对应一行结果;
- 但会多出一个字段
one_hour_prod_amount_sum; - 表示:这条订单的视角下,回顾过去 1 小时该商品的累计金额。
你可以把它理解为一种"按时间滑动的明细 + 统计"组合。
2. OVER 的基本语法:PARTITION、ORDER、RANGE / ROWS
一个典型的 OVER 聚合写法长这样:
sql
agg_func(agg_col) OVER (
[PARTITION BY col1, col2, ...]
ORDER BY time_col | non_time_col
RANGE ... | ROWS ...
)
几个关键点:
-
ORDER BY 是必须的
因为 OVER 是"在一段有顺序的行范围上做聚合",而表本身是无序的,所以必须指定一个排序字段。
-
流式查询中,ORDER BY 有限制:
- 只能按时间属性升序 或非时间字段升序;
- 不支持各种复杂排序组合。
-
PARTITION BY的语义和 GROUP BY 类似:- 在每个 partition 内单独做窗口聚合;
- 常规用法:按用户、按商品、按租户分区,互不影响。
-
RANGE / ROWS 是窗口范围的两种定义方式:
RANGE:按值(时间或非时间字段)定义范围;ROWS:按行数定义范围。
3. RANGE:按时间 / 值做"区间窗口"
3.1 时间 RANGE:过去 30 分钟的滑动窗口
最常见的写法是基于时间的 RANGE,例如:
sql
SUM(amount) OVER (
PARTITION BY product
ORDER BY order_time
RANGE BETWEEN INTERVAL '30' MINUTE PRECEDING AND CURRENT ROW
)
含义:
- 对当前行而言,把"当前时间之前 30 分钟内的所有行 + 当前行"全部纳入窗口;
- 然后在这个窗口上做
SUM(amount)。
每一行的窗口都不一样,是一个移动的时间窗口。
适用场景:
- 实时看某个用户最近 30 分钟的活跃度;
- 实时看某个商品最近 1 小时的销量;
- 实时看某个账号近 5 分钟的转账金额总和,用于风控。
3.2 非时间 RANGE:按排序键做"从头到当前"的累积
对于非时间字段,比如自增 ID 或某个数值,也可以用 RANGE:
sql
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
意味着:
- 从当前 partition 内最早的那一行,
- 一直到当前行,
- 都纳入窗口。
典型用法就是做累计值,比如"从用户注册到当前为止的总消费"。
4. ROWS:按行数做"最近 N 行"的滑动窗口
ROWS 是完全按行数来定义窗口范围:
sql
ROWS BETWEEN 10 PRECEDING AND CURRENT ROW
语义:
- 当前行之前的 10 行;
- 再加上当前行;
- 总共 11 行作为一个窗口。
特点:
- 不看时间,只看"相对行数";
- 适合那种"最近 N 条记录"的统计。
典型场景:
- 股票/行情:最近 5 根 K 线的平均价格;
- 日志分析:最近 100 条日志中 error 比例。
5. 流式查询中的一个限制:多个 OVER 必须用相同窗口
文档里有一个容易忽略的限制:
在流式查询中,如果一个
SELECT里有多个 OVER 聚合,所有 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
ORDER BY order_time
RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
);
但如果你这样写:
sql
SUM(amount) OVER (RANGE 1 HOUR ...)
AVG(amount) OVER (RANGE 2 HOUR ...)
在流模式下就不被支持,因为目前运行时代价较高,Flink 要求这些 OVER 聚合共享同一个窗口定义,才能共享状态 / 运算。
所以实战中的建议是:
- 多个 OVER 聚合 → 尽量抽到同一个
WINDOW子句里复用; - 若确实需要不同窗口,通常考虑拆成多个算子 / 多个 SQL。
6. WINDOW 子句:让 SQL 更干净、更可复用
当你有多个 OVER 聚合时,把窗口定义写在 WINDOW 里会清爽很多:
sql
SELECT
order_id,
order_time,
amount,
SUM(amount) OVER w AS sum_amount,
AVG(amount) OVER w AS avg_amount
FROM Orders
WINDOW w AS (
PARTITION BY product
ORDER BY order_time
RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
);
好处有三个:
-
语义更清晰
- SELECT 里只看得到"我用哪个窗口 w",
- 窗口如何定义单独在下面声明。
-
便于修改
- 窗口范围改成 2 小时,只用改一处
WINDOW w。
- 窗口范围改成 2 小时,只用改一处
-
满足流式多 OVER 同窗的限制
- 所有 OVER 都挂在
w上,自然是同一个窗口定义。
- 所有 OVER 都挂在
7. 什么时候该用 OVER,而不是 GROUP BY / Window TVF?
一个简单的经验法则:
- 只要你还想保留明细行,同时又想附加"基于窗口的统计值",就适合用 OVER;
- 如果你只需要"窗口级别的汇总结果",那用 Window TVF + GROUP BY 更合适。
对比一下几个典型需求:
-
需求 A:每 10 分钟统计一次某商品的销售额
- 用
TUMBLE(TABLE ..., INTERVAL '10' MINUTES) GROUP BY window_start, window_end更自然。
- 用
-
需求 B:在订单明细表中,想看到每条订单在过去 1 小时内的累计金额
- 用
SUM(amount) OVER (PARTITION BY product ORDER BY order_time RANGE 1 HOUR)非常契合。
- 用
-
需求 C:表格中每一行都显示"截止当前为止累计值"和"最近 10 行平均值"
- OVER + RANGE / ROWS 的典型使用场景。
在流式业务里,OVER 很适合做:
- 实时看板上的"当前订单 + 最近 N 分钟统计";
- 明细列表页里的"当前用户历史累计";
- 需要点状视角(行)+ 区间视角(窗口)的混合场景。
8. 小结
这一节的重点可以压缩成 3 句话:
- OVER 聚合不会减少行数,只是给每一行多算一个基于窗口的统计值,适合"明细 + 统计"的场景;
- 它依赖
PARTITION BY+ORDER BY+RANGE/ROWS定义窗口,在流式场景下多个 OVER 必须用同一个窗口; - 对只关心窗口级汇总的场景,还是优先考虑 Window TVF Aggregation;对需要在每一行旁边展示"过去一段时间统计"的场景,用 OVER 会很优雅。