为什么需要窗口函数

日常 SQL 里,分组聚合 group by 会把多行压成一行,想在同一行既看到明细又看到组内统计就做不到了。排行榜要排名、报表要累计、环比要上一期值,这些需求用子查询与自连接也能写,但 SQL 又长又难读,性能还差。窗口函数让"每一行都带上一组统计上下文"成为可能,是分析场景的瑞士军刀。

窗口函数也叫分析函数,它不改变行数,只是给每一行附加一个基于窗口计算的列。理解窗口函数的关键是三个概念:分区 PARTITION BY 决定窗口按什么分组,排序 ORDER BY 决定窗口内的顺序,框架 ROWS/RANGE 决定窗口的边界范围。

本文核心知识点
  • 窗口为每行划定的一组相关行。
  • 排名RANK、DENSE_RANK 与 ROW_NUMBER。
  • 移动LAG、LEAD 与移动平均。
  • 聚合SUM、AVG 配合窗口边界。
SQL 窗口函数配图:蓝紫渐变背景底部多行整齐数据方块行,上方一块半透明圆角矩形滑窗层覆盖部分行并边缘发光,扁平科技插画
半透明滑窗覆盖数据行,正是窗口函数"框住一部分行做计算"的直观表达。

窗口函数基础语法

窗口函数的语法骨架是:函数名 OVER 加括号,括号里是窗口定义。ORDER BY 决定窗口内顺序,PARTITION BY 决定分组;两者都可省略。框架子句 ROWS BETWEEN 用来定义边界,默认在有 ORDER BY 时是从窗口起点到当前行。

SELECT emp_id, dept, salary,
       SUM(salary) OVER (PARTITION BY dept
                         ORDER BY emp_id
                         ROWS BETWEEN UNBOUNDED PRECEDING
                                  AND CURRENT ROW) AS running_sum
FROM emp;

注意窗口函数只能出现在 SELECT 与 ORDER BY 子句里,不能在 WHERE 中过滤,因为窗口在分组过滤之后才计算。想要"取排名前 N",要包一层子查询再过滤,这是最容易踩的坑。

排序与排名场景

排名是窗口函数最经典的应用。ROW_NUMBER 给每行一个不重复序号,适合分页取 Top N;RANK 遇到并列会跳过号次,比如两个并列第一后下一个是第三;DENSE_RANK 并列不跳号,两个第一后下一个是第二;NTILE 把结果分成若干桶,适合打标签分档。

SELECT name, score,
       ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
       RANK()       OVER (ORDER BY score DESC) AS rk,
       DENSE_RANK() OVER (ORDER BY score DESC) AS drk
FROM student_score;

取每组 Top N 是面试与报表的高频题:先按组内排好序,再在外层过滤排名小于等于 N。比如"每个部门工资最高的三个人",一个窗口函数加一层子查询就能写出来,自连接写法既慢又绕。

聚合与移动计算

把 SUM、AVG、COUNT 放进窗口,就能在同一行看到累计值、移动平均值与分区占比。移动平均是趋势分析的利器:AVG 配合 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 得到三日均值,可以平滑噪声看趋势。

SELECT day, revenue,
       AVG(revenue) OVER (ORDER BY day
                          ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS ma3
FROM daily_revenue;

累计求和 running sum 用 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW;百分比占比用 SUM OVER 整个分区做分母。窗口聚合的边界要写清楚,默认边界在不同数据库里行为略不同,显式声明 ROWS 或 RANGE 更稳妥。

差值、占比与同比环比

LAG 取前一行、LEAD 取后一行,是计算环比、同比与相邻差值的核心。环比增长:本期值减上一期值再除以上一期值;同比:今年本月对比去年同月。写法上用 LAG 把上一期值拉到同一行,再算增长率。

SELECT month, amount,
       LAG(amount) OVER (ORDER BY month) AS prev_amount,
       ROUND((amount - LAG(amount) OVER (ORDER BY month))
             / LAG(amount) OVER (ORDER BY month) * 100, 2) AS mom_pct
FROM monthly_sales;

FIRST_VALUE 取窗口首行、LAST_VALUE 取末行,适合"到当前为止的最新状态"这类取值;NTILE 分桶后可以给用户分群。窗口函数把报表里那些要"上一行、下一行、第一行"的诉求全部收进标准 SQL,不用再写一堆子查询。

性能与注意事项

窗口函数的性能取决于排序与分区。每个窗口定义都要按 ORDER BY 排序,多个不同窗口定义会多次排序,能合并的窗口尽量共用。分区键要能走索引或与数据分布匹配,否则大数据量下排序会成为瓶颈。

写窗口函数还有几个纪律:窗口函数里不要用 WHERE 过滤结果,要包子查询;ORDER BY 字段最好唯一,避免并列时行序不稳定;RANGE 与 ROWS 语义不同,RANGE 对并列值一起纳入窗口,用之前想清楚要哪种;对 text 大字段做 DISTINCT 要小心内存。先在小数据集上验证结果,再上生产跑。

三条纪律
  • 过滤窗口结果用子查询再过滤。
  • 排序ORDER BY 字段尽量唯一稳定。
  • 合并相同窗口定义合并,减少排序。