用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;
很多人以为加个 LIMIT N 就完事,但那是先 LIMIT 再去重,逻辑完全反了。正确做法是:第一层去重得最

注意:第二层的 ORDER BY 必须显式写,否则数据库不保证返回顺序(即使第一层排过序,外层也不继承)。
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;
只能靠相关子查询或自连接模拟,性能差、写法绕,而且容易漏数据或误删。典型写法是:对每条记录,查同 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”的表——这时候得改表结构,否则任何“取最新”的逻辑都是赌概率。