17370845950

SQL 如何实现“去重后取最新 N 条”且保持插入顺序
用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;

“保持插入顺序”本质是依赖 idauto_increment 字段

很多场景说的“最新”,其实不是指 created_at 时间,而是指最后插入的那条(比如日志没打时间戳,或时间不准)。这时应优先用主键 id 判断新旧,它天然有序、无重复、不为空。

但要注意:如果用的是 UUID 或分布式 ID(如雪花 ID),就不能简单比大小——雪花 ID 虽含时间成分,但高位是机器号,直接 ORDER BY id DESC 可能错乱;此时仍应回归真实时间字段。

  • 本地单库 + 自增主键 → 直接 ORDER BY id DESC 最稳
  • 用了 created_at DEFAULT CURRENT_TIMESTAMP → 通常够用,但需确认应用没手动覆盖该字段
  • 批量导入或时钟不同步时,id 比时间更可靠

真正难处理的是“既没可靠时间字段,又没单调递增 ID”的表——这时候得改表结构,否则任何“取最新”的逻辑都是赌概率。