☰
SQL Server开窗函数partition by实战:成绩排名不再求GROUP BY
2026/10/2 14:38:59 网站建设 项目流程

排成绩这件事,只要是做数据库开发的,迟早都会碰上。我印象特别深的一次,是刚接触MS SQL Server那年,领导甩给我一张几百人的考试成绩表,要求"按班级排名、按总分排名、还要把每科排名都列出来"。我一听就懵了——用GROUP BY做分组统计,结果行数全被压缩了;用子查询关联排名,SQL写得跟迷宫似的,一改条件就崩。后来翻了半天资料才搞明白,真正顺手的是partition by这个开窗函数。它一个函数就能把"分组"和"排名"同时搞定,行数还不变。这篇文章就围绕成绩排名这个实战场景,把partition by的用法、原理和那些踩过的坑一次说清楚。

1. 成绩排名需求背后的核心痛点:为什么GROUP BY不够用

先聊一个最基础的问题:排名这个需求,用GROUP BY真的做不了吗?能做,但做得很别扭。GROUP BY的本质是"分组压缩",你按班级分组,出来的就是每个班级一行汇总;你按科目分组,出来的就是每科一行汇总。可成绩排名要的是什么呢?是"每个人一行原始记录,但旁边多出来一个名次列"。这两者的逻辑天然冲突。

举个例子,一张考试成绩表长这样:

学号姓名班级科目成绩
1001张三1班数学92
1001张三1班语文88
1001张三1班英语76
1002李四1班数学85
1002李四1班语文91
1002李四1班英语80

如果你想按班级看数学成绩的排名,GROUP BY班级当然可以,但结果只有两行,张三和李四每个人的其他科目信息全丢了。你还得另外再关联回原表,把明细捞出来,一来二去SQL就膨胀得没法看。

而且GROUP BY还有个老毛病:它没法在聚合的同时保留明细。你既要某个人的原始成绩,又要这个成绩在全班的位次,用GROUP BY就得做两次查询再拼起来,性能差不说,代码维护起来也痛苦。

那这时候开窗函数就显出价值了。开窗函数最大的特点是:它在不改变行数的情况下,给每一行附加一个"窗口内计算出来的值"。partition by就是窗口的分组条件,你可以按班级开一个窗口、按科目再开一个窗口,同一行数据可以同时参与多个窗口的计算,互不干扰。这就完美解决了"既要明细、又要排名"的双重需求。

我在实际项目里还发现一个更麻烦的场景:有些报表需要"总分排名"和"单科排名"同时展示。用GROUP BY你得写两个查询再union或者join,而用partition by,只在一条SELECT里分别给不同窗口开窗就行,代码量直接少一半。这也是我后来在所有排名类需求里首选partition by的根本原因。

2. partition by的执行逻辑拆解:先分组、再排序、后计算

很多初学者会把partition by理解成"高级的GROUP BY",这个理解不算错,但不精准。partition by确实有分组的功能,但它分完组之后干的事情完全不同。搞清楚它内部的执行顺序,写出来的SQL才能得心应手。

2.1 窗口函数的逻辑执行顺序

一条带窗口函数的SQL,逻辑上大致按这样的顺序走:

  1. FROM子句先把表加载进来
  2. WHERE子句把不需要的行筛掉
  3. GROUP BY / HAVING做分组和分组后的过滤(如果有的话)
  4. 窗口函数在分组结果之上逐行计算
  5. SELECT把最终列投影出来
  6. ORDER BY做最终的排序输出

也就是说,partition by的分组,发生在WHERE过滤之后,而不是之前。这个顺序非常重要。你如果想排除某些人的成绩再排名,直接在WHERE里加条件就行,开窗函数会自动基于过滤后的结果集去分组,不需要额外的子查询。很多人不知道这点,非要多套一层子查询,白白增加复杂度。

另外要注意,窗口函数的执行不改变查询结果的行数。每一行原始记录都会保留,只是在旁边多了一个计算列。这就完全是"给明细表加排名列"的天然工具。

2.2 一个简单的partition by排名示例

咱们直接用MS SQL Server的语法写一段标准成绩排名SQL。假设有张成绩表ScoreInfo,字段包括StudentID、StudentName、ClassName、SubjectName、Score,现在要给每个班级的数学成绩做排名:

SELECT StudentID, StudentName, ClassName, SubjectName, Score, ROW_NUMBER() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS RankInClass FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, RankInClass;

这段SQL做了三件事:先用WHERE把科目限定成数学,再用PARTITION BY ClassName把数据按班级切成一个个独立的小组,最后在每个小组内部按Score降序编号,得到班级内排名。输出的结果里,每一行还是原来的明细数据,只是多了个RankInClass列。

这段SQL里最容易写错的地方,是ORDER BY那个子句的方向。排成绩肯定要高分在前,所以Score后面必须跟DESC;你要是写成ASC,那就是倒着排了,成绩最差的反而是第1名,这种低级错误真发生过。另外,窗口函数里的ORDER BY和最终结果集的ORDER BY是两个概念,前者决定排名计算的顺序,后者决定展示顺序,两个都可以写,也可以只写一个,但别搞混。

2.3 partition by和group by的本质对比

为了让你彻底搞清楚这两个家伙的区别,我拿一张表直接对比:

对比项GROUP BYPARTITION BY
行数变化每个分组合并成一行行数保持不变
明细保留不保留,只能看聚合结果每一行都保留
聚合函数SUM、AVG、COUNT等都能用也能用,但必须配合OVER
与排名配合不支持排名支持ROW_NUMBER/RANK/DENSE_RANK
典型场景报表汇总排名、同比、占比、移动平均

一句话总结:GROUP BY是"把多行压成一行",PARTITION BY是"把一行放到多个组里各自算"。业务上如果只是要汇总数,用GROUP BY;如果要数带着明细一起展示,就必须用PARTITION BY。

还有一个细节容易忽略:PARTITION BY后面可以跟多个列,比如PARTITION BY ClassName, SubjectName,那就等于按"班级+科目"的联合维度分组,每个组合内部独立排名。这种多字段分组的写法,在生成复杂报表时特别常用,后面实战部分会专门演示。

3. 成绩排名的三大核心函数:ROW_NUMBER、RANK、DENSE_RANK的取舍

讲完了partition by的分组逻辑,接下来就是重头戏——排名函数本身。MS SQL Server里常用的排名函数有三个:ROW_NUMBER、RANK、DENSE_RANK。它们都配partition by用,但排名规则不一样,选错了,报表上的名次可就是另一回事了。

3.1 三个函数的区别与适用场景

我先用一个极端的例子展示差异。假设某班数学成绩有四个学生:A考了95,B考了95,C考了90,D考了85。

学生成绩ROW_NUMBERRANKDENSE_RANK
A95111
B95211
C90332
D85443

看到区别没有:

  • ROW_NUMBER:不看成绩是否相同,强制按顺序给每个学生一个唯一序号。A、B成绩一样,但一个1、一个2。适合"取前N条"的场景,比如每个班取前三个人。
  • RANK:成绩相同的人,名次并列,但下一个名次要跳号。A、B并列第1,C直接排到第3。这正是Excel里RANK函数"有并列则占用名次"的行为,符合大多数比赛规则。
  • DENSE_RANK:成绩相同名次并列,但下一个名次不跳号。A、B并列第1,C排第2,D排第3。适合"名次必须连续"的排行榜,比如积分榜。

我自己的经验是:做成绩排名,默认先问业务需求。领导说"分数一样就并列,但名次不要断",用DENSE_RANK;领导说"并列没关系,但后面的人名次往后顺延",用RANK;领导说"每个班只取前三名,名额按人数算,不搞并列",就用ROW_NUMBER,再去嵌套查一下RN <= 3就行。

3.2 三大排名函数的完整SQL写法

直接上代码。还是那张ScoreInfo表,分别写三个排名:

SELECT StudentName, ClassName, Score, ROW_NUMBER() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS RowNumRank, RANK() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS RankRank, DENSE_RANK() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS DenseRankRank FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, Score DESC;

运行结果里,RowNumRank、RankRank、DenseRankRank三列并排出现,一眼就能看出三个函数的名次差异。我建议你在本地跑一遍这个SQL,观察一下数据,比看十篇文章都管用。

3.3 为什么说RANK是最容易被误解的函数

RANK这个函数名看起来最"正统",很多人直接拿它当排名用,但结果经常跟预期不一样。原因在于,RANK遇到并列会跳号,很多业务方并不接受"第1名后面直接是第3名"。我有一次做学生成绩分析,用了RANK,结果领导看到报表直接问:"第2名哪去了?"场面非常尴尬。从那以后,我每次都要问清楚需求:并列的时候,名次是"顺延"还是"打断"。

这里也分享一个判断技巧:如果业务系统里名次后面还跟着奖金、名额等资源分配,一般要用DENSE_RANK,因为名次断了不连续,资源档次没法对应;如果只是做个"成绩从高到低的展示序号",用ROW_NUMBER反而最稳妥——至少名单上每一个学生都有一个唯一的序号,不会让人数都对不上。

4. partition by在成绩分析里的其他打开方式:聚合、极值、前后对比

排名只是partition by的入门用法。实际做成绩分析的时候,我还会用partition by干很多别的事,全都围绕"那一条又不能少的明细数据"。

4.1 用partition by配合聚合函数做占比和加总

开窗函数里可以用SUM、AVG、COUNT等聚合函数,但语法上要加OVER子句。比如要给每个学生算"数学成绩占班级数学总分的比例",直接这样写:

SELECT StudentName, ClassName, Score, SUM(Score) OVER (PARTITION BY ClassName) AS ClassTotalScore, CAST(Score * 100.0 / SUM(Score) OVER (PARTITION BY ClassName) AS DECIMAL(5,2)) AS ScorePercent FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, Score DESC;

这段SQL里,SUM(Score) OVER (PARTITION BY ClassName)的含义是:在每一行数据上,计算当前班级所有数学成绩的总分。它不压缩行数,所以每一行都能看到自己班级的总分,再除以自己的成绩,就是个人占比。有没有感觉很方便?如果不用开窗函数,你要先查一遍班级总分,再关联回去,写起来至少多三行代码。

同理,AVG(Score) OVER (PARTITION BY ClassName)能直接在每一行上算出班级平均分,用来跟个人成绩比较,判断"高于班级平均还是低于班级平均"。这类"明细行上带汇总值"的需求,在成绩分析报表里非常常见。

4.2 用FIRST_VALUE和LAST_VALUE提取组内极值

还有一个比较冷门但实战好用的函数:FIRST_VALUE。它可以在每个窗口内取第一个值。比如想看一下"每个班级数学最高分是谁":

SELECT StudentName, ClassName, Score, FIRST_VALUE(StudentName) OVER (PARTITION BY ClassName ORDER BY Score DESC) AS TopStudent FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, Score DESC;

这里需要特别注意:FIRST_VALUE默认的窗口范围是整个分组,但如果你在OVER里写了ROWS BETWEEN这样的帧条件,取值的范围就会变化。默认情况下FIRST_VALUE配合ORDER BY SCORE DESC,就是取分组内按成绩排序后的第一个人的姓名,也就是最高分的那个人。这个函数在做"跟第一名比差几分"这类分析时尤其好用:

Score - FIRST_VALUE(Score) OVER (PARTITION BY ClassName ORDER BY Score DESC) AS DiffFromTop

直接算出每个学生跟班级最高分的差距。做学情分析时,这个差距比单纯的名次更有说服力。

4.3 用LAG和LEAD对比前后名次的成绩变化

LAG和LEAD这两个函数,可以取当前行的前一行或后一行的某个字段值。在成绩排名场景里,配合partition by使用,可以看"前一名的成绩是多少、我差多少才能超越他"。

SELECT StudentName, ClassName, Score, ROW_NUMBER() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS RankNum, LAG(Score) OVER (PARTITION BY ClassName ORDER BY Score DESC) AS PrevScore, Score - LAG(Score) OVER (PARTITION BY ClassName ORDER BY Score DESC) AS GapToPrev FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, Score DESC;

结果中PrevScore列会显示每个学生前一名的成绩,GapToPrev显示跟前一名的分差。第1名的PrevScore是NULL,因为前面没人。这个写法在"进步名次分析"里也很有用:把两次考试成绩放在同一张表里,用LAG对比上次成绩,就能算出分数涨跌。

有的朋友会问:"为什么不用自连接来实现?"确实能实现,但自连接要关联两次,还得处理NULL和边界行,SQL复杂不说,性能也差。LAG/LEAD是原生优化过的,执行效率高,代码也直观。

4.4 用AVG配合帧计算做移动平均

还有一种很常见的需求:既要看学生逐次考试成绩的波动,又要看一个"平滑的移动平均线"。这种需求在成绩趋势分析里特别多。MSSQL的开窗函数里可以定义帧(BETWEEN...AND...)来控制计算范围:

SELECT StudentName, ExamDate, Score, AVG(Score) OVER (PARTITION BY StudentName ORDER BY ExamDate ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS MovingAvg3 FROM ScoreInfo WHERE StudentName = '张三' ORDER BY ExamDate;

这段SQL会计算张三每次考试前三次成绩(含当前这次)的移动平均分。移动平均能消除单次考试的偶然波动,更稳定地反映成绩趋势。这里帧的定义是"当前行往前数2行到当前行",也就是窗口大小为3。

这类计算如果不用开窗函数,你得在应用程序里循环处理,或者写复杂的子查询;用了开窗函数,一条SQL就搞定,而且数据量越大,优势越明显。

5. 综合实战:分班成绩排名报表的完整落地

理论说了这么多,接下来走一遍完整实战。我会从建表、造数开始,一步步写出能直接抄作业的SQL,覆盖单科排名、总分排名、各科前N名、名次变化等几个高频需求。

5.1 准备测试数据

先建一张学生成绩表,插入一些测试数据:

CREATE TABLE ScoreInfo ( StudentID INT, StudentName NVARCHAR(50), ClassName NVARCHAR(20), SubjectName NVARCHAR(20), Score DECIMAL(5,1), ExamDate DATE ); INSERT INTO ScoreInfo VALUES (1001, N'张三', N'1班', N'数学', 92.0, '2024-06-01'), (1001, N'张三', N'1班', N'语文', 88.0, '2024-06-01'), (1001, N'张三', N'1班', N'英语', 76.0, '2024-06-01'), (1002, N'李四', N'1班', N'数学', 85.0, '2024-06-01'), (1002, N'李四', N'1班', N'语文', 91.0, '2024-06-01'), (1002, N'李四', N'1班', N'英语', 80.0, '2024-06-01'), (1003, N'王五', N'1班', N'数学', 92.0, '2024-06-01'), (1003, N'王五', N'1班', N'语文', 79.0, '2024-06-01'), (1003, N'王五', N'1班', N'英语', 85.0, '2024-06-01'), (1004, N'赵六', N'2班', N'数学', 78.0, '2024-06-01'), (1004, N'赵六', N'2班', N'语文', 84.0, '2024-06-01'), (1004, N'赵六', N'2班', N'英语', 90.0, '2024-06-01'), (1005, N'钱七', N'2班', N'数学', 88.0, '2024-06-01'), (1005, N'钱七', N'2班', N'语文', 72.0, '2024-06-01'), (1005, N'钱七', N'2班', N'英语', 83.0, '2024-06-01');

注意这里我特意让张三和王五的数学都是92分,为的就是演示并列情况下的排名差异。

5.2 场景一:每个班级单科成绩排名

需求:按班级给数学成绩排名,并列名次要顺延(用RANK)。

SELECT StudentName, ClassName, Score, RANK() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS RankByClass FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, RankByClass;

执行结果:

StudentNameClassNameScoreRankByClass
张三1班92.01
王五1班92.01
李四1班85.03
钱七2班88.01
赵六2班78.02

张三和王五并列第1,李四直接排第3,这就是RANK的"跳号"行为。如果你想要不跳号的版本,把RANK换成DENSE_RANK,李四就会显示第2名。

5.3 场景二:每个学生的总分班级排名

排名之前先要把各科成绩汇总成总分,然后再按班级排名。这里有个关键坑:窗口函数不能直接作用在GROUP BY的结果上,你得先分组算总分,再在外面套一层窗口排名。

WITH TotalScore AS ( SELECT StudentID, StudentName, ClassName, SUM(Score) AS TotalScore FROM ScoreInfo GROUP BY StudentID, StudentName, ClassName ) SELECT StudentName, ClassName, TotalScore, DENSE_RANK() OVER (PARTITION BY ClassName ORDER BY TotalScore DESC) AS TotalRank FROM TotalScore ORDER BY ClassName, TotalRank;

结果:

StudentNameClassNameTotalScoreTotalRank
张四1班256.01
王五1班256.01
李四1班256.01
赵六2班252.01
钱七2班243.02

这里又出现一个有意思的情况:三个人的总分都是256(92+88+76、85+91+80、92+79+85),DENSE_RANK让三个人并列第1,下一名直接是第2。实际报表里,这种多名并列很常见,DENSE_RANK在这种场景下最友好。如果用ROW_NUMBER,就会随机决定谁是第1、谁是第2、谁是第3,反而没有意义。

5.4 场景三:取出每个班各科前三名

这是最典型的"分组TopN"需求,逻辑上分两步:先用ROW_NUMBER生成组内序号,再过滤掉序号大于3的行。因为窗口函数的结果不能直接用在WHERE里,所以必须套一层派生表或CTE。

WITH RankedScores AS ( SELECT StudentName, ClassName, SubjectName, Score, ROW_NUMBER() OVER (PARTITION BY ClassName, SubjectName ORDER BY Score DESC) AS RN FROM ScoreInfo ) SELECT StudentName, ClassName, SubjectName, Score, RN FROM RankedScores WHERE RN <= 3 ORDER BY ClassName, SubjectName, RN;

这个SQL里PARTITION BY ClassName, SubjectName的作用是:每个班级的每个科目都是一个独立的窗口,每个窗口内独立排1、2、3名。这就是前面提过的"多字段分组"的实际用途。

5.5 场景四:查看每个学生在班里的名次及与前一名差距

需求升级了:不只要名次,还要看跟前一名的分差。

SELECT StudentName, ClassName, Score, DENSE_RANK() OVER (PARTITION BY ClassName ORDER BY Score DESC) AS RankNum, LAG(Score) OVER (PARTITION BY ClassName ORDER BY Score DESC) AS PrevScore, Score - LAG(Score) OVER (PARTITION BY ClassName ORDER BY Score DESC) AS GapToPrev FROM ScoreInfo WHERE SubjectName = '数学' ORDER BY ClassName, RankNum;

结果里,第1名的PrevScore是NULL,GapToPrev也是NULL,应用层显示时要注意处理,别把NULL直接显示成0什么的。这个SQL在学情分析里非常实用,老师看一眼就知道"这个孩子离上一名还差几分"。

6. 实战中的五个高频坑位与排查技巧

partition by看着简单,实际跑起来翻车的次数可不少。这里把我踩过的坑和排查思路完整列出来,都是血泪教训。

6.1 坑一:窗口函数内部ORDER BY写错方向导致排名颠倒

这是最基础的坑,也是最容易犯的。RANK/ROW_NUMBER/DENSE_RANK后面必须跟着明确的排序条件。成绩排名要求高分在前,一定要ORDER BY Score DESC。如果你写成ASC,那排名就变成"最低分第1名"。我见过不止一次有人在ORDER BY里漏了DESC,排出来的名次正好倒挂。

排查思路很简单:先取一个班级的数据人工核对一下,看最高分是不是第1名。如果最高分排到最后一名,排序方向必然错了。这种问题,一条"ORDER BY ClassName, Score DESC"的结果集一眼就能看出来。

6.2 坑二:直接在WHERE里过滤窗口函数结果,直接报错

SQL的语法决定了,WHERE子句在窗口函数计算之前执行,所以你不能写WHERE ROW_NUMBER() OVER(...) = 1。正确的做法是把窗口函数包在子查询或CTE里,再在外面用WHERE过滤。

我自己早期就犯过这个错,写的SQL直接报"窗口函数不允许出现在WHERE子句中",一度怀疑是版本问题。实际上这属于逻辑顺序问题。以后凡是遇到要在窗口函数结果上做过滤的,一律先套CTE:

WITH Ranked AS ( SELECT ..., ROW_NUMBER() OVER(...) AS RN FROM ... ) SELECT * FROM Ranked WHERE RN <= 3;

6.3 坑三:PARTITION BY列粒度过细,导致窗口失去意义

PARTITION BY的粒度直接决定窗口的范围。你按学生分窗口,就只能看单个学生内部的排名;你按班级分窗口,才能看班级排名。有一次我帮同事排查SQL,他PARTITION BY写的是StudentName,结果排名每个学生都是第1,他愣是找不出原因。原因很简单:每个学生的窗口里就只有自己的数据,排名自然是1。

这种坑最大的特点是"结果看起来对,又好像不对"。排查方法也简单:SELECT DISTINCT查一下PARTITION BY用的列到底有多少个不同的值,确认粒度和需求匹配。

6.4 坑四:DISTINCT搭配窗口函数,结果出现诡异空白

有段时间我图省事,在SELECT里同时写了DISTINCT和窗口函数,结果SQL直接报错,或者输出结果莫名其妙少了行。原因是DISTINCT会先去重,再去计算窗口函数,两个逻辑叠加在一起,经常导致窗口函数的计算范围不是你想要的那个。

正确做法是:先算窗口函数,再去重。也就是说,窗口函数放在子查询里,外面的SELECT再用DISTINCT。示例:

SELECT DISTINCT StudentName, ClassName, TotalScore FROM ( SELECT StudentName, ClassName, SUM(Score) OVER (PARTITION BY StudentID, ClassName) AS TotalScore FROM ScoreInfo ) AS T;

6.5 坑五:NULL成绩参与了排名,导致结果不符合预期

成绩表里经常有缺考、缓考的情况,成绩列是NULL。如果你直接ORDER BY Score DESC,在MSSQL的默认排序规则里,NULL会排在最前面或者最后面(取决于排序方向),这会干扰排名。比如缺考的NULL成绩如果参与排序,可能直接排到第1名前面,这绝对不能接受。

处理办法:在窗口函数里显式处理NULL,比如把NULL成绩换成0再参与排名:

DENSE_RANK() OVER (PARTITION BY ClassName ORDER BY ISNULL(Score, 0) DESC) AS RankNum

或者干脆在WHERE阶段把缺考的记录先排除掉,让缺考学生不参与排名,单独在报表里标记为"缺考"。

7. 性能观察与索引建议:排名SQL跑不快的原因

写对了SQL,还得跑得快才行。成绩排名这种需求,数据量一大,几万行、几十万行,开窗函数的性能差异马上就体现出来了。

7.1 排序是最大的开销

开窗函数里的ORDER BY必然触发排序操作。PARTITION BY的分组也涉及哈希或排序。如果表上没有合适的索引,SQL Server就得在内存里临时排序,数据量一大,tempdb压力飙升,查询慢得让人怀疑人生。

所以我建议:在PARTITION BY和ORDER BY涉及的列上建立组合索引。比如对于按班级、科目排名的SQL,在ScoreInfo表上建一个如下索引:

CREATE INDEX IX_ScoreInfo_Class_Subject ON ScoreInfo(ClassName, SubjectName, Score DESC);

这个索引能让SQL Server直接通过索引顺序读取数据,省掉排序这一步,性能提升非常明显。

7.2 避免不必要的PARTITION BY

PARTITION BY不是越多越好。有些人图省事,把可能用到的分组维度全塞进PARTITION BY,结果窗口数量爆炸,每个窗口里的数据少得可怜,计算效率反而下降。每当你要加一个分组维度时,先问自己:这个维度真的需要吗?比如按班级排名的场景,PARTITION BY ClassName就够用,你把SubjectName也塞进去,反而把窗口切碎。

7.3 大结果集配合分页的思路

排名结果往往要分页展示。给成绩排名分批导出的时候,我习惯用OFFSET FETCH:

ORDER BY ClassName, RankNum OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY;

再配合前面的排名CTE,一次只取100条,避免一次性把几十万行排名结果全部拉回应用层,那样内存和网络都顶不住。

8. 从一张综合报表看MSSQL开窗函数的真实威力

最后用一个综合案例,把所有功能串起来。业务需求是:生成一份"期末考试成绩个人总览报表",每个学生一行,包含总分、班级总分排名、各科最高分对比、离最高分差距、班级平均分对比,还要算出每个学生的总分在年级的百分位。

SQL写出来是这个样子:

WITH StudentTotal AS ( SELECT StudentID, StudentName, ClassName, SUM(Score) AS TotalScore FROM ScoreInfo GROUP BY StudentID, StudentName, ClassName ), Ranked AS ( SELECT StudentName, ClassName, TotalScore, DENSE_RANK() OVER (PARTITION BY ClassName ORDER BY TotalScore DESC) AS ClassRank, CUME_DIST() OVER (ORDER BY TotalScore DESC) AS CumPercent, FIRST_VALUE(TotalScore) OVER (PARTITION BY ClassName ORDER BY TotalScore DESC) AS ClassTopScore, AVG(TotalScore) OVER (PARTITION BY ClassName) AS ClassAvgScore FROM StudentTotal ) SELECT StudentName, ClassName, TotalScore, ClassRank, CAST(CumPercent * 100 AS DECIMAL(5,1)) AS PercentileRank, ClassTopScore, TotalScore - ClassTopScore AS GapToTop, ClassAvgScore, TotalScore - ClassAvgScore AS DiffFromAvg FROM Ranked ORDER BY ClassName, ClassRank;

这个SQL一口气完成了:

  • 每班总分排名
  • 全年级百分位
  • 与班级最高分的差距
  • 与班级平均分的差距

如果不用开窗函数,这个报表至少得写四个子查询再合并,还不一定写得对。开窗函数的优势,在这种多维度数据分析场景下体现得淋漓尽致。

再说一个我自己的使用体会:很多人觉得开窗函数难,是因为没有建立起"窗口视角"。把一张表想象成一面墙,GROUP BY是把墙拆成一堆砖头的横截面,而PARTITION BY是在墙上开窗户——透过每扇窗户都能看到一组数据,但墙本身还在。想通了这一点,再复杂的排名需求也就那么回事。

这篇实战从最基础的GROUP BY痛点讲到排名函数的取舍,再到聚合、极值、前后值对比,最后落到完整报表和性能排查,算是一个相对完整的学习路径。如果你手头正好有一份成绩表,建议直接拿这些SQL去跑一遍,亲手验证一遍每个函数的行为差异——尤其是那个带并列的RANK和DENSE_RANK,跑一次比看十篇文章都记得牢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询