窗口函数

SQL 笔试与手撕题教程

面试高频
窗口函数是大厂数据分析岗笔试中考得最多的知识点,字节、美团、拼多多几乎每次都会出。Top-N 问题环比增长是最常见的两类题型。

窗口函数是什么

窗口函数对一组行执行计算,但不像 GROUP BY 那样把多行折叠成一行。它保留每一行的粒度,把计算结果作为新列追加上去。

你可以理解为:既能看到每一行的明细,又能同时拿到分组的统计值

核心语法

SELECT
    column1,
    <函数>(expr) OVER (
        [PARTITION BY col_a]
        [ORDER BY col_b DESC]
        [ROWS BETWEEN ... AND ...]
    ) AS new_col
FROM table_name;

三个关键子句:

常用帧写法

写法含义
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW累计:从分区开头到当前行
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING滑动:前一行 + 当前行 + 后一行
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW最近 5 行

注意默认行为:有 ORDER BY 时默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(累计语义);没有 ORDER BY 时默认帧覆盖整个分区。

排名函数

三个排名函数的区别,假设某组有四个值 100、90、90、80:

函数结果特点
ROW_NUMBER()1, 2, 3, 4永远不并列
RANK()1, 2, 2, 4并列后跳号
DENSE_RANK()1, 2, 2, 3并列后不跳号

面试选哪个?要求"严格 N 行"用 ROW_NUMBER,要求"包含所有并列"用 RANK,要求"N 个不同名次"用 DENSE_RANK

还有 NTILE(n),把分区均匀分成 n 桶并返回桶号,做分位数分组时很好用。

聚合函数做窗口函数

SUMAVGCOUNTMINMAX 都能跟 OVER() 搭配。

-- 每个员工的销售额占部门总额的比例
SELECT emp_id, dept_id, sales,
    sales * 1.0 / SUM(sales) OVER(PARTITION BY dept_id) AS pct
FROM employee_sales;

这里没写 ORDER BY,所以 SUM 是对整个分区求和。加上 ORDER BY 就变成累计:

-- 按日期累计
SELECT sale_date, amount,
    SUM(amount) OVER(ORDER BY sale_date) AS cumulative
FROM sales;

位移函数

LAST_VALUE 的坑:默认帧到当前行为止,所以它返回的其实是当前行自己的值。想拿分区最后一行,要显式写帧:

LAST_VALUE(salary) OVER(
    PARTITION BY dept_id ORDER BY hire_date
    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
)

练习

练习

每组 Top 3

给定 employees 表,找出每个部门工资前 3 名的员工。如果第 3 名有并列,全部保留。

提示:并列全保留 → 用 RANK 或 DENSE_RANK

SQL 编辑器

点击「运行」查看查询结果

查看答案
参考答案
SELECT * FROM (
  SELECT emp_id, name, dept_id, salary,
    DENSE_RANK() OVER(PARTITION BY dept_id ORDER BY salary DESC) AS rnk
  FROM employees
) ranked
WHERE rnk <= 3;
解析

DENSE_RANK 保证并列后名次不跳号。如果某部门前两名并列,第三个人拿到的是 rank=2,不会被跳到 3,所以 <= 3 能正确保留三个不同薪资档次的所有人。

练习

7 日移动平均

给定 sales 表,计算每天的 7 日移动平均销售额(包含当天在内的最近 7 天)。

提示:用 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW

SQL 编辑器

点击「运行」查看查询结果

查看答案
参考答案
SELECT sale_date, amount,
  ROUND(AVG(amount) OVER(
    ORDER BY sale_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ), 1) AS ma7
FROM sales;
解析

6 PRECEDING + CURRENT ROW = 7 行。前 6 天数据不足 7 行时,AVG 会自动只算已有的行,不需要额外处理。

练习

环比增长率

给定 sales 表,计算每日销售额相比前一天的增长率(百分比),第一天显示 NULL。

提示:LAG 取前一天,注意除以零的情况

SQL 编辑器

点击「运行」查看查询结果

查看答案
参考答案
SELECT sale_date, amount,
  LAG(amount) OVER(ORDER BY sale_date) AS prev_amount,
  CASE
    WHEN LAG(amount) OVER(ORDER BY sale_date) IS NULL THEN NULL
    WHEN LAG(amount) OVER(ORDER BY sale_date) = 0 THEN NULL
    ELSE ROUND((amount * 1.0 / LAG(amount) OVER(ORDER BY sale_date) - 1) * 100, 1)
  END AS growth_pct
FROM sales;
解析

第一天 LAG 返回 NULL,直接显示 NULL。前一天为 0 时除法没意义,也返回 NULL。* 1.0 避免整数除法截断。