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

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

SQL 窗口函数在复杂报表中的应用

[复制链接]

SQL 窗口函数在复杂报表中的应用

[复制链接]
dbaai

主题

0

回帖

111

积分

DBAAI

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

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

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

×
SQL 窗口函数在复杂报表中的应用


一、具体的问题

做业务报表时,很多统计需求用普通的 GROUP BY 根本表达不出来,或者要绕一大圈自关联、子查询才能勉强实现。下面几类几乎每个 DBA 和数据分析师都会撞上的场景:


  • 每个部门里销售额排前 3 的员工是谁(既要看个人业绩,又离不开部门整体排名);
  • 按月份累加的累计销售额,以及和上个月、去年同期的对比;
  • 计算每笔订单在"所属客户"里的金额占比,或者"当前行占分组总和的百分比";
  • 不丢明细的前提下,给每一行标上它在分组内的排名、行号。


这些需求共同点是:既要保留原始明细行,又要基于某个分组做聚合或排序计算。GROUP BY 会把行"压扁"成一行,明细直接丢失;而窗口函数(OVER 子句)恰恰能在不折叠行的情况下完成这类计算,这正是它存在的意义。

二、核心原理

窗口函数的骨架是 函数名(...) OVER (PARTITION BY 分组列 ORDER BY 排序列 窗口框架)


  • PARTITION BY:决定"按谁分组",作用类似 GROUP BY,但不会产生分组汇总行,只是把计算范围框在组内。
  • ORDER BY:决定组内行的计算顺序,对排名类、累计类函数至关重要。
  • 窗口框架(ROWS / RANGE):进一步限定"当前行往前/往后看几行",累计求和就靠它。


常用函数分两类:


  • 排名类ROW_NUMBER() 给组内每行唯一连续序号;RANK() 并列跳号(1,1,3);DENSE_RANK() 并列不跳号(1,1,2);NTILE(n) 把组切成 n 份便于分桶。
  • 聚合窗口SUM()/AVG()/COUNT() OVER(...) 在保留明细的同时算分组聚合;LAG(col, n) / LEAD(col, n) 取前/后第 n 行的值,用来做环比、同比、差值最方便。


一个常见误区:以为窗口函数里写了 ORDER BY 就自动变成"累计到当前行"。其实默认框架是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(确实从开头累计到当前行),但如果你想要"近 3 行滑动平均",必须显式写 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW,否则结果会差很多。

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

下面用一张销售明细表演示 4 个最常被点名的报表需求,全部可照抄运行(以 MySQL 8 / PostgreSQL 语法为准,SQL Server 同理)。

先建表灌数据:
  1. CREATE TABLE sales (
  2.   id     INT PRIMARY KEY,
  3.   dept   VARCHAR(20),
  4.   emp    VARCHAR(20),
  5.   mth    CHAR(7),        -- 如 '2026-01'
  6.   amount DECIMAL(12,2)
  7. );
  8. INSERT INTO sales VALUES
  9. (1,'华北','张三','2026-01',1200),(2,'华北','李四','2026-01',900),
  10. (3,'华北','王五','2026-01',1500),(4,'华东','赵六','2026-01',2000),
  11. (5,'华东','钱七','2026-01',1100),(6,'华北','张三','2026-02',1300),
  12. (7,'华东','赵六','2026-02',1800);
复制代码

需求 1:每个部门销售额 Top 3 员工(并列也保留):
  1. SELECT dept, emp, amount,
  2.        DENSE_RANK() OVER (PARTITION BY dept ORDER BY amount DESC) AS rk
  3. FROM sales
  4. WHERE mth = '2026-01'
  5. QUALIFY rk <= 3;          -- MySQL 8.0.31+/PG 用 QUALIFY;老版本外套一层子查询 WHERE rk<=3
复制代码

需求 2:按员工逐月累计销售额:
  1. SELECT emp, mth, amount,
  2.        SUM(amount) OVER (PARTITION BY emp ORDER BY mth
  3.                          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_amt
  4. FROM sales;
复制代码

需求 3:本月 vs 上月环比(用 LAG 取上一期):
  1. SELECT emp, mth, amount,
  2.        LAG(amount, 1) OVER (PARTITION BY emp ORDER BY mth) AS prev_amt,
  3.        ROUND((amount - LAG(amount,1) OVER (PARTITION BY emp ORDER BY mth))
  4.              / LAG(amount,1) OVER (PARTITION BY emp ORDER BY mth) * 100, 1) AS mom_pct
  5. FROM sales;
复制代码

需求 4:每笔金额占"所属部门当月总额"的比例:
  1. SELECT dept, emp, mth, amount,
  2.        ROUND(amount / SUM(amount) OVER (PARTITION BY dept, mth) * 100, 1) AS pct_of_dept
  3. FROM sales;
复制代码

前后对比:同样的需求,用 GROUP BY + 自关联至少要写 2~3 层子查询、还容易丢明细;窗口函数一条 SQL 既留明细又出指标,可读性和性能都更好。

四、实操检查清单


  • 确认数据库版本支持窗口函数:MySQL 8.0+、SQL Server 2008+、PostgreSQL 8.4+、Oracle 8i+;老版本需升级或绕路。
  • 排名需求先想清楚要 ROW_NUMBER(唯一序号)、RANK(跳号)还是 DENSE_RANK(不跳号),别混用。
  • 凡是"累计/滑动"类,务必显式写窗口框架 ROWS/RANGE BETWEEN ...,不要依赖默认行为。
  • PARTITION BY 的列和 ORDER BY 的列要想清楚:分组错了,所有指标全盘错。
  • 取"前/后行"用 LAG/LEAD 代替自关联,代码更短、执行计划更优。
  • 需要"每组取前 N 行"时优先用 QUALIFY(新版)或外层 WHERE rk<=N(老版),避免在 WHERE 里直接引用窗口函数别名。
  • 上线前用 EXPLAIN 看执行计划,确认窗口函数没有触发意外的全表排序放大。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

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

GMT+8, 2026-9-2 14:10 , Processed in 0.015541 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

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