MySQL 窗口函数详解

MySQL 自 8.0 版本起引入了窗口函数(Window Functions),它们允许在查询结果的特定“窗口”内对多行进行分析和计算。窗口函数不会将行压缩成一行,而是在保留每一行数据的基础上,增加计算的结果。窗口函数是进行高级数据分析和处理的强大工具,适用于累积计算、排名、滑动窗口计算等场景。

1. 窗口函数的基本概念

窗口函数是一种在 SQL 查询结果集中计算每一行的聚合值的函数。窗口函数的独特之处在于,它们不仅可以计算聚合值,还可以保留每一行的数据,并基于这些行进行进一步计算。窗口由 OVER() 子句定义,包含 PARTITION BY(分区)、ORDER BY(排序)和窗口帧。

2. 窗口函数的语法结构

窗口函数通常与 OVER() 子句结合使用,后者定义了计算的窗口范围和顺序。窗口函数的基本语法如下:

函数名 (参数) OVER (
    [PARTITION BY 分区列]
    [ORDER BY 排序列]
    [窗口帧定义]
)
  • PARTITION BY:指定数据的分区方式,类似于 GROUP BY,但不会聚合数据。
  • ORDER BY:指定在每个分区内的排序顺序。
  • 窗口帧定义:定义窗口的范围,即在计算时,考虑当前行周围的哪些行。

3. 常见的窗口函数分类及详细介绍

MySQL 提供了多种窗口函数,可以分为以下几类:

a. 排名函数
  • ROW_NUMBER():为每一行分配唯一的序号,按 ORDER BY 指定的顺序排列。
  • RANK():为每一行分配排名,相同值会有相同排名,但排名会跳跃。
  • DENSE_RANK():与 RANK() 类似,但排名不跳跃。
  • NTILE(n):将结果集划分为 n 个组,并为每行分配组号。

示例:

SELECT
    employee_id,
    department,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank
FROM
    employees;

此查询按部门对员工进行分区,并按薪资降序排列,计算每个员工在其部门内的排名。

b. 值函数
  • LAG(expr, offset, default):返回当前行之前某一行的值。
  • LEAD(expr, offset, default):返回当前行之后某一行的值。
  • FIRST_VALUE(expr):返回窗口内第一行的值。
  • LAST_VALUE(expr):返回窗口内最后一行的值。
  • NTH_VALUE(expr, N):返回窗口内第 N 行的值。

示例:

SELECT
    employee_id,
    salary,
    LAG(salary, 1, 0) OVER (ORDER BY salary DESC) AS prev_salary,
    LEAD(salary, 1, 0) OVER (ORDER BY salary DESC) AS next_salary
FROM
    employees;

此查询返回每个员工的薪资,以及其前一行和后一行员工的薪资。

c. 聚合窗口函数
  • SUM():计算窗口内所有值的总和。
  • AVG():计算窗口内所有值的平均值。
  • COUNT():计算窗口内的行数。
  • MIN():返回窗口内的最小值。
  • MAX():返回窗口内的最大值。

示例:

SELECT
    employee_id,
    department,
    salary,
    SUM(salary) OVER (PARTITION BY department) AS total_salary
FROM
    employees;

此查询计算每个部门的员工薪资总和。

d. 统计分布函数
  • PERCENT_RANK():计算当前行在分区中的百分比排名。
  • CUME_DIST():计算当前行在分区中的累积分布。

示例:

SELECT
    employee_id,
    department,
    salary,
    CUME_DIST() OVER (PARTITION BY department ORDER BY salary DESC) AS cum_dist
FROM
    employees;

此查询计算每个员工在其部门内薪资的累积分布值。

4. 窗口帧定义及用法

窗口帧定义了窗口函数计算的具体行范围。ROWSRANGE 是定义窗口帧的两个关键字:

  • ROWS:按物理行的数量来定义窗口范围。
  • RANGE:按逻辑值范围来定义窗口范围,通常与 ORDER BY 结合使用。

窗口帧子句:

ROWS | RANGE BETWEEN 窗口开始 AND 窗口结束
  • UNBOUNDED PRECEDING:从分区的第一行开始。
  • CURRENT ROW:当前行。
  • UNBOUNDED FOLLOWING:直到分区的最后一行。
  • n PRECEDING:当前行之前的第 n 行。
  • n FOLLOWING:当前行之后的第 n 行。

示例:

  1. 累计和计算:
SELECT
    employee_id,
    department,
    salary,
    SUM(salary) OVER (
        PARTITION BY department 
        ORDER BY salary DESC 
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_salary
FROM
    employees;

此查询计算每个部门员工薪资的累计和,即从部门内薪资最高的员工开始计算到当前行的累计和。

  1. 滑动窗口平均值:
SELECT
    employee_id,
    department,
    salary,
    AVG(salary) OVER (
        PARTITION BY department 
        ORDER BY salary ASC 
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    ) AS moving_avg
FROM
    employees;

此查询计算每个员工在其部门内的三行滑动平均薪资(前一行、当前行和后一行的平均值)。

5. 注意事项

  • 与聚合函数的区别:窗口函数不会聚合行,而是保留每一行的详细信息。
  • 性能问题:由于窗口函数计算的是整个窗口内的数据,因此在处理大数据集时,可能会导致性能问题。
  • ORDER BY 和窗口帧的影响:窗口函数的结果依赖于 ORDER BY 和窗口帧定义,错误的定义可能导致意外的结果。
  • 窗口帧的定义:特别是在滑动窗口计算中,要小心窗口帧的边界,确保计算结果符合预期。
  • NULL 处理:窗口函数对 NULL 值的处理方式可能会影响排序和计算结果,使用时需谨慎。

6. 实际应用场景示例

  1. 分区内排名:
SELECT
    product_id,
    category,
    price,
    RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rank
FROM
    products;

此查询返回每个分类内产品按价格降序的排名。

  1. 滑动窗口最大值:
SELECT
    sales_date,
    sales_amount,
    MAX(sales_amount) OVER (
        ORDER BY sales_date ASC 
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS max_sales
FROM
    sales;

此查询返回每个日期的销售额,以及包括当前日期和前两个日期的最大销售额。

7. 参考文献

以上是 MySQL 窗口函数的内容,涵盖了函数分类、窗口帧定义、使用示例以及注意事项,帮助你更好地理解和应用窗口函数。