人人都会AI编程

9.6 窗口函数:排名、聚合、偏移类窗口函数用法与场景

更新时间:2026-07-11

窗口函数(Window Function)是 MySQL 8.0 引入的重量级新特性,它解决了一个长久以来的痛点:在不改变结果集行数的情况下,对每一行进行分组相关的计算。以往你需要借助子查询、临时表甚至应用层代码才能实现的复杂逻辑,现在往往一条 SQL 就能干净利落地完成。

9.6.1 窗口函数是什么、为什么要用

普通的聚合函数(如 SUMAVG)配合 GROUP BY,会把多行合并为一行输出,你想在保留原始明细的同时看到分组统计,就必须用子查询或者联表,这既难写又难读。窗口函数的设计思路完全不同:它也是分组计算,但计算结果作为新的一列附加到每一行上,原始行的数量不变。

举个例子:你想查询每个员工的薪资,同时显示其所在部门的平均薪资。传统的做法是子查询:

SELECT 
  emp_name,
  dept_id,
  salary,
  (SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id) AS dept_avg
FROM employees e;

而窗口函数的写法是:

SELECT 
  emp_name,
  dept_id,
  salary,
  AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg
FROM employees;

PARTITION BY dept_id 定义了分组的依据(称为“窗口”),计算结果直接挂载到每个员工的行上,SQL 更简洁,执行效率也往往更好(减少子查询的嵌套循环)。

9.6.2 基本语法与执行顺序

窗口函数的通用结构是:

函数名([参数]) OVER (
  [PARTITION BY 列名1, 列名2, ...]
  [ORDER BY 列名3 [ASC|DESC], ...]
  [frame_clause]
)
  • PARTITION BY:对行进行分组,类似于 GROUP BY,但不合并行。省略时,整个结果集视为一个窗口。
  • ORDER BY:定义窗口内行的排序顺序,很大程度上影响排名类、偏移类函数的行为。
  • frame_clause(框架子句):用于聚合类窗口函数,指定计算时包含哪些行。默认窗口框架是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(从分区第一行到当前行)。

在 SQL 的执行顺序中,窗口函数是在 WHEREGROUP BYHAVING 之后,ORDER BYLIMIT 之前执行的。这意味着你可以在 WHERE 里过滤掉不需要的行,然后用窗口函数在剩余的行上计算;窗口函数的结果也可以在外层的 ORDER BYLIMIT 中使用。

9.6.3 排名类窗口函数

排名函数最常见的三类:

  • ROW_NUMBER():为窗口内的每一行分配一个唯一的连续编号,即使排序字段相同也递增。典型场景:实现各类“第几名”或分页去重。
  • RANK():值相同时同序号,后面的序号会出现跳跃。例如两个并列第一,下一个就是 3(没有 2)。
  • DENSE_RANK():值相同时同序号,但序号连续,不跳跃。两个并列第一,下一个是 2。

实际场景1:每个部门内按薪资排名

SELECT
  emp_name,
  dept_id,
  salary,
  RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_in_dept
FROM employees;

输出每一行员工,并在最后一列标记他在所属部门的薪资排名(使用 RANK,薪资相同的并列,下一名会跳号)。

实际场景2:取每个分类下最贵的 N 件商品

通过子查询结合 ROW_NUMBER(),可以轻松实现“分组 TopN”:

SELECT * FROM (
  SELECT
    product_id,
    category_id,
    price,
    ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS rn
  FROM products
) t
WHERE rn <= 3;

这在分页或排行榜需求中极为常用。重要的是,ROW_NUMBER() 在这里保证了每个分类内行号唯一,即使价格相同也不会并列。

实际场景3:连续签到或连续登陆的计数

利用 ROW_NUMBER() 和日期差值的技巧,可以找出连续日期的序列(常被称为“岛屿问题”),这是面试和实际分析中的经典应用。

9.6.4 聚合类窗口函数

所有普通的聚合函数(SUMAVGCOUNTMAXMIN 等)都可以作为窗口函数使用,结合 OVER 子句。它们的核心价值在于保持行的粒度同时提供聚合信息

典型场景:计算累计值和移动平均

  • 累计求和:常用于财务报表,比如每月营收的累计总和。
SELECT
  month,
  revenue,
  SUM(revenue) OVER (ORDER BY month) AS cumulative_revenue
FROM monthly_sales;

这里 ORDER BY month 意味着窗口按月份排序,默认框架是从开头到当前行,因此每行得到的是截至当月的总和。

  • 移动平均:比如计算每件商品最近 7 天的平均销量,可以用 AVG 配合指定行范围实现。例如:
SELECT
  sale_date,
  product_id,
  quantity,
  AVG(quantity) OVER (
    PARTITION BY product_id
    ORDER BY sale_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7days
FROM sales;

注意这里使用了 ROWS 框架,精确控制为当前行及前 6 行的平均,而不是从分区开头截止当前行。

需要注意:聚合窗口函数最频繁的问题是不加 ORDER BY 导致整个分区所有行得到相同的值,或者框架定义不对导致累计不符合预期。实际使用时要非常清楚排序和框架的默认行为。

9.6.5 偏移类窗口函数

偏移类窗口函数用于访问同一分区内“前一行”或“后一行”的数据,而无需自关联。最常用的两个:

  • LAG(列, N, 默认值):返回当前行往前第 N 行的值。
  • LEAD(列, N, 默认值):返回当前行往后第 N 行的值。
  • FIRST_VALUE(列)LAST_VALUE(列):获取窗口内第一行和最后一行的值。

场景1:计算相邻行的差值(环比增长)

SELECT
  date,
  visit_count,
  LAG(visit_count, 1) OVER (ORDER BY date) AS prev_visit,
  visit_count - LAG(visit_count, 1) OVER (ORDER BY date) AS growth
FROM site_traffic;

直接用 LAG 取上一期的数据,直接相减得到环比变化,无需自关联和子查询。

场景2:获取用户的最后一条购买记录

LAST_VALUE 需要配合窗口框架才能正确工作,因为默认框架是到当前行,看到的是当前行而不是分区最后一行。通常我们写成:

SELECT
  user_id,
  order_date,
  LAST_VALUE(order_date) OVER (
    PARTITION BY user_id
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS last_order_date
FROM orders;

或者更简便地直接用 MAX(order_date) OVER (PARTITION BY user_id) 替代,但偏移函数在顺序敏感的场景更加灵活。

场景3:时间间隔分析

LAG 可以轻松计算两次事件之间的时间差,例如用户连续两次登录的时间间隔,这对活跃度分析极其有用。

9.6.6 使用窗口函数的注意事项

  • MySQL 8.0 才支持,如果还在 5.7,请尽快升级或寻找奇技淫巧(如使用变量模拟)。
  • 不要滥用:窗口函数虽然强大,但复杂的窗口计算仍会消耗较多内存(尤其 PARTITION BY 粒度很大时),执行计划使用 Using temporary 是常态,未必比精心优化后的子查询更快。
  • 命名窗口:MySQL 8.0 允许使用 WINDOW w AS (PARTITION BY ... ORDER BY ...) 前置定义窗口,避免重复代码。例如:
  SELECT
    emp_name,
    dept_id,
    salary,
    RANK() OVER w AS rk,
    AVG(salary) OVER w AS avg_sal
  FROM employees
  WINDOW w AS (PARTITION BY dept_id ORDER BY salary DESC);
  
  • 与普通聚合区分:窗口函数不能直接用在 WHEREHAVINGGROUP BY 中,因为它们的计算在 WHERE 之后。要筛选窗口函数的结果,必须使用子查询或者 CTE。

总之,窗口函数是让 SQL 变得更聪明、更简洁的利器,把以往需要多步操作或应用代码拼接的逻辑移入数据库内部完成,提升了开发效率和代码可维护性。当你遇到“分组但不合并”的需求时,第一时间就应想到窗口函数。