MySQL中的窗口函数

一、窗口函数简介

窗口函数 (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:必需,定义排序规则,决定行的编号顺序
工作原理
  1. 首先按照 PARTITION BY 对数据进行分组(如果指定)
  2. 在每个分组内按照 ORDER BY 对数据进行排序
  3. 为每一行分配一个唯一的连续序号(从1开始)
  4. 即使值相同,也会分配不同的行号
使用示例
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:必需,定义排序规则,决定行的排名顺序

工作原理
  1. 首先按照 PARTITION BY 对数据进行分组(如果指定)
  2. 在每个分组内按照 ORDER BY 对数据进行排序
  3. 为每一行分配排名序号:
    • 相同值获得相同排名
    下一个不同值会跳过相应的序号(如两个第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开始

特点
  1. 相同值获得相同排名
  2. 会跳过后续序号(如两个第2名后,下一个是第4名)
  3. 可能有"**排名空缺"**现象
  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:必需,定义排序规则,决定行的排名顺序

工作原理
  1. 首先按照 PARTITION BY 对数据进行分组(如果指定)
  2. 在每个分组内按照 ORDER BY 对数据进行排序
  3. 为每一行分配排名序号:
    • 相同值获得相同排名
    • 下一个不同值会继续使用下一个连续序号(不会跳过数字)
使用示例
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

特点

  1. 相同值获得相同排名
  2. 不会跳过后续序号(如三个第2名后,下一个是第3名
  3. 排名序号是连续的
  4. 适合需要保持排名连续性的场景
实际应用场景

• 学生成绩排名(不允许有名次空缺)

• 比赛奖项设置(金牌1名,银牌可多名,铜牌可多名)

• 销售业绩排名(相同业绩相同名次,但保持名次连续)

• 任何需要密集排名的场景

典型应用案例
  1. 学生考试密集排名(不允许跳过名次)
sql 复制代码
SELECT 
    student_name,
    score,
    DENSE_RANK() OVER (ORDER BY score DESC) AS rank
FROM exam_results;
  1. 奥运会奖牌密集排名(允许多个并列)
sql 复制代码
SELECT 
    country,
    gold_medals,
    DENSE_RANK() OVER (ORDER BY gold_medals DESC) AS rank
FROM medal_table;
  1. 部门薪资密集排名
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组

  1. 计算基础行数:
    • 10 ÷ 4 = 2 余 2
    • 基础每组行数 = 2行
    • 余数 = 2行
  2. 分配余数:
    • 余数2表示需要给2个组各多加1行
    • 按照NTILE()的规则,余数总是从第1组开始分配
  3. 最终分配:
    • 第1组:基础2行 + 1行(余数) = 3行
    • 第2组:基础2行 + 1行(剩余余数) = 3行
    • 第3组:基础2行 (余数已分配完) = 2行
    • 第4组:基础2行 = 2行
特点
  1. 当总行数不能被桶数整除时,前面的桶会比后面的桶多1行
  2. 常用于数据分析中的分位数计算(如四分位、十分位等)
  3. 结果值范围是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"(领导者)→ 看​​未来​​数据

​​命名逻辑​​:

  1. 函数向前查看(指向当前行之后的"未来"数据)
  2. 类似"领头羊"的概念,访问的是后续行(领先于当前行的数据)
  3. 例如: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:必需,定义窗口中的行顺序

工作原理
  1. 按照PARTITION BY和ORDER BY对数据进行排序
  2. 对每一行,返回其后第offset行的指定列值
  3. 如果不存在后续行(窗口末尾),返回default_value或NULL
使用示例
sql 复制代码
-- 获取每个员工的下一个更高薪资
SELECT 
    employee_name,
    salary,
    LEAD(salary, 1) OVER (ORDER BY salary) AS next_higher_salary
FROM employees;
特点
  1. 正向偏移(向后查找)
  2. 可自定义偏移量
  3. 可处理分区边界
  4. 默认返回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], ...
)
特点
  1. 反向偏移(向前查找)
  2. 其他特性与LEAD()类似
实际应用场景

• 计算日环比/月环比

• 检测数据异常波动

• 计算移动平均值


FIRST_VALUE(列名)

FIRST_VALUE() 返回窗口框架中的第一个值 ,常用于获取分区的起始值作为基准。

基本语法
sql 复制代码
FIRST_VALUE(column_name) OVER (
    [PARTITION BY partition_expression, ...]
    ORDER BY sort_expression [ASC|DESC]
    [frame_clause]
)
特点
  1. 总是返回窗口的第一个值
  2. 受窗口框架定义影响
  3. 常用于基准比较
实际应用场景

• 计算与分区首项的差值

• 跟踪从起始点开始的变化


LAST_VALUE(列名)

LAST_VALUE() 返回窗口框架中的最后一个值,但需注意默认窗口框架的行为。

基本语法
sql 复制代码
LAST_VALUE(column_name) OVER (
    [PARTITION BY partition_expression, ...]
    ORDER BY sort_expression [ASC|DESC]
    [frame_clause]
)
特点
  1. 默认行为可能不符合预期(因为默认框架是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
  2. 通常需要显式指定窗口框架
实际应用场景

• 计算累计到当前的最新值

• 需要配合明确窗口框架使用


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]
)
特点
  1. 可以获取任意位置的数值
  2. 如果位置超出范围返回NULL
  3. 同样受窗口框架影响
实际应用场景

• 获取中位数或其他分位数

• 比较当前值与特定位置的值

• 复杂的分位数分析


总结

所有偏移函数都支持:

  1. PARTITION BY子句进行分组计算
  2. ORDER BY子句定义窗口顺序
  3. 框架规范定义窗口范围
  4. 默认值处理选项

典型应用模式包括:移动平均、差值计算、趋势分析、缺口检测等数据分析场景。

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,你还可以用 ​​ROWSRANGE​​ 来定义:

  1. 计算时包含哪些行(例如:当前行 + 前3行)
  2. 是否包含当前行
  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;

关键点解析

  1. RANGE vs ROWS
    RANGE:按逻辑范围计算(相同日期的行会被合并统计)。
    ROWS:按物理行计算(严格按行数计算,即使日期相同也会分开统计)。
  2. UNBOUNDED PRECEDING
    • 表示从分区的第一行开始计算(这里是按日期排序后的最早日期)。
  3. 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
)
相关推荐
5173 分钟前
pymysql
java·数据库·oracle
2022计科一班唐文30 分钟前
数据库所有知识
数据库·mysql
Johny_Zhao1 小时前
Oracle、MySQL、SQL Server、PostgreSQL、Redis 五大数据库的区别
linux·redis·sql·mysql·信息安全·oracle·云计算·shell·yum源·系统运维
木木子99991 小时前
SQL中的Subquery & CTE & Temporary Table 区别
数据库·sql
钢铁男儿1 小时前
C# 类的基本概念(从类的内部访问成员和从类的外部访问成员)
java·数据库·c#
deepdata_cn3 小时前
开源分布式数据库(TiDB)
数据库·分布式
在未来等你3 小时前
互联网大厂Java面试:从Spring到微服务的技术探讨
数据库·spring boot·微服务·java面试·技术栈·互联网大厂
小小不董3 小时前
Oracle RAC ‘Metrics Global Cache Blocks Lost‘告警解决处理
linux·服务器·数据库·oracle·dba
江沉晚呤时4 小时前
深入解析 SqlSugar 与泛型封装:实现通用数据访问层
数据结构·数据库·oracle·排序算法·.netcore