MySQL 窗口函数按分组取每组最新记录的实现方法
来源:17golang原创
时间:2026-09-15 20:06:46 185浏览 收藏
我在整理订单状态报表时,最容易写错的不是“按时间倒序”,而是“每组只留一条”这一句。直接给每个客户排序并不能自动减少结果行,GROUP BY 也会把明细列的选择变得含糊。更稳的做法是先用 ROW_NUMBER() 在每个分组内编号,再由外层查询筛出 rn = 1。
PARTITION BY决定“每组”的边界,ORDER BY决定最新记录的顺序。- 时间相同不能只依赖数据库的自然顺序,应追加唯一键作为稳定的第二排序条件。
- 窗口函数先产生编号,外层查询再过滤;过滤条件和索引要放在正确的数据边界上。
先把“每组最新”拆成三个字段
下面假设表 order_status_events 保存订单状态事件,customer_id 是分组键,updated_at 表示事件时间,event_id 是唯一递增标识。实际业务中,分组键也可能是门店、设备或项目,关键是先说清楚一组到底代表什么。
| 问题 | 对应 SQL 位置 | 判断标准 |
|---|---|---|
| 按谁分组 | PARTITION BY customer_id | 同一客户的事件进入同一窗口 |
| 什么叫最新 | ORDER BY updated_at DESC | 时间越晚排名越靠前 |
| 时间相同怎么办 | event_id DESC | 用唯一键固定最终选择 |

用 ROW_NUMBER() 给每个分组编号
窗口函数不会像 GROUP BY 那样把一组折叠成一行,而是为每条明细计算一个结果。MySQL 官方语法中,PARTITION BY 划分窗口,窗口内的 ORDER BY 负责排序;因此可以把“取最新”写成下面的 CTE。
WITH ranked_events AS (
SELECT
customer_id,
order_id,
status,
updated_at,
event_id,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC, event_id DESC
) AS rn -- 先按时间,再用唯一键稳定打破并列
FROM order_status_events
)
SELECT
customer_id,
order_id,
status,
updated_at
FROM ranked_events
WHERE rn = 1 -- 外层过滤每组的第一名
ORDER BY customer_id;
这里的 rn = 1 不是在同一层窗口表达式旁边直接过滤,而是放在外层查询。这样写也方便后续把 rn 改成“每组最新三条”。
时间相同仍要给出确定结果
只写 ORDER BY updated_at DESC 时,如果同一客户有两条事件的时间完全相同,它们在窗口排序中属于并列记录,哪一条得到第一名就不再是明确的业务规则。生产查询应追加能唯一确定顺序的字段,例如 event_id、雪花 ID 或可靠的写入序号。
如果业务口径是“每个订单最新状态”,窗口应改为 PARTITION BY order_id;如果是“每个客户每种状态各取一条”,则要写成 PARTITION BY customer_id, status。这不是换一个字段的小修饰,而是在改变结果的粒度,最好先用三四组样例数据核对。

过滤范围、索引和复核清单
如果只查最近 30 天的事件,应把时间条件放进 CTE,让窗口函数只对目标范围编号:
WITH ranked_events AS (
SELECT
customer_id, order_id, status, updated_at, event_id,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC, event_id DESC
) AS rn -- 窗口只处理已经过滤的数据
FROM order_status_events
WHERE updated_at >= '2026-08-16 00:00:00' -- 示例时间边界
)
SELECT customer_id, order_id, status, updated_at
FROM ranked_events
WHERE rn = 1;
索引可以从过滤列、分组列和排序列的组合开始评估,例如 (updated_at, customer_id, event_id) 或按实际过滤方式调整;窗口排序是否仍需要额外工作,要以 EXPLAIN 的访问路径和数据规模为准,不能只凭索引名字判断。
- 核对分组列是否真的是业务口径,而不是误把
order_id当成客户分组。 - 给时间相同的样例补上唯一键,确认重复执行得到同一条记录。
- 先限定数据范围再编号,避免把历史全量数据带进当前报表。
常见问题
为什么不能直接 GROUP BY customer_id 取 updated_at?
MAX(updated_at) 只能得到最大时间,不能自动把同一行的 order_id 和 status 一起带出来。先编号再取第一名,才能保留完整明细。
RANK() 能不能替代 ROW_NUMBER()?
如果并列时间需要全部保留,可以考虑 RANK();如果要求每组严格一行,就使用 ROW_NUMBER() 并补充唯一排序键。
窗口函数是不是一定比相关子查询快?
不能一概而论。窗口写法更直接,但最终仍受过滤范围、数据量、索引和排序代价影响,应结合实际数据用 EXPLAIN 比较。
这类查询的核心不是记住一段固定 SQL,而是先确定分组粒度,再用完整排序条件把“最新”定义成一个确定结果。只要 PARTITION BY、排序键和外层 rn = 1 三处口径一致,后续扩展到每组 Top N 也只是调整筛选条件。
-
374 收藏
-
499 收藏
-
384 收藏
-
184 收藏
-
265 收藏
-
148 收藏
-
409 收藏
-
118 收藏
-
101 收藏
-
221 收藏
-
数据库 · MySQL | 9小时前 | MySQL · 性能分析 · Performance Schema · SQL耗时 · mysql Performance Schema events_statements 平均耗时125 收藏
-
167 收藏
-
253 收藏
-
312 收藏
-
191 收藏
-
446 收藏
-
107 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习