SQL 窗口函数,别再死记硬背了:一篇彻底讲透 OVER / PARTITION BY / ROW_NUMBER
如果你学 SQL 时总觉得窗口函数“看起来懂了,一写就废”,那不是你笨,是很多教程都没讲人话。
它们喜欢先甩给你一串语法:
OVER(PARTITION BY ... ORDER BY ...),然后再贴几个例子,最后你记住了ROW_NUMBER(),却还是不知道什么时候该用、为什么好用。这一篇不绕弯,直接把 SQL 窗口函数最常用、最容易卡壳的部分讲透。
先说人话版定义:
窗口函数 = 在“不压缩结果行数”的前提下,按某个范围对数据做计算。
这是它和 GROUP BY 最大的区别。
GROUP BY 会把多行压成一行比如你按部门统计员工人数:
SELECT department, COUNT(*) AS emp_count
FROM employees
GROUP BY department;结果是每个部门只剩一行。
这很正常,但问题来了:
如果你既想保留每个员工这一行,又想知道他所在部门有多少人,怎么办?
这时候 GROUP BY 就不顺手了,窗口函数就是来干这个的。
SELECT
employee_id,
name,
department,
salary,
COUNT(*) OVER(PARTITION BY department) AS dept_emp_count
FROM employees;你会得到这样的效果:
这就是窗口函数最核心的价值:
明细不丢,统计也有。
这四个字,值千金。
窗口函数常见写法长这样:
函数名() OVER(
PARTITION BY ...
ORDER BY ...
)别一看括号多就头大,其实就三层意思。
OVER():声明“我要按窗口算”没有 OVER(),很多函数就是普通聚合函数。
比如:
SUM(salary)这是普通聚合。
而:
SUM(salary) OVER()这是窗口函数,表示:
在整个结果集这个窗口里,计算总工资,但不要把行合并掉。
PARTITION BY:先分组,但不折叠行它很像 GROUP BY,但只是在逻辑上分区,不会把数据压缩。
SUM(salary) OVER(PARTITION BY department)意思是:
你可以把它理解成:
是“分组统计”,但保留每条明细。
ORDER BY:定义窗口内的顺序很多窗口函数都和“顺序”强相关,比如:
这时候就必须指定顺序:
ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC)意思是:
这是窗口函数入门里最实用的一组。
假设有一张成绩表:
student_scores
-------------------------
student_name | class | score
张三 | A | 95
李四 | A | 95
王五 | A | 90
赵六 | A | 88ROW_NUMBER():强行排唯一名次SELECT
student_name,
class,
score,
ROW_NUMBER() OVER(PARTITION BY class ORDER BY score DESC) AS rn
FROM student_scores;结果可能是:
即使张三和李四同分,ROW_NUMBER() 也会硬分先后。
适用场景:
RANK():并列占同名,后面跳号SELECT
student_name,
class,
score,
RANK() OVER(PARTITION BY class ORDER BY score DESC) AS rk
FROM student_scores;结果会变成:
因为前两个人并列第一,所以第三个人的名次直接变成 3。
适用场景:
DENSE_RANK():并列占同名,但不跳号SELECT
student_name,
class,
score,
DENSE_RANK() OVER(PARTITION BY class ORDER BY score DESC) AS dr
FROM student_scores;结果是:
适用场景:
这题你应该见过:
查询每个部门工资最高的 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;为什么这么写?
第一层:
第二层:
这就是窗口函数最经典的应用之一。
如果你想保留并列,比如第三名有两个人同薪,别用 ROW_NUMBER(),要用 RANK() 或 DENSE_RANK()。
不然你会“误杀”一个本该入榜的人。
假设你有每日销售额表:
daily_sales
----------------------
sale_date | amount
2026-03-01 | 100
2026-03-02 | 150
2026-03-03 | 120
2026-03-04 | 180现在你想知道:
窗口函数一把梭:
SELECT
sale_date,
amount,
SUM(amount) OVER(ORDER BY sale_date) AS running_total
FROM daily_sales;这会得到:
这就是累计求和。
SELECT
store_id,
sale_date,
amount,
SUM(amount) OVER(PARTITION BY store_id ORDER BY sale_date) AS running_total
FROM daily_sales;意思很简单:
这类写法在下面场景非常常见:
很多新手写 SQL 时,一遇到“和上一天比较”,就想自连接。
说实话,能写,但丑。
窗口函数里有更直接的:
LAG():看上一行LEAD():看下一行SELECT
sale_date,
amount,
LAG(amount, 1) OVER(ORDER BY sale_date) AS prev_amount
FROM daily_sales;这里:
LAG(amount, 1) 表示取前 1 行的 amountNULLSELECT
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;这就能直接算出和上一天相比增减了多少。
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() 就完事,更多时候是组合使用。
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;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;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;这些需求,如果不用窗口函数,不是不能做,是会写得更丑、更绕、更难维护。
PARTITION BY 当成 GROUP BY记住:
GROUP BY:会减少结果行数PARTITION BY:不会减少结果行数别混。
ORDER BY像 ROW_NUMBER()、RANK() 这种函数,不写排序基本等于耍流氓。
你不指定顺序,数据库怎么知道谁第一?
ROW_NUMBER()RANK()DENSE_RANK()别无脑都写 ROW_NUMBER()。
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;尤其是 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 的核心能力之一。
比如:
这些更进阶,但前面没打牢,学它们就是硬啃。
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)建议你把这几种写法手打 5 遍,比看 20 遍教程有用。
很多人学 SQL,总爱把窗口函数当成“高级语法”,仿佛会了就显得厉害。
其实不是。
它不是炫技工具,而是分析 SQL 的基本功。
你一旦开始做下面这些事,就根本绕不开它:
说白了:
不会窗口函数,你写 SQL 只能算“能查数据”;会窗口函数,你才开始真正“分析数据”。
这个差距,不小。
如果你现在对窗口函数还有点模糊,不用慌。
先把这篇文章里的几个核心例子亲手敲一遍:
ROW_NUMBER() 做每组 Top NSUM() OVER(ORDER BY ...) 做累计求和LAG() 做和上一条记录对比把这三类写顺了,你对窗口函数的理解会立刻从“看懂”升级到“会用”。
真正学 SQL,别只背语法。要建立场景感。
窗口函数这东西,一旦开窍,后面很多分析题你都会觉得:
就这?原来以前是在绕远路。
阅读原文:原文链接