用ROW_NUMBER()按时间倒序分组去重的核心是:先按user_id分组、created_at降序排序并编号,再筛选rn=1的最新记录;若取最新N条需两层嵌套,外层必须显式ORDER BY created_at DESC;MySQL 5.7以下需用NOT EXISTS子查询并建联合索引;无可靠时间或ID时应改造表结构。

用 ROW_NUMBER() 按时间倒序分组去重
核心思路是:先按业务主键(如 user_id)分组,再按时间字段(如 created_at)降序排序,给每组最新记录标上 1,最后筛出所有 rn = 1 的行。这能保证“每个用户只留一条,且是最新那条”。
常见错误是直接 GROUP BY + MAX(created_at),但这样拿不到整行数据(比如用户名、状态等其他字段会丢失或随机)。
- 必须用窗口函数,不能用聚合函数直接取整行
-
ORDER BY created_at DESC是关键,升序就会取到最老的记录 - 如果时间字段有重复,建议追加
id DESC做二级排序,避免结果不稳定
SELECT user_id, name, status, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM logs
) t
WHERE rn = 1;
取“去重后最新 N 条”要两层嵌套
很多人以为加个 LIMIT N 就完事,但那是先 LIMIT 再去重,逻辑完全反了。正确做法是:第一层去重得最新记录 → 第二层对这些去重结果再按时间倒序取前 N 条。
注意:第二层的 ORDER BY 必须显式写,否则数据库不保证返回顺序(即使第一层排过序,外层也不继承)。
- 内层负责“每个 key 留最新一条”
- 外层负责“从所有最新记录里取时间最近的 N 条”
- 外层
ORDER BY created_at DESC不可省,尤其在 PostgreSQL / MySQL 8.0+ 中
SELECT user_id, name, status, created_at
FROM (
SELECT user_id, name, status, created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM logs
) t
WHERE rn = 1
ORDER BY created_at DESC
LIMIT 5;
MySQL 5.7 或更老版本不能用窗口函数怎么办
只能靠相关子查询或自连接模拟,性能差、写法绕,而且容易漏数据或误删。典型写法是:对每条记录,查同 user_id 下有没有更新的记录;如果没有,说明它就是最新的。
这种写法在数据量稍大(比如 >10 万行)时就明显变慢,且 created_at 字段必须有索引,否则全表扫描。
- 子查询中
WHERE l2.created_at > l1.created_at要注意 NULL 安全,建议created_at设为NOT NULL - 务必给
(user_id, created_at)建联合索引,否则性能雪崩 - 如果存在多条同时间记录,此方法可能保留多条,不如窗口函数精确
SELECT l1.user_id, l1.name, l1.status, l1.created_at
FROM logs l1
WHERE NOT EXISTS (
SELECT 1 FROM logs l2
WHERE l2.user_id = l1.user_id
AND l2.created_at > l1.created_at
)
ORDER BY l1.created_at DESC
LIMIT 5;
“保持插入顺序”本质是依赖 id 或 auto_increment 字段
很多场景说的“最新”,其实不是指 created_at 时间,而是指最后插入的那条(比如日志没打时间戳,或时间不准)。这时应优先用主键 id 判断新旧,它天然有序、无重复、不为空。
但要注意:如果用的是 UUID 或分布式 ID(如雪花 ID),就不能简单比大小——雪花 ID 虽含时间成分,但高位是机器号,直接 ORDER BY id DESC 可能错乱;此时仍应回归真实时间字段。
- 本地单库 + 自增主键 → 直接
ORDER BY id DESC最稳 - 用了
created_at DEFAULT CURRENT_TIMESTAMP→ 通常够用,但需确认应用没手动覆盖该字段 - 批量导入或时钟不同步时,
id比时间更可靠
真正难处理的是“既没可靠时间字段,又没单调递增 ID”的表——这时候得改表结构,否则任何“取最新”的逻辑都是赌概率。










