SQL 使用窗口函数实现去重保留最新记录

9次阅读

ROW_NUMBER() 是去重保最新最常用的选择,因其可在分组内按时间倒序编号后筛选序号为1的完整记录,逻辑清晰且主流数据库均支持;需配合OVER()中ORDER BY指定排序,重复时间时应加二级排序防不稳定。

SQL 使用窗口函数实现去重保留最新记录

为什么 ROW_NUMBER() 是去重保最新最常用的选择

直接用 GROUP BY 无法保留“最新”那条完整记录,而 DISTINCT 又不认时间字段的大小关系。窗口函数中 ROW_NUMBER() 能按分组内时间倒序编号,再筛出序号为 1 的行,逻辑清晰、兼容性好(mysql 8.0+、postgresql、SQL Server、oracle 都支持)。

  • 必须配合 ORDER BY 子句在 OVER() 中指定排序依据,比如 created_at DESCid DESC
  • 如果时间字段有重复,建议加二级排序(如 ORDER BY updated_at DESC, id DESC),避免结果不稳定
  • RANK()DENSE_RANK() 会把相同时间的多条都标为 1,达不到“只留一条”的目的

实际写法:子查询 + ROW_NUMBER() 筛选

核心是先在子查询或 CTE 中计算行号,外层过滤 rn = 1。不能在同一个查询层级里 WHERE 引用窗口函数结果。

SELECT id, user_id, status, updated_at FROM (   SELECT *,          ROW_NUMBER() OVER (            PARTITION BY user_id             ORDER BY updated_at DESC, id DESC          ) AS rn   FROM orders ) t WHERE rn = 1;
  • PARTITION BY user_id 表示按用户分组去重;换成 product_id 就是按商品去重
  • ORDER BY updated_at DESC, id DESC 确保更新时间最新且 ID 最大的那条胜出
  • 别忘了给子查询起别名(如 t),否则 MySQL 会报错

MySQL 5.7 或更老版本怎么办

这些版本不支持窗口函数,得用自连接或相关子查询模拟。性能差、写法绕,但有时不得不做:

SELECT o1.* FROM orders o1 WHERE o1.updated_at = (   SELECT MAX(o2.updated_at)   FROM orders o2   WHERE o2.user_id = o1.user_id ) AND o1.id = (   SELECT MAX(o3.id)   FROM orders o3   WHERE o3.user_id = o1.user_id     AND o3.updated_at = o1.updated_at );
  • 这个写法假设“同用户同时间有多条时,取 id 最大的那条”
  • 没有索引的话,user_id + updated_at 组合索引能大幅提速
  • 如果数据量大,这种写法可能比窗口函数慢一个数量级,别在线上表盲目套用

容易被忽略的 NULL 和时区问题

updated_atNULL 的记录会被排在最前面(多数数据库默认 NULLS FIRST),导致它们意外中标。还有时区不一致会让“最新”判断出错。

  • ORDER BY 中显式控制 NULL:加 NULLS LAST(PostgreSQL、Oracle 支持),MySQL 不支持,得提前用 COALESCE(updated_at, '1970-01-01') 处理
  • 确保所有时间字段已转为统一时区(如 UTC),否则跨服务器写入的数据可能因本地时区不同导致排序错乱
  • 如果业务要求“最后插入的那条”,优先用自增 id 排序,它比时间字段更可靠

text=ZqhQzanResources