窗口函数
窗口函数的本质 = 在"分组后的有序数据集"上做计算,但不丢行(不像 GROUP BY)
基础结构:
mysql
函数() OVER (
PARTITION BY 分组
ORDER BY 排序
)
-- PARTITION BY → 分组(类似 GROUP BY,但不聚合)
-- ORDER BY → 分组内排序
-- 不写 PARTITION → 全表一个窗口
ROW_NUMBER()
去重取一条(去重保留最优记录)
mysql
SELECT * FROM (
SELECT t.*, ROW_NUMBER() OVER ( PARTITION BY 分组键 ORDER BY 优先级/时间/质量 DESC) AS rn
FROM t
) s WHERE rn = 1;
-- PARTITION BY = 你要"去重"的维度(如 code、user_id)
-- ORDER BY = 你定义的"谁更优"(时间最新、优先级最小/最大、评分最高等)
-- ROW_NUMBER 保证唯一(不会并列)
建模思想
Step 1:列出"候选来源",统一为一张"候选表"
Step 2:设计 sorting(命中精度、业务优先级、时效性、质量/权重)最终转成一个可排序的字段
Step 3:统一排序规则,单字段(ORDER BY sorting ASC),多字段(ORDER BY match_level ASC, update_time DESC )
Step 4:row_number 取最优
RANK()
排名(允许并列,但会跳过后续排名)
mysql
SELECT
t.*,
RANK() OVER (PARTITION BY 分组键 ORDER BY 排序字段 DESC) AS rk
FROM t;
-- 与 ROW_NUMBER 的区别:
-- 排序值相同时 → 并列排名(相同名次)
-- 并列后 → 跳过下一个名次(如 1,1,3,4)
-- 适用于:排行榜、竞赛排名、需要"并列但留空位"的场景
典型场景:取每组前 N 名(允许并列)
mysql
SELECT * FROM (
SELECT
t.*,
RANK() OVER (PARTITION BY 部门 ORDER BY 销售额 DESC) AS rk
FROM 销售表 t
) s WHERE rk <= 3;
-- 如果第3名有并列,会返回多个第3名
-- 但第4名会被跳过(因为并列占用了名次)
DENSE_RANK()
密集排名(允许并列,但不跳过排名)
mysql
SELECT
t.*,
DENSE_RANK() OVER (PARTITION BY 分组键 ORDER BY 排序字段 DESC) AS drk
FROM t;
-- 与 RANK 的区别:
-- 排序值相同时 → 并列排名(相同名次)
-- 并列后 → 不跳过下一个名次(如 1,1,2,3)
-- 适用于:需要连续排名、不关心空位的场景
典型场景:取每组前 N 名(密集版)
mysql
SELECT * FROM (
SELECT
t.*,
DENSE_RANK() OVER (PARTITION BY 班级 ORDER BY 总分 DESC) AS drk
FROM 成绩表 t
) s WHERE drk <= 3;
-- 如果第1名有并列,第2名仍然存在(不会被跳过)
-- 适合"我要前3档"而不是"前3个人"
三种排名函数对比
| 函数 | 并列时 | 排名是否连续 | 典型用途 |
|---|---|---|---|
| ROW_NUMBER | 不并列(随机/按其他字段) | 连续 | 去重取一条 |
| RANK | 并列 | 不连续(跳过) | 排行榜、竞赛 |
| DENSE_RANK | 并列 | 连续 | 分档、等级划分 |
- ROW_NUMBER() :生成连续唯一的序号,即使数据相同也强制排序。核心用途:数据去重,从一组重复记录中按规则(如时间最新、优先级最高)选取唯一的一条。
- RANK() :允许并列排名,并列后会跳过后续名次(如 1,1,3)。核心用途:传统排行榜、竞赛排名,需要体现"并列但名次稀缺"的场景。
- DENSE_RANK() :允许并列排名,但名次连续不跳过(如 1,1,2)。核心用途:等级划分、分档评级,关注"档位"而非具体名次。
选择指南 :需要唯一序号选 ROW_NUMBER;需要传统排名(允许跳名次)选 RANK;需要连续档位选 DENSE_RANK。
SUM()
累计值
mysql
SELECT
t.*,
SUM(变动量) OVER (
PARTITION BY 账户/物料
ORDER BY 时间
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS balance
FROM t;
-- ORDER BY 时间 决定累计顺序
-- 窗口框架 UNBOUNDED PRECEDING → CURRENT ROW = 从开头累计到当前
-- 分组后各自独立累计
- SUM() OVER(...) :最典型的累计计算。通过
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW框架,实现从分区开头到当前行的累计,常用于计算余额、累计销售额、累计用户数等。关键 :ORDER BY决定了累计的顺序。
LAG()
对比前一条(环比/差值/增长率)
mysql
SELECT
t.*,
LAG(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) AS prev_val,
指标 - LAG(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) AS diff,
CASE
WHEN LAG(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间) = 0 THEN NULL
ELSE (指标 - LAG(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间))
/ LAG(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间)
END AS growth_rate
FROM t;
-- LAG(col, 1) 取上一行
-- 用于:环比、涨跌幅、相邻差值
-- 注意除零保护
LEAD()
对比后一条(下期值/未来值/趋势判断)
mysql
SELECT
t.*,
LEAD(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) AS next_val,
LEAD(指标, 1) OVER (PARTITION BY 分组键 ORDER BY 时间) - 指标 AS next_diff,
CASE
WHEN 指标 = 0 THEN NULL
ELSE (LEAD(指标,1) OVER (PARTITION BY 分组键 ORDER BY 时间) - 指标) / 指标
END AS next_growth_rate
FROM t;
-- LEAD(col, 1) 取下一行(与 LAG 相反)
-- 用于:下期预测、趋势判断、环比下期
-- 注意最后一行 LEAD 返回 NULL
典型场景:判断趋势(涨/跌/平)
mysql
SELECT
t.*,
CASE
WHEN LEAD(价格, 1) OVER (PARTITION BY 商品 ORDER BY 日期) > 价格 THEN '上涨'
WHEN LEAD(价格, 1) OVER (PARTITION BY 商品 ORDER BY 日期) < 价格 THEN '下跌'
WHEN LEAD(价格, 1) OVER (PARTITION BY 商品 ORDER BY 日期) = 价格 THEN '持平'
ELSE '最后一天'
END AS 趋势
FROM 价格表 t;
LAG 与 LEAD 对比
| 函数 | 方向 | 用途 |
|---|---|---|
| LAG | 取上一行(过去) | 环比、同比、差值 |
| LEAD | 取下一行(未来) | 趋势预测、下期对比 |
- LAG(列, N) :访问当前行之前 第 N 行的数据。核心用途:计算环比(本期 vs 上期)、计算差值、计算增长率。务必注意处理边界值(NULL)和除零问题。
- LEAD(列, N) :访问当前行之后 第 N 行的数据。核心用途:进行趋势预测(与下期对比)、计算未来差值、判断价格或指标的涨跌趋势。
LAG vs LEAD:一个回头看(分析历史),一个向前看(预测未来),是时间序列和趋势分析的基础。
ROWS vs RANGE
本文示例主要使用了 ROWS 物理行框架。另一个重要概念是 RANGE 逻辑值框架:
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING:取前后各一行。RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW:取当前行值之前7天内的所有行(按值范围)。
RANGE在处理时间范围累计时更为直观。
性能与最佳实践
- 索引 :在
PARTITION BY和ORDER BY的列上建立索引可以大幅提升窗口函数性能。 - 避免嵌套:尽量避免在子查询中多层嵌套窗口函数,考虑使用 CTE(公用表表达式)来分步计算,提升可读性和可调试性。
- 理解执行顺序 :窗口函数在
SELECT列表计算,但在WHERE,GROUP BY,HAVING之后。不能直接在WHERE中引用窗口函数列,需要嵌套子查询或使用 CTE。
总结
窗口函数是 SQL 中用于在"分组后的有序数据集"上进行计算而不聚合(不丢行)的强大工具。其核心在于 OVER() 子句,它定义了数据窗口的划分(PARTITION BY)和排序(ORDER BY)。
掌握窗口函数,能让你用声明式的 SQL 优雅地解决复杂的数据切片、对比、累计和排名问题,将许多原本需要应用程序循环处理的逻辑移回数据库层,极大地提升开发效率和执行性能。