一、窗口函数简介
窗口函数 (Window Functions)是 MySQL 8.0 及以上版本引入的一种强大功能,它允许用户在不减少查询结果行数 的情况下,对一组相关的行(称为"窗口")执行计算。
窗口函数特点
窗口函数是在查询结果的特定"窗口"(一组相关行)上执行计算的函数,它具备以下特点:
- 不折叠行:与GROUP BY不同,窗口函数会保留所有原始行
- 定义窗口 :通过OVER()子句 指定计算的数据范围("窗口")
- 逐行计算 :为每一行 返回一个基于其所在窗口的计算结果
基本语法
sql
窗口函数名([参数]) OVER (
[PARTITION BY 分区表达式, ...]
[ORDER BY 排序表达式 [ASC|DESC], ...]
[frame_clause]
)
与GROUP BY的关键区别
特性 | 窗口函数 | GROUP BY |
---|---|---|
行数 | 保持原行数 | 合并行 |
计算方式 | 基于窗口计算 | 基于分组计算 |
结果 | 每行都有计算结果 | 每组一行结果 |
使用场景 | 排名、累计、移动平均等分析需求 | 汇总统计、分组聚合 |
窗口函数的应用场景
掌握窗口函数可以让你解决许多传统SQL难以处理的分析问题,如:
- 计算同比/环比增长率
- 识别数据趋势
- 处理复杂的排名和分组分析
- 计算各种滑动窗口指标
二、窗口函数家族
1. 排名类函数
ROW_NUMBER()
ROW_NUMBER() 是 MySQL 窗口函数中的一种行号分配函数 ,用于为结果集中的每一行分配一个唯一的连续序号(从1开始)。它不考虑值是否相同,严格按照排序顺序编号。
基本语法
sql
ROW_NUMBER() OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC], ...
)
参数说明
PARTITION BY
:可选,定义分区,在每个分区内独立进行编号ORDER BY
:必需,定义排序规则,决定行的编号顺序
工作原理
- 首先按照 PARTITION BY 对数据进行分组(如果指定)
- 在每个分组内按照 ORDER BY 对数据进行排序
- 为每一行分配一个唯一的连续序号(从1开始)
- 即使值相同,也会分配不同的行号
使用示例
sql
-- 为员工按部门分组并按薪资降序编号
SELECT
department_id,
employee_name,
salary,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS dept_salary_rank
FROM employees;
示例数据表 (employees)
employee_id | employee_name | department_id | salary |
---|---|---|---|
1 | 张三 | 1 | 85000 |
2 | 李四 | 1 | 85000 |
3 | 王五 | 1 | 95000 |
4 | 赵六 | 2 | 75000 |
5 | 钱七 | 2 | 75000 |
6 | 孙八 | 2 | 105000 |
查询结果
department_id | employee_name | salary | dept_salary_rank |
---|---|---|---|
1 | 王五 | 95000 | 1 |
1 | 张三 | 85000 | 2 |
1 | 李四 | 85000 | 3 |
2 | 孙八 | 105000 | 1 |
2 | 赵六 | 75000 | 2 |
2 | 钱七 | 75000 | 3 |
特点
• 总是生成唯一的连续序号(即使排序值相同)
• 不跳过任何数字(与RANK()不同)
• 结果值范围是从1开始的无间隔整数序列
• 常用于需要严格排序的场景
与RANK()、DENSE_RANK()的区别
• ROW_NUMBER()
:相同值也分配不同序号(1,2,3,4,...)
• RANK()
:相同值分配相同序号,后续序号跳过(1,2,2,4,...)
• DENSE_RANK()
:相同值分配相同序号,后续序号不跳过(1,2,2,3,...)
实际应用场景
(1)分页查询(获取第N到M条记录)
问题:
传统 LIMIT 分页
在大数据量时性能差,且无法实现"按某列排序后取中间某段数据"
解决方案:
sql
-- 获取薪资排名第11-20名的员工
WITH ranked_employees AS (
SELECT
employee_name,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rank_num
FROM employees
)
SELECT * FROM ranked_employees
WHERE rank_num BETWEEN 11 AND 20;
优势:
• 先排序编号再筛选,比 LIMIT offset 性能更好
• 可以灵活获取任意区间的数据
(2)数据去重(保留每组的第一条记录
)
问题:表中有重复数据,需要每组保留一条(如相同产品ID保留最新记录)
解决方案:
sql
-- 每个部门保留薪资最高的一条记录
WITH deduplicated AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn
FROM employees
)
SELECT * FROM deduplicated WHERE rn = 1;
执行过程:
• 按部门分组,按薪资降序编号
• 只保留每组编号为1的记录
(3)生成唯一标识序号
问题:需要为结果集添加连续序号列,不受后续操作影响
解决方案:
sql
-- 为查询结果添加永久行号
SELECT
ROW_NUMBER() OVER (ORDER BY hire_date) AS serial_num,
employee_name,
hire_date
FROM employees;
特点:
• 比应用层生成序号更可靠
• 序号会随排序规则变化而变化
(4)需要严格排序的报表制作
问题:制作报表时要求严格的行顺序,且可能有并列情况
解决方案:
sql
-- 生成严格按薪资排序的报表(即使薪资相同也区分顺序)
SELECT
ROW_NUMBER() OVER (ORDER BY salary DESC) AS strict_rank,
employee_name,
salary,
department_name
FROM employees
JOIN departments USING (department_id);
对比:
• 如果用 RANK(),相同薪资会有相同名次
• ROW_NUMBER() 确保每行有唯一位置,适合打印报表
实际案例
(1)电商场景:获取每个品类销量前三的商品
sql
WITH product_ranking AS (
SELECT
product_id,
category_id,
sales_volume,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY sales_volume DESC
) AS rank_in_category
FROM products
)
SELECT * FROM product_ranking
WHERE rank_in_category <= 3;
(2)日志分析:获取每个用户最近一次登录记录
sql
WITH user_logins AS (
SELECT
user_id,
login_time,
ip_address,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY login_time DESC
) AS login_seq
FROM login_logs
)
SELECT * FROM user_logins WHERE login_seq = 1;
RANK()
RANK() 是 MySQL 窗口函数中的一种排名函数,用于为结果集中的每一行分配一个排名序号。与 ROW_NUMBER() 不同,RANK() 会为相同值的行分配相同的排名,并跳过后续序号(1,2,2,4...)。
RANK() 函数特别适合需要处理数值相同但又要反映真实排名位置的场景,是数据分析中常用的排名计算工具。
基本语法
sql
RANK() OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC], ...
)
参数说明
• PARTITION BY
:可选,定义分区,在每个分区内独立进行排名
• ORDER BY
:必需,定义排序规则,决定行的排名顺序
工作原理
- 首先按照 PARTITION BY 对数据进行分组(如果指定)
- 在每个分组内按照 ORDER BY 对数据进行排序
- 为每一行分配排名序号:
• 相同值获得相同排名
•下一个不同值会跳过相应的序号
(如两个第1名后,下一个是第3名)
使用示例
sql
-- 为员工按部门分组并按薪资排名
SELECT
department_id,
employee_name,
salary,
RANK() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS dept_salary_rank
FROM employees;
示例数据表 (employees)
employee_id | employee_name | department_id | salary |
---|---|---|---|
1 | 张三 | 1 | 85000 |
2 | 李四 | 1 | 85000 |
3 | 王五 | 1 | 95000 |
4 | 赵六 | 2 | 75000 |
5 | 钱七 | 2 | 75000 |
6 | 孙八 | 2 | 105000 |
7 | 周九 | 2 | 75000 |
查询结果
department_id | employee_name | salary | dept_salary_rank |
---|---|---|---|
1 | 王五 | 95000 | 1 |
1 | 张三 | 85000 | 2 |
1 | 李四 | 85000 | 2 |
2 | 孙八 | 105000 | 1 |
2 | 赵六 | 75000 | 2 |
2 | 钱七 | 75000 | 2 |
2 | 周九 | 75000 | 2 |
下一个不同值会从5开始
特点
- 相同值获得相同排名
- 会跳过后续序号(如两个第2名后,下一个是第4名)
- 可能有"**排名空缺"**现象
- 排名序号不一定连续
实际应用场景
• 比赛成绩排名(允许并列名次)
• 销售业绩排名(相同业绩相同名次)
• 学生考试成绩排名
• 任何需要处理并列情况的排名场景
典型应用案例
1. 销售团队月度业绩排名(允许并列)
sql
SELECT
salesperson,
sales_amount,
RANK() OVER (ORDER BY sales_amount DESC) AS sales_rank
FROM monthly_sales;
2. 奥运会奖牌榜排名
sql
SELECT
country,
gold_medals,
silver_medals,
bronze_medals,
RANK() OVER (ORDER BY gold_medals DESC, silver_medals DESC, bronze_medals DESC) AS rank
FROM medal_table;
3. 按地区分组的房价排名
sql
SELECT
region,
district,
average_price,
RANK() OVER (PARTITION BY region ORDER BY average_price DESC) AS price_rank_in_region
FROM housing_prices;
DENSE_RANK()
DENSE_RANK()
是 MySQL 窗口函数中的一种排名函数,用于为结果集中的每一行分配一个无间隔的排名序号 。与 RANK() 不同,DENSE_RANK() 会为相同值的行分配相同排名 ,但不会跳过后续序号(1,2,2,3...)。
DENSE_RANK() 函数特别适合需要处理数值相同但又要保持排名连续性的场景,是数据分析中常用的排名计算工具。
基本语法
sql
DENSE_RANK() OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC], ...
)
参数说明
• PARTITION BY
:可选,定义分区,在每个分区内独立进行排名
• ORDER BY
:必需,定义排序规则,决定行的排名顺序
工作原理
- 首先按照 PARTITION BY 对数据进行分组(如果指定)
- 在每个分组内按照 ORDER BY 对数据进行排序
- 为每一行分配排名序号:
• 相同值获得相同排名
• 下一个不同值会继续使用下一个连续序号(不会跳过数字)
使用示例
sql
-- 为员工按部门分组并按薪资密集排名
SELECT
department_id,
employee_name,
salary,
DENSE_RANK() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS dept_salary_rank
FROM employees;
示例数据表 (employees)
employee_id | employee_name | department_id | salary |
---|---|---|---|
1 | 张三 | 1 | 85000 |
2 | 李四 | 1 | 85000 |
3 | 王五 | 1 | 95000 |
4 | 赵六 | 2 | 75000 |
5 | 钱七 | 2 | 75000 |
6 | 孙八 | 2 | 105000 |
7 | 周九 | 2 | 75000 |
查询结果
department_id | employee_name | salary | dept_salary_rank |
---|---|---|---|
1 | 王五 | 95000 | 1 |
1 | 张三 | 85000 | 2 |
1 | 李四 | 85000 | 2 |
2 | 孙八 | 105000 | 1 |
2 | 赵六 | 75000 | 2 |
2 | 钱七 | 75000 | 2 |
2 | 周九 | 75000 | 2 |
特点
- 相同值获得相同排名
- 不会跳过后续序号(
如三个第2名后,下一个是第3名
) - 排名序号是连续的
- 适合需要保持排名连续性的场景
实际应用场景
• 学生成绩排名(不允许有名次空缺)
• 比赛奖项设置(金牌1名,银牌可多名,铜牌可多名)
• 销售业绩排名(相同业绩相同名次,但保持名次连续)
• 任何需要密集排名的场景
典型应用案例
- 学生考试密集排名(不允许跳过名次)
sql
SELECT
student_name,
score,
DENSE_RANK() OVER (ORDER BY score DESC) AS rank
FROM exam_results;
- 奥运会奖牌密集排名(允许多个并列)
sql
SELECT
country,
gold_medals,
DENSE_RANK() OVER (ORDER BY gold_medals DESC) AS rank
FROM medal_table;
- 部门薪资密集排名
sql
SELECT
department_name,
employee_name,
salary,
DENSE_RANK() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS salary_rank
FROM employees
JOIN departments USING (department_id);
NTILE()
NTILE() 是 MySQL 窗口函数中的一种分桶函数 ,用于将有序 数据集划分为大致相等的若干组 (桶),并为每一行分配其所属的组号 。NTILE() 适合需要将数据均匀分组
的场景。
基本语法
sql
NTILE(bucket_count) OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC], ...
)
参数说明
• bucket_count
:指定要将数据分成的桶数 (必须是正整数)
• PARTITION BY
:可选,定义分区,在每个分区内独立进行分桶
• ORDER BY
:必需,定义排序规则,决定数据如何分配到各桶
工作原理
- 首先按照 ORDER BY 对数据进行排序
- 然后将数据尽可能均匀地分配到指定数量的桶中(尽量让每组行数相同)
- 为每一行分配一个桶编号(从1开始)
- 不能整除时,前面的组多一行,余数从前往后分配
使用示例
sql
-- 将员工按薪水高低分成4个等级
SELECT
employee_name,
salary,
NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;
示例数据表 (employees)
employee_id | employee_name | salary |
---|---|---|
1 | 张三 | 85000 |
2 | 李四 | 65000 |
3 | 王五 | 95000 |
4 | 赵六 | 75000 |
5 | 钱七 | 55000 |
6 | 孙八 | 105000 |
7 | 周九 | 45000 |
8 | 吴十 | 80000 |
9 | 郑十一 | 70000 |
10 | 王十二 | 60000 |
查询结果
employee_name | salary | salary_quartile |
---|---|---|
孙八 | 105000 | 1 |
王五 | 95000 | 1 |
张三 | 85000 | 1 |
吴十 | 80000 | 2 |
赵六 | 75000 | 2 |
郑十一 | 70000 | 2 |
李四 | 65000 | 3 |
王十二 | 60000 | 3 |
钱七 | 55000 | 4 |
周九 | 45000 | 4 |
分配逻辑说明
数据总数:10行 ,分组数:4组
- 计算基础行数:
• 10 ÷ 4 = 2 余 2
• 基础每组行数 = 2行
• 余数 = 2行 - 分配余数:
• 余数2表示需要给2个组各多加1行
• 按照NTILE()的规则,余数总是从第1组开始分配 - 最终分配:
• 第1组:基础2行 + 1行(余数) = 3行
• 第2组:基础2行 + 1行(剩余余数) = 3行
• 第3组:基础2行 (余数已分配完) = 2行
• 第4组:基础2行 = 2行
特点
- 当总行数不能被桶数整除时,前面的桶会比后面的桶多1行
- 常用于数据分析中的分位数计算(如四分位、十分位等)
- 结果值范围是1到bucket_count的整数
实际应用场景
• 客户价值分层(高/中/低价值客户)
• 成绩等级划分
• 销售业绩分组排名
• 数据采样时均匀分组
2. 分析类函数
-
LEAD(列名, n)
:获取当前行后第n行的值 -
LAG(列名, n)
:获取当前行前第n行的值 -
FIRST_VALUE(列名)
:窗口第一个值 -
LAST_VALUE(列名)
:窗口最后一个值 -
NTH_VALUE(列名, n)
:窗口第n个值
LEAD(列名, n)
LEAD()
是 MySQL 窗口函数中的一种偏移函数 ,用于访问当前行之后的指定行数据 ,它允许在不使用自连接的情况下获取后续行的值,是数据分析中实现行间比较
的重要工具。
怎么记忆?
LEAD(向前/未来导向)
词源:英语中"lead"作动词意为"引领、领先 "
LEAD → 联想"Leader"(领导者)→ 看未来数据
命名逻辑:
- 函数向前查看(指向当前行之后的"未来"数据)
- 类似"领头羊"的概念,访问的是后续行(领先于当前行的数据)
- 例如:LEAD(salary, 1) 获取"下一个"更高薪资(领先当前行的值)
基本语法
sql
LEAD(column_name, offset, default_value) OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC], ...
)
参数说明
• column_name
:要获取后续值的列
• offset
:向后偏移的行数(默认为1)
• default_value
:当超出范围时返回的默认值(可选)
• PARTITION BY
:可选,定义分区范围
• ORDER BY
:必需,定义窗口中的行顺序
工作原理
- 按照PARTITION BY和ORDER BY对数据进行排序
- 对每一行,返回其后第offset行的指定列值
- 如果不存在后续行(窗口末尾),返回default_value或NULL
使用示例
sql
-- 获取每个员工的下一个更高薪资
SELECT
employee_name,
salary,
LEAD(salary, 1) OVER (ORDER BY salary) AS next_higher_salary
FROM employees;
特点
- 正向偏移(向后查找)
- 可自定义偏移量
- 可处理分区边界
- 默认返回NULL当超出范围
实际应用场景
• 计算环比增长率
sql
-- 计算月度销售额环比增长率(本月 vs 上月)
SELECT
month,
sales_amount AS current_month_sales,
LEAD(sales_amount, 1) OVER (ORDER BY month) AS next_month_sales,
ROUND(
(LEAD(sales_amount, 1) OVER (ORDER BY month) - sales_amount) /
sales_amount * 100,
2
) AS mom_growth_rate_percent
FROM monthly_sales
ORDER BY month DESC; -- 按月份降序显示最新月份在前
/* 结果示例:
month | current_month_sales | next_month_sales | mom_growth_rate_percent
-----------+---------------------+------------------+-----------------------
2023-04-01 | 125000 | 118000 | -5.60 (4月比3月下降5.6%)
2023-03-01 | 118000 | 135000 | 14.41 (3月比2月增长14.41%)
2023-02-01 | 135000 | 120000 | -11.11 (2月比1月下降11.11%)
2023-01-01 | 120000 | NULL | NULL (没有上个月数据)
*/
• 查找连续日期中的缺口
sql
-- 检测用户登录记录中的日期间断
WITH login_dates_with_next AS (
SELECT
user_id,
login_date,
LEAD(login_date, 1) OVER (PARTITION BY user_id ORDER BY login_date) AS next_login_date
FROM user_logins
)
SELECT
user_id,
login_date,
next_login_date,
DATEDIFF(next_login_date, login_date) AS days_between_logins
FROM login_dates_with_next
WHERE DATEDIFF(next_login_date, login_date) > 1 -- 只显示有间隔的记录
ORDER BY user_id, login_date;
/* 结果示例:
user_id | login_date | next_login_date | days_between_logins
--------+------------+-----------------+--------------------
1001 | 2023-01-05 | 2023-01-08 | 3 (1月5日后下次登录是8日,间隔3天)
1002 | 2023-02-10 | 2023-02-15 | 5 (2月10日后下次登录是15日,间隔5天)
*/
• 比较当前值与下一个值
sql
-- 比较股票连续交易日的涨跌幅(当日收盘价 vs 次日收盘价)
SELECT
stock_code,
trade_date,
close_price AS today_close,
LEAD(close_price, 1) OVER (PARTITION BY stock_code ORDER BY trade_date) AS next_day_close,
ROUND(
(LEAD(close_price, 1) OVER (PARTITION BY stock_code ORDER BY trade_date) - close_price) /
close_price * 100,
2
) AS daily_change_percent,
CASE
WHEN LEAD(close_price, 1) OVER (PARTITION BY stock_code ORDER BY trade_date) > close_price
THEN '上涨'
WHEN LEAD(close_price, 1) OVER (PARTITION BY stock_code ORDER BY trade_date) < close_price
THEN '下跌'
ELSE '平盘'
END AS price_movement
FROM stock_daily
WHERE stock_code = 'AAPL' -- 苹果公司股票
AND trade_date BETWEEN '2023-01-01' AND '2023-01-10'
ORDER BY trade_date DESC; -- 按日期降序显示最新日期在前
/* 结果示例:
stock_code | trade_date | today_close | next_day_close | daily_change_percent | price_movement
-----------+------------+-------------+----------------+----------------------+---------------
AAPL | 2023-01-10 | 142.53 | 143.86 | 0.93 | 上涨
AAPL | 2023-01-09 | 143.86 | 140.92 | -2.04 | 下跌
AAPL | 2023-01-06 | 140.92 | 129.50 | -8.10 | 下跌
AAPL | 2023-01-05 | 129.50 | 130.15 | 0.50 | 上涨
AAPL | 2023-01-04 | 130.15 | NULL | NULL | NULL
*/
LAG(列名, n)
LAG()
是 MySQL 窗口函数中的一种偏移函数 ,用于访问当前行之前 的指定行数据。它与LEAD()相反,常用于比较当前行与历史数据。
怎么记忆?
LAG
(向后/历史导向)
• 词源:英语中"lag"作动词意为"滞后、落后 "
• 命名逻辑:
• 函数向后查看(指向当前行之前的"历史"数据)
• 类似"拖后腿"的概念,访问的是先前行(滞后于当前行的数据)
• 例如:LAG(salary, 1)
获取"上一个"更低薪资(落后当前行的值)
LAG → 联想"Lag behind"(落后)→ 看历史数据
基本语法
sql
LAG(column_name, offset, default_value) OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC], ...
)
特点
- 反向偏移(向前查找)
- 其他特性与LEAD()类似
实际应用场景
• 计算日环比/月环比
• 检测数据异常波动
• 计算移动平均值
FIRST_VALUE(列名)
FIRST_VALUE()
返回窗口框架中的第一个值 ,常用于获取分区的起始值作为基准。
基本语法
sql
FIRST_VALUE(column_name) OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC]
[frame_clause]
)
特点
- 总是返回窗口的第一个值
- 受窗口框架定义影响
- 常用于基准比较
实际应用场景
• 计算与分区首项的差值
• 跟踪从起始点开始的变化
LAST_VALUE(列名)
LAST_VALUE() 返回窗口框架中的最后一个值,但需注意默认窗口框架的行为。
基本语法
sql
LAST_VALUE(column_name) OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC]
[frame_clause]
)
特点
- 默认行为可能不符合预期(因为默认框架是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
- 通常需要显式指定窗口框架
实际应用场景
• 计算累计到当前的最新值
• 需要配合明确窗口框架使用
NTH_VALUE(列名, n)
NTH_VALUE() 返回窗口中指定位置的第N个值,提供了更灵活的位置访问能力。
基本语法
sql
NTH_VALUE(column_name, N) OVER (
[PARTITION BY partition_expression, ...]
ORDER BY sort_expression [ASC|DESC]
[frame_clause]
)
特点
- 可以获取任意位置的数值
- 如果位置超出范围返回NULL
- 同样受窗口框架影响
实际应用场景
• 获取中位数或其他分位数
• 比较当前值与特定位置的值
• 复杂的分位数分析
总结
所有偏移函数都支持:
- PARTITION BY子句进行分组计算
- ORDER BY子句定义窗口顺序
- 框架规范定义窗口范围
- 默认值处理选项
典型应用模式包括:移动平均、差值计算、趋势分析、缺口检测等数据分析场景。
3. 聚合函数作为窗口函数
SUM() OVER()
AVG() OVER()
COUNT() OVER()
MAX() OVER()
MIN() OVER()
三、窗口定义三要素
窗口函数的强大之处在于可以精确控制"窗口"范围:
sql
函数() OVER(
[PARTITION BY 分组列]
[ORDER BY 排序列]
[ROWS/RANGE 框架]
)
1. PARTITION BY
将数据分成多个组,函数在每个组内独立计算(类似GROUP BY但不合并行)
2. ORDER BY
定义分区内的排序方式,影响序号分配和滑动窗口计算
3. 窗口框架
窗口框架(Window Frame) 是 SQL 窗口函数中一个 高级但极其有用 的功能,它允许你 更精细地控制计算范围,而不仅仅是按 PARTITION BY 分组或按 ORDER BY 排序。
窗口框架的作用
在 OVER() 子句中,除了 PARTITION BY 和 ORDER BY,你还可以用 ROWS 或 RANGE 来定义:
- 计算时包含哪些行(例如:当前行 + 前3行)
- 是否包含当前行
- 是否包含未来的行(FOLLOWING)
典型应用场景
✅ 移动平均(Moving Average)
✅ 累计计算(Running Total)
✅ 前后行对比(Lag/Lead 分析)
✅ 滑动窗口统计(如最近5天的总和)
基本语法
sql
SUM(column) OVER(
[PARTITION BY ...]
[ORDER BY ...]
ROWS|RANGE BETWEEN <start> AND <end>
)
- ROWS → 按 物理行 计算(固定行数)
- RANGE → 按 逻辑范围 计算(如相同值的行视为同一组)
窗口框架的边界选项
选项 | 含义 |
---|---|
UNBOUNDED PRECEDING |
从分区的第一行开始 |
n PRECEDING |
当前行之前的 n 行 |
CURRENT ROW |
当前行 |
n FOLLOWING |
当前行之后的 n 行 |
UNBOUNDED FOLLOWING |
直到分区的最后一行 |
3. 实际案例
案例1:计算3天移动平均(ROWS)
sql
SELECT
date, -- 日期列
revenue, -- 当天的收入
AVG(revenue) OVER( -- 计算收入的移动平均值
ORDER BY date -- 按日期排序
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 包含当前行 + 前2行
) AS moving_avg_3day -- 结果列名
FROM sales;
关键点:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
•
2 PRECEDING
= 当前行的前2行•
CURRENT ROW
= 当前行→ 合起来就是 当前行 + 前2行,共3行数据
假设数据是这样的:
date | revenue |
---|---|
2023-01-01 | 100 |
2023-01-02 | 150 |
2023-01-03 | 200 |
2023-01-04 | 250 |
查询结果会是:
date | revenue | moving_avg_3day | 计算方式 |
---|---|---|---|
2023-01-01 | 100 | 100.0 | (100) / 1(只有1天数据) |
2023-01-02 | 150 | 125.0 | (100 + 150) / 2 |
2023-01-03 | 200 | 150.0 | (100 + 150 + 200) / 3 |
2023-01-04 | 250 | 200.0 | (150 + 200 + 250) / 3 |
前几行不够怎么办?
• 比如第1天(2023-01-01),前面没有数据,就只算它自己。
• 第2天(2023-01-02),只有前1天的数据,就只算2天的平均。
案例2:计算累计到当前行的总和(RANGE)
sql
# 计算销售数据的累计收入(running total),也就是从最早日期到当前日期的收入总和
SELECT
date,
revenue,
SUM(revenue) OVER( #对 revenue 列求和
ORDER BY date #按日期排序
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
# 计算范围是从最早日期(UNBOUNDED PRECEDING)到当前行(CURRENT ROW)
) AS running_total
FROM sales;
关键点解析
RANGE
vsROWS
:
•RANGE
:按逻辑范围计算(相同日期的行会被合并统计)。
•ROWS
:按物理行计算(严格按行数计算,即使日期相同也会分开统计)。UNBOUNDED PRECEDING
:
• 表示从分区的第一行开始计算(这里是按日期排序后的最早日期)。CURRENT ROW
:
• 计算到当前行为止。
假设原始数据:
date | revenue |
---|---|
2023-01-01 | 100 |
2023-01-02 | 150 |
2023-01-03 | 200 |
查询结果:
date | revenue | running_total | 计算逻辑 |
---|---|---|---|
2023-01-01 | 100 | 100 | 100 |
2023-01-02 | 150 | 250 | 100 (前一天) + 150 |
2023-01-03 | 200 | 450 | 250 (前累计) + 200 |
案例3:计算当前行 + 前后各1行的总和(滑动窗口)
sql
SELECT
date,
revenue,
SUM(revenue) OVER(
ORDER BY date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS sliding_sum
FROM sales;
结果示例:
date | revenue | sliding_sum |
---|---|---|
2023-01-01 | 100 | 250 |
2023-01-02 | 150 | 450 |
2023-01-03 | 200 | 600 |
2023-01-04 | 250 | 450 |
4. ROWS
vs RANGE
的区别
类型 | 行为 | 适用场景 |
---|---|---|
ROWS |
按 物理行 计算(固定行数) | 移动平均、滑动窗口 |
RANGE |
按 逻辑范围 计算(相同值的行视为同一组) | 处理重复值(如相同日期的数据) |
示例(RANGE
处理相同日期的数据):
sql
SELECT
date,
revenue,
SUM(revenue) OVER(
ORDER BY date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM sales;
如果 2023-01-01
有2行数据(100 和 150),RANGE
会 同时计算这两行,而 ROWS
会逐行计算。
5. 总结
• 窗口框架(Window Frame) 让你可以 更灵活地定义计算范围,而不仅仅是按分区或排序计算。
• ROWS
→ 适用于 固定行数 的计算(如移动平均)。
• RANGE
→ 适用于 逻辑范围 的计算(如相同日期的数据)。
• 常见用途:移动平均、累计计算、滑动窗口统计。
四、实际应用示例
示例1:计算累计和
sql
# 计算销售数据的每日收入以及累计收入
SELECT
date,
revenue,
SUM(revenue) OVER(ORDER BY date) AS running_total
FROM sales;
示例2:计算同部门薪资排名
sql
SELECT
employee_name,
department,
salary,
RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;
示例3:计算3个月移动平均
sql
SELECT
month,
sales,
AVG(sales) OVER(ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg
FROM monthly_sales;
五、补充
(1)RANGE 和 ROWS 在窗口函数中的区别?
核心区别
• ROWS
= 按物理行计算(数"行数")
• RANGE
= 按逻辑范围计算(看"值的大小")
具体区别
特性 | ROWS |
RANGE |
---|---|---|
计算方式 | 按绝对行数 | 按排序字段的值范围 |
相同值处理 | 每行独立计算 | 相同值会被合并计算 |
性能 | 更快 | 较慢(需要额外计算) |
典型用途 | 移动平均、固定行数计算 | 处理重复值、按实际范围计算 |
实际例子说明
假设有这样的销售数据:
sql
-- 测试数据
INSERT INTO sales VALUES
('2023-01-01', 100),
('2023-01-02', 150),
('2023-01-02', 200), -- 注意这里有重复日期
('2023-01-03', 50);
使用ROWS的查询
sql
SELECT
date,
revenue,
SUM(revenue) OVER(
ORDER BY date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS rows_running_total
FROM sales;
结果:
date | revenue | rows_running_total
-----------+---------+-------------------
2023-01-01 | 100 | 100 ← 第1行
2023-01-02 | 150 | 250 ← 100+150 (第1+2行)
2023-01-02 | 200 | 450 ← 250+200 (第1+2+3行)
2023-01-03 | 50 | 500 ← 450+50
使用RANGE的查询
sql
SELECT
date,
revenue,
SUM(revenue) OVER(
ORDER BY date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS range_running_total
FROM sales;
结果:
date | revenue | range_running_total
-----------+---------+--------------------
2023-01-01 | 100 | 100 ← 只有这一天
2023-01-02 | 150 | 450 ← 100+150+200 (所有1月2日的数据)
2023-01-02 | 200 | 450 ← 同上(相同日期被合并计算)
2023-01-03 | 50 | 500 ← 450+50
关键区别图示
数据行: [2023-01-01/100] → [2023-01-02/150] → [2023-01-02/200] → [2023-01-03/50]
ROWS计算: 行1 行1+2 行1+2+3 行1+2+3+4
[100] [250] [450] [500]
RANGE计算: date<=1/1 date<=1/2 date<=1/3
[100] [100+150+200] [100+150+200+50]
[450]重复两次 [500]
什么时候用哪个?
**用 ROWS
**
• 需要计算固定行数(如"最近3行")
• 数据没有重复排序值
• 需要更高性能
用 RANGE
• 排序字段可能有重复值(如相同日期)
• 需要按实际值范围计算(如"所有小于当前值的行")
• 做时间序列分析时更准确
进阶技巧
可以组合使用:
sql
-- 计算当前日期及前2天(按日期范围)
SUM(revenue) OVER(
ORDER BY date
RANGE BETWEEN INTERVAL '2' DAY PRECEDING AND CURRENT ROW
)