MySQL 中的窗口函数详解:从入门到实战
🌟 前言
在日常开发中,我们经常需要对数据进行排序、分组、排名、累计求和等操作。传统的 GROUP BY
和子查询虽然能解决部分问题,但写法复杂、性能差,且难以处理“每组内计算”的场景。
从 MySQL 8.0 开始,官方正式支持了 窗口函数(Window Functions),这让复杂的数据分析变得简单高效。本文将带你全面掌握 MySQL 窗口函数的语法、常用函数及实际应用场景。
📚 一、什么是窗口函数?
1.1 定义
窗口函数(Window Function) 是一种在“结果集的一个窗口”上进行计算的函数。它不会像 GROUP BY
那样将多行合并为一行,而是为每一行都返回一个计算结果,保留原始行数。
✅ 简单理解:分组但不聚合,每行都能看到“当前组”的统计信息。
1.2 与 GROUP BY 的区别
特性 | GROUP BY | 窗口函数 |
---|---|---|
行数变化 | 合并为一行 | 保留所有行 |
聚合后能否访问原始列 | ❌ 不能 | ✅ 可以 |
是否支持排序内计算 | ❌ 困难 | ✅ 支持 |
典型用途 | 统计总数、平均值 | 排名、累计、移动平均 |
🔤 二、窗口函数基本语法
function_name(...) OVER ([PARTITION BY 分组字段][ORDER BY 排序字段][FRAME 子句]
)
function_name
:如ROW_NUMBER()
、SUM()
、AVG()
等PARTITION BY
:定义“窗口”范围,类似GROUP BY
ORDER BY
:定义窗口内的排序方式FRAME
:定义窗口的起始和结束行(如ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
)
🧰 三、常用窗口函数分类
3.1 排名函数
函数 | 说明 | 示例 |
---|---|---|
ROW_NUMBER() | 行号,不重复 | 1,2,3,4... |
RANK() | 并列排名,留空 | 1,2,2,4... |
DENSE_RANK() | 并列排名,不留空 | 1,2,2,3... |
示例:学生成绩排名
SELECT name, subject, score,ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC) AS row_num,RANK() OVER (PARTITION BY subject ORDER BY score DESC) AS rank_num,DENSE_RANK() OVER (PARTITION BY subject ORDER BY score DESC) AS dense_rank_num
FROM student_score;
输出:
name | subject | score | row_num | rank_num | dense_rank_num |
---|---|---|---|---|---|
张三 | 数学 | 95 | 1 | 1 | 1 |
李四 | 数学 | 90 | 2 | 2 | 2 |
王五 | 数学 | 90 | 3 | 2 | 2 |
赵六 | 数学 | 85 | 4 | 4 | 3 |
3.2 聚合函数(作为窗口函数使用)
常见的聚合函数都可以作为窗口函数使用:
SUM()
AVG()
COUNT()
MIN()
/MAX()
示例:每月累计销售额
SELECT order_month,monthly_sales,SUM(monthly_sales) OVER (ORDER BY order_month) AS cum_sales
FROM sales_summary;
输出:
order_month | monthly_sales | cum_sales |
---|---|---|
2025-01 | 10000 | 10000 |
2025-02 | 15000 | 25000 |
2025-03 | 12000 | 37000 |
3.3 值函数(前后行取值)
函数 | 说明 |
---|---|
LAG(col, n) | 向前取第 n 行的值 |
LEAD(col, n) | 向后取第 n 行的值 |
FIRST_VALUE(col) | 窗口内第一行的值 |
LAST_VALUE(col) | 窗口内最后一行的值 |
示例:计算每日销售额环比增长
SELECT date,sales,LAG(sales, 1) OVER (ORDER BY date) AS last_day_sales,ROUND((sales - LAG(sales, 1) OVER (ORDER BY date)) / LAG(sales, 1) OVER (ORDER BY date) * 100, 2) AS growth_rate
FROM daily_sales;
🛠 四、实战案例:客户最近一次会话查询
假设我们有一个客服系统,需求是:查询每个客户(from_account)最新的会话记录。
表结构
CREATE TABLE itsm_helpdesk_session (id BIGINT,from_account VARCHAR(50),create_time DATETIME,service_status INT
);
使用窗口函数实现
SELECT *
FROM (SELECT *,ROW_NUMBER() OVER (PARTITION BY from_account ORDER BY create_time DESC) AS rnFROM itsm_helpdesk_session
) t
WHERE rn = 1;
✅ 优势:简洁、高效、易维护,避免了复杂的子查询或自连接。
⚠️ 五、注意事项与性能优化
5.1 必须使用 OVER()
,否则报错
-- ❌ 错误
SELECT ROW_NUMBER() FROM table;-- ✅ 正确
SELECT ROW_NUMBER() OVER () FROM table;
5.2 合理使用索引
为 PARTITION BY
和 ORDER BY
字段建立复合索引,可大幅提升性能。
-- 示例
CREATE INDEX idx_session_account_time
ON itsm_helpdesk_session(from_account, create_time DESC);
5.3 避免全表排序
如果数据量大,尽量在 WHERE
中先过滤,再使用窗口函数。
-- 推荐写法
SELECT *
FROM (SELECT *, ROW_NUMBER() OVER (...)FROM table WHERE status = 1 -- 先过滤
) t
WHERE rn = 1;
🔄 六、常见问题解答
Q1:MySQL 5.7 支持窗口函数吗?
❌ 不支持。窗口函数从 MySQL 8.0 开始引入。5.7 用户需升级或使用变量模拟(不推荐)。
Q2:窗口函数会影响性能吗?
✅ 合理使用并配合索引,性能很好。
❌ 但如果在大表上无索引使用 ORDER BY
,会导致全表排序,性能极差。
Q3:可以嵌套窗口函数吗?
❌ 不可以。窗口函数不能作为其他窗口函数的参数。
📈 七、总结
特性 | 说明 |
---|---|
✅ 优势 | 简化复杂查询、提升可读性、支持高级分析 |
❌ 限制 | 仅 MySQL 8.0+ 支持 |
🔧 推荐场景 | 排名、累计、移动平均、分组取最新 |
📚 学习建议 | 多练习 ROW_NUMBER() 、RANK() 、LAG/LEAD |
💡 结语
窗口函数是 SQL 进阶的必备技能,尤其在数据分析、报表开发中应用广泛。掌握它,不仅能写出更优雅的 SQL,还能显著提升查询效率。
赶快在你的 MySQL 8.0+ 环境中试试吧!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!
也欢迎在评论区留下你的疑问或实战案例,我们一起交流进步!
📌 推荐阅读:
- 《MySQL 8.0 新特性详解》
- 《SQL 性能优化实战》
- 《Elasticsearch 聚合查询指南》