核心要点

  • 标准套路:ROW_NUMBER() OVER(PARTITION BY 去重键 ORDER BY 时间戳 DESC),外层取 rn=1

  • PARTITION BY 决定「按什么去重」,ORDER BY ... DESC 决定「保留哪一条」(最新)

  • 相比 GROUP BY + MAX(ts),窗口函数能一次性带出整行其他字段,无需二次自连接

  • 易错点:排序键有并列(同一时刻多条)时 ROW_NUMBER 任意取一条,需加次级排序键保证确定性

标准回答

去重保留最新的通用做法是窗口函数 ROW_NUMBER():按去重键 PARTITION BY,按时间戳 ORDER BY DESC 编号,组内最新的那条编号恒为 1,外层过滤 rn=1 即可,且能直接带出整行所有列。如果只用 GROUP BY user_id, MAX(update_time),还要再自连接回明细才能取到其他字段,既啰嗦又可能因同一时间多条而放大。当排序键存在并列时,应追加一个唯一的次级排序键(如自增 id)保证结果稳定。

sql
-- 每个用户只保留更新时间最新的一条订单记录
SELECT user_id, order_id, amount, update_time
FROM (
  SELECT
    user_id,
    order_id,
    amount,
    update_time,
    ROW_NUMBER() OVER (
      PARTITION BY user_id           -- 按用户去重
      ORDER BY update_time DESC,     -- 时间最新优先
               order_id DESC         -- 同一时刻时用 id 兜底,保证确定性
    ) AS rn
  FROM orders
) t
WHERE rn = 1;                        -- 每个用户只留最新一条

常见误区

⚠️ 常见踩坑

用 GROUP BY user_id 后直接 SELECT 非聚合字段在严格模式下会报错,且 MAX(update_time) 配其他列会取到不匹配的行;ROW_NUMBER 仅按时间排序而时间有重复时结果不确定,必须加唯一次级排序键。

追问

追问 1如果想保留每组「最新的 K 条」而不是 1 条,怎么改?

把外层条件从 rn = 1 改成 rn <= K 即可,其余不变。这正是「每组 Top-N」的同一套路:ROW_NUMBER 编号 + 外层过滤。若需要并列的最新记录都保留,则把 ROW_NUMBER 换成 RANK/DENSE_RANK,按 update_time 并列的多条会拿到相同名次而一并保留。

追问 2物理删除表里的重复行(只留最新)应该怎么写?

先用窗口函数标出要删的行,再删。MySQL 8+/PostgreSQL 可用 CTE:WITH d AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY update_time DESC) rn FROM orders) DELETE FROM orders WHERE id IN (SELECT id FROM d WHERE rn > 1)。删除前务必备份并先用 SELECT 验证待删行,避免误删。

延伸学习

与本题相关的知识库文章、术语、工具与行业资讯。