MySQL 窗口函数按时间去重时如何保留最新行
来源:17golang原创
时间:2026-09-15 07:09:41 312浏览 收藏
日志表、订单状态表、同步快照表经常会出现同一个业务键的多条记录。要按时间去重并保留最新行,最稳妥的写法是:用 ROW_NUMBER() 在每个业务分组内按时间倒序编号,再在外层筛选 rn = 1。不要直接用 GROUP BY 配合 MAX(updated_at),那样很容易丢掉与最新时间对应的其他列。
官方地址:https://dev.mysql.com/doc/refman/8.4/en/window-functions-usage.html
PARTITION BY决定“同一组”是什么,ORDER BY决定哪一行排在最前。- 时间相同时追加唯一键倒序,避免同一批数据每次返回不同记录。
- 窗口函数结果要放进 CTE 或派生表,再由外层过滤
rn = 1。
先把“同一条记录”和“最新”定义清楚
我处理这类数据时,第一步不是写 SQL,而是先确认去重键。例如一个账号下同一个业务编码只能留一行,就用 account_id, biz_key 作为分组键;如果同一编码在不同租户之间互不相同,还必须把 tenant_id 加进去。分组键少了,会把本不该合并的记录删掉。
“最新”通常对应 updated_at,但创建时间、采集时间、状态生效时间的含义并不一样。还要确认它是否允许 NULL,以及精度是否只有秒级。如果多个记录共享同一个时间,必须准备一个唯一且稳定的次排序列,常见的是自增 id。
用 ROW_NUMBER() 给每个分组排出第一行
下面的例子按账号和业务键去重,保留更新时间最大的完整记录。窗口函数本身仍会为每一行产生结果,所以先在 CTE 中增加序号,再从外层取第一行。
WITH ranked AS (
SELECT
id, account_id, biz_key, updated_at, payload,
ROW_NUMBER() OVER (
PARTITION BY account_id, biz_key
ORDER BY updated_at DESC, id DESC
) AS rn -- 时间相同则用唯一 id 稳定决胜
FROM activity_log
)
SELECT id, account_id, biz_key, updated_at, payload
FROM ranked
WHERE rn = 1; -- 外层过滤,保留每组排名第一的完整行
PARTITION BY 只负责划分窗口,不会像 GROUP BY 那样把行合并;ORDER BY updated_at DESC 让最新时间排在前面,id DESC 则处理同一时间的并列。MySQL 手册也明确说明,ROW_NUMBER() 在没有排序时编号是不确定的,因此不要省略窗口内的排序。

并列时间、NULL 和性能取舍要单独处理
如果只写 ORDER BY updated_at DESC,同一组里时间相同的行都是并列项,数据库没有义务替你选择固定的一行。追加 id DESC 后,规则就变成“时间更新者优先,时间相同取 id 较大者”。如果业务上不允许任意决胜,则应增加版本号或明确的生效序列,而不是把选择交给执行计划。
降序排序时 NULL 的位置也要确认。MySQL 中升序 NULL 排在前面,降序 NULL 排在后面;如果 NULL 代表“未知”而不是“最旧”,可以显式写出业务规则,例如用 CASE WHEN updated_at IS NULL THEN 1 ELSE 0 END 作为首个排序表达式,并在注释中说明原因。
数据量较大时,窗口排序仍可能需要处理大量候选行。先用明确的时间范围、租户条件或状态条件缩小输入,再观察 EXPLAIN,通常比盲目添加索引更可靠。索引列顺序要围绕真实过滤条件和分组键设计,不能因为查询里出现了 ORDER BY 就假设一定可以完全避免排序。

上线前用四项检查确认结果
| 检查项 | 要确认的内容 |
|---|---|
| 分组键 | 是否包含租户、账号和业务对象的全部唯一维度 |
| 时间字段 | 是否真代表最新状态,时区、精度和 NULL 语义是否明确 |
| 并列规则 | 是否有唯一键、版本号或业务序列作为次排序 |
| 结果复核 | 随机抽取一个分组,检查留下的行确实对应最大时间及决胜列 |
如果只是展示最新状态,这个查询通常足够;如果要把去重结果写回新表或删除历史行,则应先备份、统计重复组数量,并把写操作放到可回滚的变更流程里。
常见问题
为什么不能直接用 MAX(updated_at)?
MAX() 只能得到最大时间,不能自动带回该时间对应的完整行;窗口编号可以保留整行字段。
ROW_NUMBER() 和 RANK() 怎么选?
要每组严格保留一行,用 ROW_NUMBER();如果并列记录都应该保留,再考虑 RANK() 或 DENSE_RANK()。
为什么 rn 不能直接写在 WHERE 里?
窗口函数在当前查询层生成结果,先放入 CTE 或派生表,再由外层按 rn 过滤,边界最清楚。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
253 收藏
-
191 收藏
-
446 收藏
-
107 收藏
-
181 收藏
-
432 收藏
-
327 收藏
-
284 收藏
-
199 收藏
-
数据库 · MySQL | 12小时前 | MySQL · 数据类型 · JSON · SQL排错 · MEMBER OF · MySQL MEMBER OF MySQL JSON 数组成员判断 MEMBER OF 类型不匹配 JSON 数字字符串区别 MySQL JSON 查询167 收藏
-
数据库 · MySQL | 14小时前 | MySQL · 数据库查询 · JSON 函数 · SQL 边界 · JSON 数组 · JSON_CONTAINS MySQL JSON_OVERLAPS JSON 数组相交 MySQL JSON 类型比较 MySQL NULL 边界130 收藏
-
418 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习