1. 为什么说窗口函数能让明细和汇总同时留在结果里
1.1 一段最常见的SQL经历:想到排名第一反应是group by和子查询
我最早开始做数据相关工作时,写SQL几乎全靠 GROUP BY + 子查询硬扛。当时接到的需求很常见:把每个部门里工资最高的员工捞出来。第一版写法大概是先 GROUP BY department_id 算出每个部门 max(salary),再把这个结果和员工表做 JOIN,最后才能把对应的员工姓名带出来。
这种写法能跑通,但代码会膨胀得很厉害。如果你把需求改成“每个部门工资前三名”,就得在 JOIN 里带上排名逻辑,或者用相关子查询逐行判断“比我工资高的人有多少个”。相关子查询写法如下:
sql复制SELECT e1.employee_id, e1.employee_name, e1.department_id, e1.salary
FROM employee e1
WHERE (
SELECT COUNT(*)
FROM employee e2
WHERE e2.department_id = e1.department_id
AND e2.salary > e1.salary
) < 3;
这种写法的缺点是相当明显的:它要对 employee 表的每一行执行一次子查询,数据量稍微上来一点,执行计划就比较难看。而且业务含义藏得很深,读代码的人需要想半天才明白这是在找部门前三。
后来换用 SQL窗口函数,同样需求只需要在 SELECT 里加一列:
sql复制SELECT employee_id, employee_name, department_id, salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employee;
再把结果包一层 WHERE rn <= 3,完事。语句从“子查询嵌套子查询”变成了一条主查询加一个过滤条件。这个体验上的差距,做过报表的人应该都能理解。
1.2 窗口函数与GROUP BY的本质区别:不折叠明细
很多人刚接触窗口函数时的困惑点在于:它和 GROUP BY 有什么区别?为什么 SELECT 里既可以出现原始行字段,又可以出现聚合结果?
GROUP BY 的逻辑是“分组后折叠”,每个分组只会保留一行,明细数据直接消失。比如你按部门求总工资,GROUP BY department_id 之后,部门下面的员工明细就看不到了。如果你想在明细旁边看到每个部门的汇总值,以前的常规做法是用一个子查询先聚合再 JOIN 回来。
窗口函数不一样。它做的是“分组后计算,但是不折叠明细”。每一行原始数据都还在,只是在行旁边多出了聚合结果。比如:
sql复制SELECT employee_id, employee_name, department_id, salary,
SUM(salary) OVER (PARTITION BY department_id) AS dept_total_salary
FROM employee;
这条 SQL 返回的行数和 employee 表原行数完全一致。每个员工行旁边都带上了自己部门的工资总额。你可以直接拿 salary / dept_total_salary 算员工工资占部门总工资的比例,整个操作只需要一个语句,不需要 JOIN 自己,也不需要用临时表中间过渡。
我经常用一句话给新手讲这个区别:GROUP BY 是把很多行压缩成一行,窗口函数是让每行都看见自己所在的组整体长什么样。这个“看见整体”的能力,才是后面各种排名、累计、移动平均、同比环比能够简化写法的根本原因。
1.3 OVER语法中最值得先记住的三大要素
窗口函数的骨架是 OVER(),里面可以放三个东西:PARTITION BY、ORDER BY、滑窗范围。理解它们的分工,比死记函数名重要。
PARTITION BY 负责把数据切分成若干分区,作用上可以理解为“分组”,但是不合并行。不写 PARTITION BY,就是整张表作为一个分区处理。
ORDER BY 决定窗口内数据的排序。注意它和普通 ORDER BY 有一个很大的差异:窗口里的 ORDER BY 不只是控制输出顺序,还会影响窗口的计算范围。
滑窗范围是很多人忽略的部分。当你在窗口函数里写了 ORDER BY,但没有指定窗口边界时,数据库默认的行为是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是从分区起点到当前行。这种默认边界在聚合类窗口函数里特别容易引起误解,后面我会专门展开讲。
排在最前面的典型窗口函数大概分三类:排名函数(ROW_NUMBER、RANK、DENSE_RANK、NTILE)、聚合函数加 OVER(SUM、AVG、COUNT、MIN、MAX 的窗口用法)、偏移函数(LAG、LEAD)。下面几章我拆开讲,每一类都配上实际业务场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 排名类窗口函数实战:ROW_NUMBER、RANK、DENSE_RANK、NTILE应该怎么选
2.1 四类排名函数的实际差异,比想象中更重要
先看结果差异。假设有一张成绩表,里面的分数为 98、95、95、90、88,如果用不同的排名函数,结果分别如下:
| 分数 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 98 | 1 | 1 | 1 |
| 95 | 2 | 2 | 2 |
| 95 | 3 | 2 | 2 |
| 90 | 4 | 4 | 3 |
| 88 | 5 | 5 | 4 |
三者的核心区别在于处理“并列值”的
