1. 从一次被吐槽的统计需求说起:为什么要学窗口函数
先讲个真实经历。有次业务方扔给我一个需求,描述特别简单:“把每个部门的员工按薪资排名,还要保留所有员工信息”。我当时第一反应是用GROUP BY,结果写完就发现不对劲——GROUP BY一做分组,每个部门只剩下一行聚合结果,员工明细全被压扁了。为了保留明细又拿到排名,我只能用相关子查询或者临时表,一条SQL写得又臭又长,跑起来还慢,被同事笑称“用生命在写SQL”。
后来我认真啃了一遍SQL窗口函数,回头看当时写的那些嵌套子查询,简直想穿越回去给自己一巴掌。窗口函数本质上就是在不合并行的前提下,对每一行做一次“局部计算”,排名、累计、移动平均、同环比、分组TopN,都是它最擅长的领域。你只要掌握一个OVER()子句,就能在一个查询里同时拿到明细数据和统计结果,改写过去那些“做完一个聚合再JOIN回去”的绕路操作。
这篇内容适合谁?如果你是数据分析师、数据工程师,或者日常工作中要写大量复杂统计SQL的开发,窗口函数会成为你工具箱里最趁手的家伙。文章会从底层逻辑讲到语法框架,再配合我做过的真实案例,把排名、累计、占比、偏移计算这些高频场景逐个拆开。每个代码块都可以直接拿去改改字段名就能用,不玩虚的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 窗口函数是什么:一张图搞懂“窗口”与“分组”的本质区别
2.1 GROUP BY是折叠,窗口函数是切片
很多人学窗口函数时卡住的第一个点,就是搞不懂它和GROUP BY到底差在哪。我用一个特别生活化的类比来解释:
GROUP BY像把一锅汤倒进浓缩机,压出一小碗精华——你只看到每个分组的汇总数字,原始行全没了。窗口函数则像给每一行戴上一副“局部放大镜”,你依然能看到所有细节,只是每一行都额外带上了自己所属那个组的统计信息。
举例子。有一张销售记录表:
sql复制CREATE TABLE sales_records (
record_id INT,
department VARCHAR(50),
sales_person VARCHAR(50),
sales_date DATE,
amount DECIMAL(10,2)
);
如果按部门算总销售额,GROUP BY的结果是每个部门一行;而窗口函数的写法是:
sql复制SELECT
department,
sales_person,
amount,
SUM(amount) OVER(PARTITION BY department) AS dept_total
FROM sales_records;
结果每一行都在,旁边多了一列“本部门总额”。这个效果GROUP BY做不到,子查询或自联结虽然能做,但SQL又臭又长,性能还不一定好。窗口函数最核心的价值,就是把“每行自己的值”和“所属分组的状态”放在同一个查询结果里,让后续计算不用反复JOIN。
2.2 OVER()子句的三大组成:PARTITION BY、ORDER BY、ROWS/RANGE
窗口函数的核心语法就是函数名() OVER(窗口定义)。窗口定义有三大块,理解它们就理解了窗口函数八成内容。
PARTITION BY:负责把数据切成多个“小组”,逻辑上和GROUP BY的分组概念很像,但只控制窗口范围,不折叠行。不写的时候,整张表就是一个大窗口。
ORDER BY:负责定义窗口内的排序规则。它决定了排名类函数怎么排,也决定了累计类函数的计算方向——是“从分区第一行累加到我这一行”,还是“从分区末尾倒着累加”。
ROWS/RANGE:负责进一步约束窗口的“物理范围”或“逻辑范围”。比如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示向前数两行,RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW表示按日期范围向前推一周。这是移动平均、滚动统计等场景的关键参数。
三个部件可以自由组合。光写PARTITION BY没写ORDER BY,窗口就是“整个分区”。两个都写,窗口默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是常说的“从分区起点到当前行”。这个默认行为很关键,很多人在累计场景里漏了ORDER BY,结果SUM变成全分区求和,和预期完全不符。
