在数据分析、业务报表开发等场景中,我们经常需要处理跨行统计、排名、累计计算等复杂需求,传统的GROUP BY聚合只能返回分组后的汇总结果,无法保留原始行的明细信息,窗口函数的出现完美解决了这个问题。窗口函数可以在不改变原表行数的前提下,对每一行数据进行基于特定窗口的计算,既可以完成排名、滑动统计等复杂分析,也能保留原始数据的明细字段,大幅降低了复杂查询的编写难度。

窗口函数基础语法与核心概念
窗口函数的基本语法结构为:函数名() OVER (PARTITION BY 分组字段 ORDER BY 排序字段 窗口框架),其中OVER子句是窗口函数的核心,用来定义计算的窗口范围。
几个核心概念需要提前明确:
- PARTITION BY:类似GROUP BY的分组逻辑,用来将数据集划分成多个独立的窗口,每个窗口内的计算互相独立,不指定时则对整个数据集作为一个窗口计算。
- ORDER BY:用来指定窗口内数据的排序规则,很多窗口函数的计算结果依赖排序,比如排名函数、累计计算函数都需要明确的排序规则。
- 窗口框架:用来进一步限定窗口内的计算范围,分为ROWS(按行数界定)和RANGE(按值范围界定)两种模式,不指定时默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(从窗口第一行到当前行)。
需要注意的是,窗口函数只能出现在SELECT子句和ORDER BY子句中,不能用于WHERE、GROUP BY、HAVING这些过滤和分组子句,因为SQL的执行顺序是先处理这些子句,再计算SELECT中的窗口函数。
排名场景的专业技巧:三类排名函数差异与适用场景
排名是窗口函数最常用的场景之一,SQL提供了ROW_NUMBER、RANK、DENSE_RANK三个排名函数,很多开发者不清楚三者的差异,导致排名结果不符合业务预期,下面通过具体示例说明它们的区别。
首先准备测试数据,创建一张学生成绩表并插入测试数据:
-- 创建学生成绩表
CREATE TABLE student_score (
student_id INT,
class VARCHAR(10),
score INT
);
-- 插入测试数据
INSERT INTO student_score VALUES
(1, '一班', 90),
(2, '一班', 90),
(3, '一班', 85),
(4, '一班', 80),
(5, '二班', 95),
(6, '二班', 90),
(7, '二班', 90),
(8, '二班', 88);接下来分别使用三个排名函数,按班级分组、成绩降序排名,查看结果差异:
SELECT
student_id,
class,
score,
-- 行号排名,不管值是否相同,依次递增编号
ROW_NUMBER() OVER (PARTITION BY class ORDER BY score DESC) AS row_num,
-- 跳跃排名,值相同排名相同,下一个排名跳过占用的位数
RANK() OVER (PARTITION BY class ORDER BY score DESC) AS rank_num,
-- 连续排名,值相同排名相同,下一个排名不跳跃
DENSE_RANK() OVER (PARTITION BY class ORDER BY score DESC) AS dense_rank_num
FROM student_score
ORDER BY class, score DESC;执行上述查询后,一班的结果如下:
| student_id | class | score | row_num | rank_num | dense_rank_num |
|---|---|---|---|---|---|
| 1 | 一班 | 90 | 1 | 1 | 1 |
| 2 | 一班 | 90 | 2 | 1 | 1 |
| 3 | 一班 | 85 | 3 | 3 | 2 |
| 4 | 一班 | 80 | 4 | 4 | 3 |
可以看到三者的核心差异:
ROW_NUMBER:即使分数相同,也会给出不同的排名,相当于给每一行数据一个唯一的行号,适合需要唯一序号的场景,比如分页查询时给每行加一个连续编号。RANK:分数相同的行排名相同,下一个排名会跳过重复占用的位数,比如两个第一名,下一个就是第三名,适合竞赛排名、业绩排名等需要体现并列且后续排名顺延的场景。DENSE_RANK:分数相同的行排名相同,下一个排名不会跳跃,比如两个第一名,下一个就是第二名,适合需要连续排名的场景,比如等级划分时,90分以上为A,80-89为B,需要连续排名对应等级。
实际业务中如果需要取每个班级的前两名,使用RANK会返回并列第一的两名和唯一的第二名,总共3条数据;使用DENSE_RANK会返回并列第一的两名和并列第二的两名,总共4条数据;使用ROW_NUMBER只会返回每个班级分数最高的两名,即使有并列也只取排序后的前两行,需要根据业务需求选择合适的函数。
滑动计算核心技巧:ROWS与RANGE框架的差异
滑动计算是窗口函数另一个高频使用场景,比如移动平均、累计求和、近N天统计等,核心是要正确设置窗口框架,很多开发者混淆ROWS和RANGE的区别,导致计算结果不符合预期。
ROWS框架:按行数界定窗口范围
ROWS是基于行的物理位置来界定窗口,比如ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示窗口包含当前行和往前数2行的数据,总共3行数据,不管这些行的值是否相同。
示例:计算每个学生的成绩与往前2名同学的成绩移动平均值:
SELECT
student_id,
class,
score,
-- 计算当前行+前2行的移动平均,共3行数据
AVG(score) OVER (
PARTITION BY class
ORDER BY score DESC
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3rows
FROM student_score
ORDER BY class, score DESC;以一班为例,排序后成绩为90、90、85、80,第一行的移动平均就是90/1=90,第二行是(90+90)/2=90,第三行是(90+90+85)/3≈88.33,第四行是(90+85+80)/3=85,完全按照行数计算,和分数值无关。
RANGE框架:按值范围界定窗口范围
RANGE是基于ORDER BY字段的值范围来界定窗口,相同值的行会被归入同一个窗口范围。比如ORDER BY score DESC,那么所有score相同的行,在计算RANGE窗口时会被视为同一个当前行范围。
同样用上面的例子,使用RANGE框架计算移动平均:
SELECT
student_id,
class,
score,
-- 计算当前值及相同值+前序不同值的移动平均,基于值范围
AVG(score) OVER (
PARTITION BY class
ORDER BY score DESC
RANGE BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_range
FROM student_score
ORDER BY class, score DESC;这里需要注意,RANGE的PRECEDING是基于ORDER BY字段的值差,比如ORDER BY score DESC,那么RANGE BETWEEN 2 PRECEDING AND CURRENT ROW表示score值大于等于当前score-2的所有行,因为成绩都是整数,所以一班90分的行,RANGE窗口会包含所有score≥88的行,也就是两个90分的行,所以第一行和第二行的移动平均都是(90+90)/2=90;第三行score是85,RANGE窗口是score≥83的行,也就是90、90、85三个行,移动平均是(90+90+85)/3≈88.33;第四行score是80,RANGE窗口是score≥78的行,也就是所有4行,移动平均是(90+90+85+80)/4=86.25。
可以看到当存在相同值的行时,ROWS和RANGE的计算结果会有明显差异,实际使用中如果需要对相同值的行做统一计算,比如相同分数的行算同一个统计单元,就用RANGE;如果需要严格按行数计算,不管值是否相同,就用ROWS。
常见滑动计算场景的实战示例
累计求和与累计计数
累计统计是业务报表中非常常见的需求,比如统计每个月的累计销售额、每个用户的累计消费金额,只需要使用默认的窗口框架即可,默认框架就是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,即从窗口第一行到当前行。
示例:统计每个班级的成绩累计总和,按成绩降序排列:
SELECT
student_id,
class,
score,
-- 累计求和,从班级第一行到当前行
SUM(score) OVER (
PARTITION BY class
ORDER BY score DESC
-- 这里可以省略窗口框架,默认就是UNBOUNDED PRECEDING TO CURRENT ROW
) AS cumulative_sum
FROM student_score
ORDER BY class, score DESC;一班的结果中,第一行累计和是90,第二行是180,第三行是265,第四行是345,符合累计计算的逻辑。
近N行移动平均与移动求和
比如需要统计每个学生的近3次成绩移动平均,或者近7天的销售额移动平均,只需要调整ROWS框架的范围即可,比如近3行就是ROWS BETWEEN 2 PRECEDING AND CURRENT ROW,如果当前行前面不足2行,就按实际行数计算。
如果需求是近N天的时间范围滑动计算,比如近7天的销售额移动平均,这时候ORDER BY的是日期字段,建议使用RANGE框架,因为日期是连续的,RANGE BETWEEN INTERVAL '6' DAY PRECEDING AND CURRENT ROW(不同数据库语法略有差异,比如MySQL中可以用RANGE BETWEEN 6 PRECEDING AND CURRENT ROW如果日期是连续的数值类型),这样可以自动包含近7天内的所有数据,即使某天没有数据也不会出错。
同比环比计算
同比环比是分析数据增长趋势的常用指标,环比是和上一个周期比,同比是和去年同期比,窗口函数可以很方便地取到上一个周期的数据。
示例:假设有一张销售表,包含月份和销售额,计算每月的环比增长率:
-- 创建销售表
CREATE TABLE monthly_sales (
month DATE,
sales_amount DECIMAL(10,2)
);
-- 插入测试数据
INSERT INTO monthly_sales VALUES
('2024-01-01', 10000),
('2024-02-01', 12000),
('2024-03-01', 11000),
('2024-04-01', 13000);
-- 计算环比增长率
SELECT
month,
sales_amount,
-- 取上个月的销售额,按月份排序,取前一行
LAG(sales_amount, 1) OVER (ORDER BY month) AS last_month_sales,
-- 计算环比增长率
ROUND(
(sales_amount - LAG(sales_amount, 1) OVER (ORDER BY month)) /
LAG(sales_amount, 1) OVER (ORDER BY month) * 100,
2
) AS mom_growth_rate
FROM monthly_sales
ORDER BY month;这里用到了LAG函数,它是窗口函数中的偏移函数,用来取当前行往前第N行的值,对应的LEAD函数是取往后第N行的值,非常适合同比环比、间隔数据对比的场景。
窗口函数使用常见误区与性能优化
在实际使用窗口函数时,很多开发者会遇到结果不符合预期或者查询性能差的问题,下面总结几个常见的注意点:
- 不要混淆PARTITION BY和GROUP BY:PARTITION BY只是划分窗口,不会合并行,GROUP BY会合并行,需要保留明细用PARTITION BY,只需要汇总用GROUP BY。
- 排序字段的选择:排名、累计计算、滑动计算都依赖ORDER BY的排序规则,一定要确认排序字段符合业务需求,比如按成绩降序还是升序,按日期升序还是降序。
- 窗口框架的显式指定:尽量不要依赖默认窗口框架,尤其是使用RANGE框架的时候,不同数据库的默认框架可能有差异,显式指定可以避免结果不一致。
- 性能优化:窗口函数的计算是在内存中进行的,如果数据量很大,PARTITION BY的字段最好有索引,ORDER BY的字段也建议有索引,可以减少排序的开销;尽量避免在窗口函数中使用复杂的嵌套子查询,先过滤数据再计算窗口函数,减少计算的数据量。
另外要注意不同数据库对窗口函数的支持略有差异,比如MySQL从8.0版本开始支持窗口函数,SQL Server、PostgreSQL、Oracle都支持,使用时要确认数据库版本是否支持对应的窗口函数。
总结
SQL窗口函数是处理复杂分析场景的强大工具,排名场景中要区分ROW_NUMBER、RANK、DENSE_RANK的差异,选择符合业务需求的函数;滑动计算中要搞清楚ROWS和RANGE框架的区别,按行数还是按值范围计算选择对应的框架。结合偏移函数LAG、LEAD,可以完成同比环比、间隔数据对比等复杂需求。只要掌握核心语法和使用场景,就能大幅简化复杂分析的SQL编写,提升查询效率与代码可维护性。