为什么需要窗口函数
日常 SQL 里,分组聚合 group by 会把多行压成一行,想在同一行既看到明细又看到组内统计就做不到了。排行榜要排名、报表要累计、环比要上一期值,这些需求用子查询与自连接也能写,但 SQL 又长又难读,性能还差。窗口函数让"每一行都带上一组统计上下文"成为可能,是分析场景的瑞士军刀。
窗口函数也叫分析函数,它不改变行数,只是给每一行附加一个基于窗口计算的列。理解窗口函数的关键是三个概念:分区 PARTITION BY 决定窗口按什么分组,排序 ORDER BY 决定窗口内的顺序,框架 ROWS/RANGE 决定窗口的边界范围。
- 窗口为每行划定的一组相关行。
- 排名RANK、DENSE_RANK 与 ROW_NUMBER。
- 移动LAG、LEAD 与移动平均。
- 聚合SUM、AVG 配合窗口边界。
窗口函数基础语法
窗口函数的语法骨架是:函数名 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 字段尽量唯一稳定。
- 合并相同窗口定义合并,减少排序。
落地清单
窗口函数是分析型 SQL 的基本功,值得系统练一遍:先掌握 OVER 语法与三种排名,再练移动聚合与 LAG/LEAD,最后挑战分组 Top N 与同比环比。写完后用 EXPLAIN 看执行计划,确认排序是否走了索引、有没有多余的全表扫描。
把它沉淀成团队的 SQL 规范:报表查询优先窗口函数而不是多层子查询;涉及"上一行、累计、排名"的场景统一用窗口函数;窗口函数不能进 WHERE,统一包子查询。规范一立,报表 SQL 的可读性与性能都会上一个台阶。
- 排名、累计、移动计算优先窗口函数。
- 窗口结果过滤必须包子查询。
- 相同窗口定义合并复用。
- 用 EXPLAIN 验证排序与扫描。