MySQL进阶实战:窗口函数 VS 聚合函数,性能与场景全对比
传统的聚合函数(Aggregate Functions)虽然功能强大,但在某些复杂分析场景下存在明显局限。自 MySQL 8.0 引入窗口函数(Window Functions)以来,SQL 语句的表达能力得到了极大扩展。
本篇文章将围绕“窗口函数 VS 聚合函数”展开全方位对比,从语法、执行原理、适用场景、性能分析、优化建议等多个角度进行深入解析,帮助你在实际开发中合理选择、更高效地编写 SQL 语句。
二、基础概念回顾
2.1 聚合函数简介
聚合函数是用于对一组值执行计算并返回单一值的函数,常见的有:
SUM()
:求和COUNT()
:计数AVG()
:平均值MAX()
:最大值MIN()
:最小值
通常与 GROUP BY
联合使用,例如:
SELECT department, AVG(salary)
FROM employees
GROUP BY department;
该语句将员工按部门分组,计算每个部门的平均工资。
2.2 窗口函数简介
窗口函数则是对查询结果中的每一行应用聚合计算,同时保留详细数据。它不会将结果按组折叠,而是保留原始行。典型语法结构为:
<窗口函数> OVER (PARTITION BY <分区列>ORDER BY <排序列>ROWS BETWEEN <窗口范围>
)
常见窗口函数包括:
ROW_NUMBER()
:行号RANK()
/DENSE_RANK()
:排名SUM()
,AVG()
,MAX()
等聚合函数也可作为窗口函数使用
示例:
SELECT employee_id, department, salary,RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;
此语句为每个部门内员工按工资从高到低排名。
三、语法与表达能力对比
功能对比项 | 聚合函数 (Aggregate) | 窗口函数 (Window) |
---|---|---|
是否折叠数据 | 是 | 否 |
是否支持分区 | 是(通过 GROUP BY ) | 是(PARTITION BY ) |
是否支持排序 | 否(需配合子查询/变量) | 是(ORDER BY ) |
是否保留原始行数据 | 否(每组输出 1 行) | 是(对每行应用计算) |
是否支持滑动窗口 | 否 | 是(通过 ROWS BETWEEN ) |
典型使用场景 | 汇总统计、图表聚合 | 排名、环比、累计值、序列分析 |
MySQL版本支持 | 所有版本 | 8.0 及以上 |
聚合函数表达能力有限,窗口函数支持更复杂的数据分析需求。
四、典型场景对比
4.1 需求一:获取每个部门最高工资的员工信息
聚合函数写法(需子查询):
SELECT e.*
FROM employees e
JOIN (SELECT department_id, MAX(salary) AS max_salaryFROM employeesGROUP BY department_id
) m ON e.department_id = m.department_id AND e.salary = m.max_salary;
窗口函数写法(更直观):
SELECT *
FROM (SELECT *, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnkFROM employees
) t
WHERE rnk = 1;
4.2 需求二:统计员工工资的部门平均值,同时保留每条记录
聚合函数写法(需子查询或 JOIN
):
SELECT e.*, d.avg_salary
FROM employees e
JOIN (SELECT department_id, AVG(salary) AS avg_salaryFROM employeesGROUP BY department_id
) d ON e.department_id = d.department_id;
窗口函数一行搞定:
SELECT *, AVG(salary) OVER (PARTITION BY department_id) AS avg_salary
FROM employees;
4.3 需求三:计算每个员工相对于部门的工资环比(前一行)
SELECT employee_id, department_id, salary,salary - LAG(salary) OVER (PARTITION BY department_id ORDER BY salary) AS salary_diff
FROM employees;
聚合函数无法实现这种按行滑动比较的需求,窗口函数是唯一选择。
五、性能对比实测
5.1 测试环境
- MySQL 版本:8.0.36
- 表结构:employees(约 100 万条记录)
- 索引:
department_id
,salary
- 硬件:8C/32G,SSD
5.2 测试案例
用例一:每个部门工资最高员工
- 聚合 + JOIN:执行时间 ≈ 210ms
- 窗口函数 + 子查询 + RANK:执行时间 ≈ 160ms
用例二:每位员工部门平均工资
- 聚合 + JOIN:≈ 195ms
- 窗口函数:≈ 140ms
用例三:员工工资同比增长
- 聚合实现困难或需复杂子查询
- 窗口函数:≈ 180ms
5.3 总结
在大量数据场景下,窗口函数通常具有更优的性能和更简洁的语法结构,尤其在不需要将数据折叠时效果更佳。
六、执行计划剖析与优化建议
6.1 执行计划观察方式
EXPLAIN FORMAT=JSON <SQL语句>;
重点关注字段:
using_temporary
using_filesort
window_functions
attached_conditions
6.2 窗口函数优化建议
- 优化
PARTITION BY
和ORDER BY
字段顺序,添加索引 - 避免在窗口函数内部使用复杂表达式
- 尽量减少窗口范围,必要时用
ROWS BETWEEN
6.3 聚合函数优化建议
- 为
GROUP BY
字段建立联合索引 - 用
ROLLUP
优化多级汇总 - 考虑
HASH GROUP BY
替代SORT GROUP BY
七、实战案例分享
案例一:用户留存率计算
需求:每天新增用户中,在接下来 7 天内是否登录过
SELECT user_id, register_date,MAX(CASE WHEN login_date BETWEEN register_date AND register_date + INTERVAL 7 DAY THEN 1 ELSE 0 END)OVER (PARTITION BY user_id) AS retained
FROM user_activity;
案例二:电商订单连续下单检测
SELECT user_id, order_date,DATEDIFF(order_date, LAG(order_date) OVER (PARTITION BY user_id ORDER BY order_date)) AS interval_days
FROM orders;
如 interval_days = 1
,表示连续下单。
案例三:销售额累计趋势图
SELECT sale_date, region,SUM(sales_amount) OVER (PARTITION BY region ORDER BY sale_date) AS cumulative_sales
FROM sales;
使用窗口函数实现“累计和”非常直观。
八、何时使用窗口函数 VS 聚合函数?
聚合函数适用:
- 需要折叠数据输出(如总和、计数)
- 数据量小,逻辑简单
- 查询可直接满足需求,无需保留明细
窗口函数适用:
- 保留详细行数据
- 涉及排序/排名、累计值、环比分析等
- 避免嵌套子查询、逻辑清晰度优先
九、开发实战建议
场景/目的 | 推荐函数 | 说明 |
---|---|---|
汇总报表 | 聚合函数 | 可读性强,执行高效 |
行内比较 | 窗口函数 | LAG/LEAD 实现环比同比、连续天检测 |
排名类分析 | 窗口函数 | RANK/DENSE_RANK |
分类累计值计算 | 窗口函数 | SUM() OVER + ORDER BY 实现趋势分析 |
同时使用多种聚合维度 | 窗口函数更简洁 | 避免 JOIN 或子查询 |