SQL 窗口函数:从 ROW_NUMBER 到滑动窗口

SQL 窗口函数四类用法示意图

窗口函数是在不折叠原始行的前提下做分组计算。它与 GROUP BY 的根本区别是:GROUP BY 把多行压成一行,窗口函数保留全部原始行,再把计算结果附加到每一行上。

SQL 窗口函数四类用法示意图

GROUP BY 与窗口函数的区别

GROUP BY 输出的是聚合后的行,窗口函数输出的是原始行 + 计算列。

-- GROUP BY:每个部门一行,看不到具体员工
SELECT dept, AVG(salary) AS avg_salary
FROM employee
GROUP BY dept;

-- 窗口函数:每个员工一行,同时带上部门平均
SELECT name, dept, salary,
       AVG(salary) OVER (PARTITION BY dept) AS dept_avg
FROM employee;

这个能力解决了一类此前必须靠自连接或子查询才能解决的问题:把聚合值与原行放在一起比较——「每个员工的薪资与部门平均差多少」。

第一类:排名函数

三个排名函数的差别只在「遇到并列时怎么办」:ROW_NUMBER 不并列、RANK 并列后跳号、DENSE_RANK 并列后不跳号。

SELECT name, score,
       ROW_NUMBER() OVER (ORDER BY score DESC) AS row_no,
       RANK()       OVER (ORDER BY score DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rnk
FROM student;

假设分数为 100, 95, 95, 90:

score ROW_NUMBER RANK DENSE_RANK
100 1 1 1
95 2 2 2
95 3 2 2
90 4 4 3

「取每门课前 3 名」用哪个?用 RANK 能保留并列第三名(返回 4 行);用 ROW_NUMBER 会强制截断到恰好 3 行,但并列者谁入选是任意的。选择取决于业务是否接受「并列同分却被排除」。

第二类:取前后行(LAG / LEAD)

LAG 取当前行之前第 N 行的值,LEAD 取之后第 N 行,用于计算环比、同比、间隔。

SELECT month, revenue,
       LAG(revenue, 1)  OVER (ORDER BY month) AS prev_month,
       revenue - LAG(revenue, 1) OVER (ORDER BY month) AS mom_change,
       LEAD(revenue, 1) OVER (ORDER BY month) AS next_month
FROM monthly_revenue;

第一行没有「上一行」,所以 LAG 返回 NULL——计算增长率时要处理这个 NULL,否则整个计算会变成 NULL。

-- 环比增长率,首行用 NULLIF 避免除零
SELECT month, revenue,
       ROUND(
         (revenue - LAG(revenue) OVER (ORDER BY month))::numeric
         / NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2
       ) AS mom_pct
FROM monthly_revenue;

第三类:聚合窗口(累计与移动平均)

在 OVER 里加 ORDER BY 会定义「帧」,默认是从第一行到当前行——这正好是累计求和。

SELECT day, amount,
       -- 累计求和:默认帧 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       SUM(amount) OVER (ORDER BY day) AS running_total,

       -- 7 日移动平均:显式指定帧
       AVG(amount) OVER (
         ORDER BY day
         ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
       ) AS ma7
FROM daily_sales;

不写 ORDER BY 的聚合窗口是「整组聚合」,写了的才是「累计」。这个差别极易遗漏:

SUM(x) OVER (PARTITION BY dept)                     -- 部门总额(每行相同)
SUM(x) OVER (PARTITION BY dept ORDER BY day)        -- 部门内累计
SUM(x) OVER (PARTITION BY dept ORDER BY day
             ROWS BETWEEN UNBOUNDED PRECEDING
                      AND UNBOUNDED FOLLOWING)      -- 又是部门总额

第四类:分桶与百分位

NTILE 把结果按顺序分成 N 桶,PERCENT_RANK 给出相对位置,CUME_DIST 给出累积分布。

SELECT name, score,
       NTILE(4)   OVER (ORDER BY score DESC) AS quartile,
       PERCENT_RANK() OVER (ORDER BY score DESC) AS pct_rank,
       CUME_DIST()    OVER (ORDER BY score DESC) AS cum_dist
FROM student;

NTILE 常用于分层抽样——按某个指标分成四组,每组抽同样数量,保证样本覆盖全部分布区间。

PARTITION BY:分组的边界

PARTITION BY 定义「在哪个范围内排名 / 累计」,不影响输出行数。

-- 每门课程内排名,而不是全表排名
SELECT student_id, course_id, score,
       RANK() OVER (PARTITION BY course_id ORDER BY score DESC) AS rnk_in_course
FROM enrollment;

-- 多个分区键
SELECT region, product, month, sales,
       SUM(sales) OVER (PARTITION BY region, product ORDER BY month) AS running
FROM sales_fact;

帧定义:ROWS 与 RANGE 的区别

ROWS 按物理行数计算边界,RANGE 按排序值计算——在存在并列值时两者结果不同。

-- ROWS:严格取前 2 行 + 当前行
SUM(x) OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)

-- RANGE:取「值在当前值减 2 以内」的所有行,并列值会一并包含
SUM(x) OVER (ORDER BY day RANGE BETWEEN 2 PRECEDING AND CURRENT ROW)

做移动平均时通常用 ROWS(想要「最近 N 条记录」),做日期区间汇总时用 RANGE(想要「最近 N 天」)。

执行顺序与常见限制

窗口函数在 WHERE / GROUP BY / HAVING 之后、ORDER BY 之前执行,所以不能写在 WHERE 里。

-- ❌ 错误:窗口函数不能出现在 WHERE 中
SELECT * FROM (
  SELECT name, RANK() OVER (ORDER BY score DESC) AS rnk FROM student
) t WHERE rnk <= 3;   -- 这里 rnk 是子查询的输出列,可以筛选

-- ✅ 正确:用子查询或 CTE 包一层
WITH ranked AS (
  SELECT name, score, RANK() OVER (ORDER BY score DESC) AS rnk FROM student
)
SELECT * FROM ranked WHERE rnk <= 3;

常见错误

  • 把窗口函数写在 WHERE 里(应包一层子查询)。
  • 以为 ROW_NUMBER 会自动处理并列(它不会,顺序任意)。
  • 忘记 ORDER BY 却期待累计求和。
  • LAG 首行 NULL 未处理,导致增长率整列为空。
  • 移动平均没指定帧,结果与预期不同。
  • PARTITION BY 写错,变成全表排名。
  • 在 GROUP BY 查询里误用窗口函数(两者作用层级不同)。
  • MySQL 8.0 以下不支持窗口函数(需用自连接模拟)。

常见问题

窗口函数和 GROUP BY 能一起用吗?

可以,但要注意顺序:GROUP BY 先聚合,窗口函数在聚合结果上再计算。例如「先按部门汇总总额,再给部门总额排名」——外层用窗口函数,内层用 GROUP BY。

MySQL 5.7 没有窗口函数怎么办?

用变量或自连接模拟排名:SELECT a.*, (SELECT COUNT(*) FROM t b WHERE b.score > a.score) + 1 AS rnk FROM t a。这种写法在数据量大时很慢,能升级到 8.0 就升级。

RANK 和 DENSE_RANK 到底该用哪个?

看业务是否允许「名次跳跃」。评奖通常用 RANK(两个并列第二,下一个是第四);分级(如 ABC 三档)用 DENSE_RANK(两个第二,下一个是第三)。

为什么我的窗口函数结果和预期不一样?

三个高频原因:忘了 ORDER BY(导致默认整组聚合而不是累计)、PARTITION BY 写错(分组范围不对)、帧定义默认是 RANGE 而不是 ROWS(并列值处理不同)。逐项检查这三点。

窗口函数性能怎么样?

通常比等价的子查询快,因为它只需一次扫描。但 PARTITION BY 涉及排序,数据量大时要注意是否有对应索引。用 EXPLAIN 看执行计划里有没有额外的 sort 步骤。

可以在窗口函数里用 DISTINCT 吗?

大多数数据库不支持 COUNT(DISTINCT x) OVER (...)。变通做法是先按分区键去重再计数,或用 DENSE_RANK 等技巧模拟。PostgreSQL 支持部分场景,需查对应版本文档。

想搞懂 SQL 的执行顺序?

窗口函数能不能用、写在哪里,取决于 SQL 的执行顺序(FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY)。如果你对这条链路还不清楚,可以看看学习辅导——用具体的查询例子讲清楚每一步发生了什么。

相关案例与服务

需要有人帮你把这门作业做完?

把作业要求直接发过来,通常 10 分钟内回复给出报价与交付时间。不满意不接单。

每天 09:00 – 24:00(北京时间) | hecrereed@163.com

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

需要代写帮助?

通常 10 分钟内回复

邮箱 hecrereed@163.com 填写需求,免费报价 →
每天 09:00 – 24:00(北京时间)