它们喜欢先甩给你一串语法:OVER(PARTITION BY ... ORDER BY ...),然后再贴几个例子,最后你记住了 ROW_NUMBER(),却还是不知道什么时候该用、为什么好用。
这一篇不绕弯,直接把 SQL 窗口函数最常用、最容易卡壳的部分讲透。
一、窗口函数到底是干嘛的?
先说人话版定义:
窗口函数 = 在“不压缩结果行数”的前提下,按某个范围对数据做计算。
这是它和 GROUP BY 最大的区别。
1. GROUP BY 会把多行压成一行
比如你按部门统计员工人数:
SELECT department, COUNT(*) AS emp_count FROM employees GROUP BY department;
结果是每个部门只剩一行。
这很正常,但问题来了:
如果你既想保留每个员工这一行,又想知道他所在部门有多少人,怎么办?
这时候 GROUP BY 就不顺手了,窗口函数就是来干这个的。
2. 窗口函数保留明细,同时做聚合
SELECT employee_id, name, department, salary, COUNT(*) OVER(PARTITION BY department) AS dept_emp_count FROM employees;
你会得到这样的效果:
• 每个员工仍然保留一行
• 同时多出一个字段:这个员工所在部门的人数
这就是窗口函数最核心的价值:
明细不丢,统计也有。
这四个字,值千金。
二、先把最关键的语法拆开
窗口函数常见写法长这样:
函数名() OVER( PARTITION BY ... ORDER BY ... )
别一看括号多就头大,其实就三层意思。
1. OVER():声明“我要按窗口算”
没有 OVER(),很多函数就是普通聚合函数。
比如:
SUM(salary)
这是普通聚合。
而:
SUM(salary) OVER()
这是窗口函数,表示:
在整个结果集这个窗口里,计算总工资,但不要把行合并掉。
2. PARTITION BY:先分组,但不折叠行
它很像 GROUP BY,但只是在逻辑上分区,不会把数据压缩。
SUM(salary) OVER(PARTITION BY department)
意思是:
• 以部门为单位划分窗口
• 在每个部门窗口内计算工资总和
• 结果回填到该部门的每一行
你可以把它理解成:
是“分组统计”,但保留每条明细。
3. ORDER BY:定义窗口内的顺序
很多窗口函数都和“顺序”强相关,比如:
• 排名
• 累计值
• 上一行 / 下一行
这时候就必须指定顺序:
ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC)
意思是:
• 先按部门分区
• 再在每个部门内部,按工资从高到低排序
• 给每个人编号
三、先学会最常用的 3 个排名函数
这是窗口函数入门里最实用的一组。
假设有一张成绩表:
student_scores ------------------------- student_name | class | score 张三 | A | 95 李四 | A | 95 王五 | A | 90 赵六 | A | 88
1. ROW_NUMBER():强行排唯一名次
SELECT student_name, class, score, ROW_NUMBER() OVER(PARTITION BY class ORDER BY score DESC) AS rn FROM student_scores;
结果可能是:
• 张三 1
• 李四 2
• 王五 3
• 赵六 4
即使张三和李四同分,ROW_NUMBER() 也会硬分先后。
适用场景:
• 每组取 Top 1 / Top N
• 去重保留一条
• 分页编号
2. RANK():并列占同名,后面跳号
SELECT student_name, class, score, RANK() OVER(PARTITION BY class ORDER BY score DESC) AS rk FROM student_scores;
结果会变成:
• 张三 1
• 李四 1
• 王五 3
• 赵六 4
因为前两个人并列第一,所以第三个人的名次直接变成 3。
适用场景:
• 正常比赛排名
• 允许并列且保留“跳号”语义的榜单
3. DENSE_RANK():并列占同名,但不跳号
SELECT student_name, class, score, DENSE_RANK() OVER(PARTITION BY class ORDER BY score DESC) AS dr FROM student_scores;
结果是:
• 张三 1
• 李四 1
• 王五 2
• 赵六 3
适用场景:
• 分层级
• 分档次
• 想体现并列,但不想出现名次断层
四、SQL 面试和实战都爱考:每组 Top N
这题你应该见过:
查询每个部门工资最高的 3 个人。
很多人第一反应是子查询套子查询,写得像意大利面。
其实窗口函数是标准答案。
SELECT * FROM ( SELECT employee_id, name, department, salary, ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rn FROM employees ) t WHERE rn <= 3;
SELECT sale_date, amount, SUM(amount) OVER(ORDER BY sale_date) AS running_total FROM daily_sales;
这会得到:
• 3 月 1 日:100
• 3 月 2 日:250
• 3 月 3 日:370
• 3 月 4 日:550
这就是累计求和。
如果想按门店分别累计?
SELECT store_id, sale_date, amount, SUM(amount) OVER(PARTITION BY store_id ORDER BY sale_date) AS running_total FROM daily_sales;
意思很简单:
• 每个门店是一个窗口
• 门店内部按日期排序
• 分别计算各自累计值
这类写法在下面场景非常常见:
• 日活 / 周活累计
• 财务累计收入
• 用户增长曲线
• 库存变化趋势
六、LAG 和 LEAD:查上一行、下一行,真的太好用
很多新手写 SQL 时,一遇到“和上一天比较”,就想自连接。
说实话,能写,但丑。
窗口函数里有更直接的:
• LAG():看上一行
• LEAD():看下一行
1. 看上一天销售额
SELECT sale_date, amount, LAG(amount, 1) OVER(ORDER BY sale_date) AS prev_amount FROM daily_sales;
这里:
• LAG(amount, 1) 表示取前 1 行的 amount
• 如果前面没有行,就返回 NULL
2. 直接算日增长
SELECT sale_date, amount, LAG(amount, 1) OVER(ORDER BY sale_date) AS prev_amount, amount - LAG(amount, 1) OVER(ORDER BY sale_date) AS diff_amount FROM daily_sales;
这就能直接算出和上一天相比增减了多少。
3. 再进一步,算增长率
SELECT sale_date, amount, LAG(amount, 1) OVER(ORDER BY sale_date) AS prev_amount, ROUND( (amount - LAG(amount, 1) OVER(ORDER BY sale_date)) / NULLIF(LAG(amount, 1) OVER(ORDER BY sale_date), 0), 2 ) AS growth_rate FROM daily_sales;
这个场景在业务分析里非常常见:
• 环比增长
• 次日留存对比
• 用户行为变化
• 价格波动分析
七、很多人学不会窗口函数,不是因为难,是因为脑子里没画面
你可以用这个心智模型记:
普通聚合函数
像把很多行扔进榨汁机,最后变成一杯。
窗口函数
像你把每一行放在原位,只是给它发一张“统计结果小纸条”。
比如这一行员工数据旁边贴一张:
• 部门平均工资
• 部门排名
• 上一位同事工资
• 截至当前的累计销售额
行没消失,信息却更丰富了。
这就是窗口函数的本质。
八、再讲一个关键点:窗口函数常见组合拳
实战里,你不是只会写一个 ROW_NUMBER() 就完事,更多时候是组合使用。
场景 1:找每个用户最近一次登录
SELECT * FROM ( SELECT user_id, login_time, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_time DESC) AS rn FROM user_login_log ) t WHERE rn = 1;
场景 2:计算每个部门的平均工资,并看员工是否高于部门均值
SELECT employee_id, name, department, salary, AVG(salary) OVER(PARTITION BY department) AS dept_avg_salary, CASE WHEN salary > AVG(salary) OVER(PARTITION BY department) THEN '高于均值' ELSE '不高于均值' END AS salary_level FROM employees;
场景 3:找连续上涨的数据趋势
SELECT sale_date, amount, LAG(amount, 1) OVER(ORDER BY sale_date) AS prev_amount, CASE WHEN amount > LAG(amount, 1) OVER(ORDER BY sale_date) THEN '上涨' WHEN amount < LAG(amount, 1) OVER(ORDER BY sale_date) THEN '下降' ELSE '持平' END AS trend FROM daily_sales;
这些需求,如果不用窗口函数,不是不能做,是会写得更丑、更绕、更难维护。
九、初学者最容易踩的 5 个坑
1. 把 PARTITION BY 当成 GROUP BY
记住:
• GROUP BY:会减少结果行数
• PARTITION BY:不会减少结果行数
别混。
2. 排名函数没写 ORDER BY
像 ROW_NUMBER()、RANK() 这种函数,不写排序基本等于耍流氓。
你不指定顺序,数据库怎么知道谁第一?
3. 取 Top N 时选错函数
• 只要唯一编号:ROW_NUMBER()
• 允许并列且跳号:RANK()
• 允许并列且不跳号:DENSE_RANK()
别无脑都写 ROW_NUMBER()。
4. 忘了窗口函数是在 SELECT 结果阶段计算
所以你通常不能直接在 里过滤窗口函数结果,要包一层子查询或用 QUALIFY(如果数据库支持)。
错误示意:
SELECT name, ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rn FROM employees WHERE rn <= 3;
这在很多数据库里会报错。
正确方式:
SELECT * FROM ( SELECT name, ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS rn FROM employees ) t WHERE rn <= 3;
5. 不理解默认窗口范围
尤其是 SUM()、AVG() 这种带 ORDER BY 的窗口函数,很多数据库会有默认 frame 行为。
你如果发现结果和预期不一致,就要看看是不是该显式写:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
对于初学者来说,这一层先记住一句就够了:
只要你在做累计值,最好知道数据库默认 frame 是怎么处理的。
十、一套最实用的学习路径,别乱学
如果你想真正掌握 SQL 窗口函数,建议按下面顺序学:
第一步:先理解它和 GROUP BY 的区别
如果这一步不清楚,后面都会糊。
第二步:先练 ROW_NUMBER()、RANK()、DENSE_RANK()
因为这是最容易出现在题目和业务里的。
第三步:练累计求和
把 SUM() OVER(ORDER BY ...) 写顺手。
第四步:练 LAG() / LEAD()
这是分析类 SQL 的核心能力之一。
第五步:再去碰 frame、滑动窗口这些高级用法
比如:
• 最近 7 天移动平均
• 最近 3 条记录求均值
• 滚动统计
这些更进阶,但前面没打牢,学它们就是硬啃。
十一、给你一份窗口函数速记表
排名类
• ROW_NUMBER():唯一编号,不管并列
• RANK():并列占同名,后面跳号
• DENSE_RANK():并列占同名,后面不跳号
聚合类
• SUM() OVER(...):窗口求和
• AVG() OVER(...):窗口平均值
• COUNT() OVER(...):窗口计数
• MAX() OVER(...) / MIN() OVER(...):窗口最大最小值
偏移类
• LAG():取前 N 行
• LEAD():取后 N 行
常见结构
-- 分组排名 ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) -- 分组累计 SUM(amount) OVER(PARTITION BY store_id ORDER BY sale_date) -- 取上一行 LAG(amount, 1) OVER(ORDER BY sale_date)