前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 9|回复: 0

[开发应用] T-SQL 窗口函数实战:排名与聚合

[复制链接]

[开发应用] T-SQL 窗口函数实战:排名与聚合

[复制链接]
dbaai

主题

0

回帖

131

积分

DBAAI

积分
131
8 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
T-SQL 窗口函数实战:排名与聚合


一、具体的问题

很多写 T-SQL 的人,一遇到"按部门给员工工资排名""算每个月的累计销售额""取每组前三条记录"这类需求,第一反应是 GROUP BY 配合子查询,甚至把整张表 self-join 好几遍。结果 SQL 又长又慢,还经常因为 GROUP BY 把明细行压没了而算错数。

窗口函数(Window Function)就是为这种"既要保留明细行、又要在行间做计算"的场景而生的。本文用一个员工薪酬 + 销售业绩的真实模型,把 ROW_NUMBER / RANK / DENSE_RANK 三种排名函数,以及 SUM() OVER 累计、移动平均讲清楚,并给出可照做的 T-SQL。

二、核心原理

1. 窗口函数不折叠行

和聚合函数最大的区别:窗口函数不会把多行合并成一行。它用 OVER(...) 定义一个"窗口"(一组相关行),对每一行计算一个值,但原始明细行全部保留。这正是它能边排名边保留原始数据的根本原因。

2. 三种排名函数的差异


  • ROW_NUMBER():纯顺序编号,1、2、3、4……即使值相同也给出不同序号,无并列。
  • RANK():相同值并列,并列后跳过名次。如两个第 2 名,下一个是第 4 名。
  • DENSE_RANK():相同值并列,但不跳名次。两个第 2 名,下一个是第 3 名。


选错函数,排名结果会差很大。

3. PARTITION BY 与 ORDER BY

OVERPARTITION BY 决定"在哪个分组内排"(如按部门分组),ORDER BY 决定"按什么顺序排"。这两个是窗口函数的灵魂。注意:窗口里的 ORDER BY 是"计算排序"而非"结果去重",和查询顶层的 ORDER BY 是两回事。

4. 聚合 + 窗口 = 累计与移动

SUM(sal) OVER(PARTITION BY dept ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 就能算出"到当前月为止的累计销售额",不需要自连接。再用 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 可以算移动平均。

三、实例参考(动手步骤)

建一张员工薪酬表和销售事实表,跑通三种排名与累计。
  1. -- 1) 建表并灌入测试数据
  2. CREATE TABLE dbo.EmpSalary (
  3.   emp_id   INT,
  4.   dept     VARCHAR(20),
  5.   salary   INT
  6. );
  7. INSERT INTO dbo.EmpSalary VALUES
  8.   (1,'研发',18000),(2,'研发',15000),(3,'研发',15000),(4,'研发',12000),
  9.   (5,'销售',16000),(6,'销售',16000),(7,'销售',9000);
  10. -- 2) 三种排名对比(按部门内 salary 降序)
  11. SELECT dept, emp_id, salary,
  12.        ROW_NUMBER() OVER(PARTITION BY dept ORDER BY salary DESC) AS rn,
  13.        RANK()       OVER(PARTITION BY dept ORDER BY salary DESC) AS rk,
  14.        DENSE_RANK() OVER(PARTITION BY dept ORDER BY salary DESC) AS drk
  15. FROM dbo.EmpSalary
  16. ORDER BY dept, salary DESC;
  17. -- 观察:研发部 salary=15000 的两人,ROW_NUMBER 给出 2/3(无并列),
  18. --       RANK 给出 2/2(并列第2、下一人第4),DENSE_RANK 给出 2/2(并列第2、下一人第3)。
复制代码
  1. -- 3) 每月累计销售额(移动累计,不丢明细行)
  2. CREATE TABLE dbo.MonthSales (sales_month DATE, amount INT);
  3. INSERT INTO dbo.MonthSales VALUES
  4.   ('2026-01-01',1000),('2026-02-01',1500),('2026-03-01',800),('2026-04-01',2000);
  5. SELECT sales_month, amount,
  6.        SUM(amount) OVER(ORDER BY sales_month
  7.                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
  8.        AVG(amount) OVER(ORDER BY sales_month
  9.                         ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg_3
  10. FROM dbo.MonthSales
  11. ORDER BY sales_month;
  12. -- running_total 依次累加到 5300;moving_avg_3 为当前月与前后各一月的均值。
复制代码
  1. -- 4) 取每个部门工资最高的前两名(TOP N per group)
  2. WITH ranked AS (
  3.   SELECT dept, emp_id, salary,
  4.          ROW_NUMBER() OVER(PARTITION BY dept ORDER BY salary DESC) AS rn
  5.   FROM dbo.EmpSalary
  6. )
  7. SELECT dept, emp_id, salary
  8. FROM ranked
  9. WHERE rn <= 2
  10. ORDER BY dept, rn;
  11. -- 研发返回 18000/15000;销售返回 16000/16000(ROW_NUMBER 在两值相同时任意取两个)。
复制代码

前后对比:用 GROUP BY + 子查询实现"取每个部门前两名"至少要两次扫描或 self-join,窗口函数一次扫描搞定,执行计划里少了 Nested Loop,逻辑读明显下降。

四、实操检查清单


  • 排名需求是否真的需要"并列"?要并列用 RANK/DENSE_RANK,要唯一序号用 ROW_NUMBER
  • OVER 里是否漏了 PARTITION BY?漏了会导致全表当成一个分组排名。
  • 累计/移动窗口的 ROWS BETWEEN 边界是否正确(UNBOUNDED PRECEDING 还是 N PRECEDING)?
  • 窗口里的 ORDER BY 和查询顶层 ORDER BY 是否混淆?前者决定窗口计算顺序,后者决定结果展示顺序。
  • 取 TOP N per group 是否用 CTE + ROW_NUMBER 过滤,而非 TOP 配合错误的 GROUP BY
  • 大表上窗口函数是否配合了正确的索引(PARTITION BY/ORDER BY 列最好有复合索引)避免排序溢出到 tempdb?


五、一个常被忽略的坑:窗口函数里的排序溢出

曾有一条按月累计的报表 SQL 在 500 万行的表上跑了 40 秒,执行计划显示"Sort"操作符把整个数据集溢写到 tempdb。根因是窗口函数的 ORDER BY 列没有对应索引,SQL Server 只能先全表排序再开窗。补上 (sales_month) 上的索引后,排序被索引顺序替代,耗时降到 2 秒。教训:窗口函数的排序列务必有索引支撑,否则性能会随数据量线性恶化。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-9-6 16:38 , Processed in 0.020802 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表