很多人第一次接触 OVER 窗口函数,都是为了解决这类典型的数据分析需求:
- 计算每个月的累计用户数。
- 统计截止当月的累计收入。
- 生成按时间滚动的各项指标。
SQL 语句写是写出来了,可一旦执行结果和预期不符,调试过程往往让人怀疑人生。其实,问题通常不在于 SQL 语法技巧本身,而在于一个更根本的困惑:没有真正理解 OVER 到底是在对“哪一批”数据进行计算。
本文将通过一套极简、脱敏、专为讲解而设计的 SQL 示例,帮你把 OVER 窗口函数的计算逻辑彻底捋清楚。
一、一个精心设计的简化场景
假设我们有一张订单表 order_table,它记录了每天产生的订单金额:
order_table
| 字段名 |
含义 |
| order_date |
订单日期 |
| order_amount |
订单金额 |
我们的分析目标是:
统计每个月的订单总额,并同时计算出“截至当月月底”的累计订单总金额。
请特别注意这个关键词:截至当月月底(累计)。这意味着计算需要带上历史数据。
二、第一步:先算清“每个月的新增额”(不使用 OVER)
在进行累计计算之前,我们得先把基础数据——每个月的独立销售额——汇总清楚。
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month_time,
SUM(order_amount) AS month_amount
FROM order_table
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month_time;
这一步的运算非常纯粹:
- 不涉及任何历史数据。
- 不进行任何累计计算。
- 仅仅是把“散落在每一天的数据”按月份聚合,压缩成“每个月一行”的汇总结果。
查询结果大致如下:
| month_time |
month_amount |
| 2025-01 |
10000 |
| 2025-02 |
15000 |
| 2025-03 |
12000 |
三、关键问题:如何得到“截至当月的累计值”?
面对这个需求,很多人的第一反应是:能不能在 GROUP BY 分组的时候,“顺手”把累计值也算出来?
答案是:不行。
因为 GROUP BY 的核心职责只有一个:把多行数据根据分组键聚合成一行。一旦聚合完成,明细行就消失了。
而“累计”这件事的本质是:
站在当前这一行结果的位置,回过头去“看”历史上所有符合条件的数据行。
这种“既需要保留明细行,又需要基于某种顺序访问其他行数据”的能力,正是 窗口函数(Window Function) 存在的意义。
四、OVER 登场:定义你的数据“窗口”
现在,我们把上一步得到的月度汇总数据当作一个子查询,然后在其之上应用 OVER 来进行累计计算。
SELECT
month_time,
month_amount,
SUM(month_amount) OVER (
ORDER BY month_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS total_amount_at_month_end
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month_time,
SUM(order_amount) AS month_amount
FROM order_table
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) t
ORDER BY month_time;
这条 SQL 是理解 OVER 逻辑的最佳切入点。我们来逐部分拆解。
五、逐句拆解:OVER 到底在干什么?
1️⃣ SUM(month_amount)
这部分并非重点,它只是一个普通的聚合函数(如 SUM, AVG, COUNT 等)。真正的魔法在于后面的 OVER() 子句。
2️⃣ ORDER BY month_time
这一句定义了累计的“方向”或“顺序”。
它告诉数据库:“请按照 month_time 从早到晚的顺序来建立累计的逻辑。” 如果没有这个排序,就谈不上“累计”概念。
3️⃣ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
这是整个 OVER 子句的灵魂,决定了“窗口”的范围。
直译过来是:“从(分区的)第一行开始,一直计算到当前行。”
换成业务语言就是:历史所有月份 + 当前月份。
六、用“人脑”模拟窗口的滑动过程
假设我们得到的第一步月度数据如下:
| month_time |
month_amount |
| 2025-01 |
10000 |
| 2025-02 |
15000 |
| 2025-03 |
12000 |
那么,窗口函数在处理每一行时,其“看到”的数据窗口和计算结果分别是:
- 处理
2025-01 这一行时
- 数据窗口 =
[2025-01]
- 累计金额 = 10000
- 处理
2025-02 这一行时
- 数据窗口 =
[2025-01 → 2025-02]
- 累计金额 = 10000 + 15000 = 25000
- 处理
2025-03 这一行时
- 数据窗口 =
[2025-01 → 2025-03]
- 累计金额 = 10000 + 15000 + 12000 = 37000
这个过程完美诠释了“滑动窗口”的直观含义:窗口随着当前处理行的移动,在有序的数据集上向后滑动,并重新划定计算范围。
七、OVER 与 GROUP BY 的核心区别
很多人容易混淆这两者,下表给出了清晰的对比:
| 维度 |
GROUP BY |
OVER (窗口函数) |
| 是否合并输出行 |
是(多行变一行) |
否(保持原行数) |
| 是否保留明细数据 |
否 |
是 |
| 是否能进行累计、排名等计算 |
否 |
是 |
| 计算是否依赖行间顺序 |
否 |
是 |
一句话总结:
GROUP BY 改变了查询结果集的结构(行数减少);而 OVER 不改变结果集结构,它只改变每一行数据在进行计算时所“看到”的数据范围。
八、一个实用变体:实现滚动统计
理解了窗口范围的定义后,我们可以轻松实现各种变体。例如,将窗口范围改为:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
其含义立刻变为:“从当前行往前数2行,直到当前行”,也就是 近3行 的数据。
在按月排序的上下文中,这就实现了“近3个月的滚动销售总额”的计算。
看,SQL的主体结构完全没变,我们仅仅通过修改 OVER() 子句中的窗口范围定义,就实现了截然不同的业务逻辑。这正是窗口函数强大且优雅的地方,也是它被称为“分析型SQL”利器的原因。
九、最终要记住的三个要点
关于 OVER 窗口函数,你只需要深刻理解这三句话:
- 它不是用来分组聚合的,而是用来定义数据观察“视角”或“窗口”的。
ORDER BY 决定了计算进行的逻辑或时间方向(从哪到哪)。
ROWS BETWEEN ... 则精确划定了这个“窗口”的大小,即你能看到多远(多少行)的历史或未来数据。
当你真正在脑中建立起“滑动窗口”的动态画面时,你会发现,无论是累计求和 (SUM)、排名 (RANK、ROW_NUMBER),还是计算同比环比,本质上都是同一套窗口机制在不同场景下的应用。
希望这个从零拆解的示例能帮助你彻底掌握 SQL 窗口函数的精髓。如果你想深入探索更多数据库高级特性或实践案例,欢迎在 云栈社区 与更多开发者交流学习。