背景:一个写了 60 行的排行榜 SQL
七月做运营后台,产品要「每个城市的销售 Top3」和「月度累计业绩」。第一版我用子查询嵌套,SQL 拉到 60 多行,GROUP BY 套 GROUP BY,执行计划里全是 DEPENDENT SUBQUERY,跑一次 3 秒多。同事提醒我:你们库早升 MySQL 8.0 了,窗口函数不用白不用。
排名场景:ROW_NUMBER vs RANK
「每城市销售 Top3」用 ROW_NUMBER() 开窗,按城市分区、业绩降序:
SELECT city, sales_name, amount, rnk
FROM (
SELECT city, sales_name, amount,
ROW_NUMBER() OVER (
PARTITION BY city
ORDER BY amount DESC
) AS rnk
FROM sales_daily
WHERE dt = '2023-07-01'
) t
WHERE rnk <= 3;
注意并列处理:ROW_NUMBER() 不处理并列,同分也排出 1、2、3;要「并列同名次」用 RANK(),要「并列占同号但后续不跳号」用 DENSE_RANK()。之前子查询版本为了处理并列写了额外的计数逻辑,窗口函数一行搞定。
累计场景:SUM OVER 替代自连接
「月度累计业绩」以前要自己和自己 JOIN 出所有更早的日期再求和,复杂度 O(n²)。窗口函数用 ROWS BETWEEN:
SELECT sales_name, dt, amount,
SUM(amount) OVER (
PARTITION BY sales_name
ORDER BY dt
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cum_amount
FROM sales_daily
WHERE dt BETWEEN '2023-07-01' AND '2023-07-31';
执行计划从两次全表扫描 + 排序降到一次顺序扫描,耗时从 2.4 秒掉到 180 毫秒。
移动平均场景:滑动窗口
看趋势常用 7 日移动平均,窗口函数天然支持滑动范围:
SELECT dt, amount,
AVG(amount) OVER (
ORDER BY dt
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma7
FROM sales_daily;
替代复杂子查询的收益
| 写法 | 代码行数 | 查询耗时 |
|---|---|---|
| 子查询嵌套 | 60+ | 3.1s |
| 窗口函数 | 18 | 0.2s |
逻辑也清晰得多:开窗逻辑在 OVER() 里一目了然,不用脑补几层子查询的关联条件。
小结
MySQL 8.0 的窗口函数是把「难写的分析 SQL」变简单的利器,排名、累计、移动平均三类场景几乎都能一行解决,性能和可读性双提升。但要留意:窗口函数仍要 ORDER BY 列有索引,不然分区内的排序会拖慢。别再手写嵌套子查询了,8.0 早已不是当年的 MySQL。