ROW_NUMBER()按时间降序编号取rn=1是最通用清晰的去重方案,适用于MySQL 8.0+、PostgreSQL等支持窗口函数的数据库;老版本MySQL可用GROUP BY+子查询匹配最大时间;清理数据时推荐自连接删非最新行。
用 ROW_NUMBER() 按时间排序后取 row_num = 1
这是跨数据库最通用、语义最清晰的做法,尤其适合需要“保留最新时间那条”的场景。核心是把每组重复记录按时间字段(比如 updated_at 或 created_at)降序编号,再筛出编号为 1 的行。
常见错误是写成 ORDER BY updated_at ASC——这会保留最老的那条,不是你要的“最新”。
- 必须用
PARTITION BY明确分组字段(如user_id、email),不能漏 -
ORDER BY updated_at DESC才能确保最新时间排第一 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;MySQL 5.7 及更早不支持窗口函数,得换方案
- 示例(查 users 表中每个 email 最新一条):
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY updated_at DESC) AS rn FROM users ) SELECT id, email, updated_at FROM ranked WHERE rn = 1;
用 GROUP BY + 子查询关联找最大时间点
适用于不支持窗口函数的老版本 MySQL(如 5.7),或你只想查数据、不删数据的轻量场景。本质是先算出每组的 MAX(updated_at),再反向匹配原表中对应时间戳的完整行。
容易踩的坑是:如果同一组里有两条记录时间完全相同(比如秒级精度下并发写入),这个方法可能返回多条——它不保证“只一条”,只保证“时间最新”。
- 子查询必须包含所有用于判断重复的字段(如
email)和时间字段(MAX(updated_at)) - 主查询 JOIN 条件要同时匹配分组字段和最大时间值,缺一不可
- 示例:
SELECT u1.* FROM users u1 JOIN ( SELECT email, MAX(updated_at) AS max_time FROM users GROUP BY email ) u2 ON u1.email = u2.email AND u1.updated_at = u2.max_time;
- 若字段允许 NULL,
GROUP BY email会把所有NULL归为一组,需提前用WHERE email IS NOT NULL过滤
DELETE 时直接删掉非最新行(MySQL/PostgreSQL)
真要清理脏数据,不能只查,得删。但直接 DELETE ... WHERE id NOT IN (...) 容易锁表或超内存,推荐用自连接方式逐组比对 ID 或时间戳。
关键点在于:比较逻辑必须严格对应“保留最新”的定义。用时间字段比 ID 更可靠——ID 大不一定更新晚,尤其当数据来自不同导入批次或软删除恢复时。
- MySQL 写法(按
email分组,删updated_at更小的):DELETE u1 FROM users u1 INNER JOIN users u2 WHERE u1.email = u2.email AND u1.updated_at < u2.updated_at;
- PostgreSQL 要改用
USING语法,且不能在USING子句里引用别名u1,得写成:DELETE FROM users u1 USING users u2 WHERE u1.email = u2.email AND u1.updated_at < u2.updated_at;
- 执行前务必加事务并测试:
BEGIN; DELETE ...; SELECT COUNT(*) FROM users;确认删得对再COMMIT
索引没建好,再好的 SQL 也慢得像卡住
所有上述方案在大表(比如百万级以上)上跑,如果没有针对性索引,要么超时,要么拖垮库。这不是可选项,是必做项。
很多人试了几次都失败,最后发现只是缺一个联合索引——不是字段单独有索引就行,必须是组合的。
- 判重字段 + 时间字段要建联合索引,顺序很重要:先写分组字段(如
email),再写排序字段(如updated_at),即INDEX(email, updated_at) - 如果查询还涉及其他 WHERE 条件(比如
status = 'active'),要把这些也加进索引前缀 - 用
EXPLAIN看执行计划:确认type是ref或range,没出现Using filesort或Using temporary - 没索引时,
GROUP BY email在千万级表上可能吃光内存,直接 OOM
真正难的不是写出某条 SQL,而是判断“最新”到底由什么定义——是时间戳?是自增 ID?还是业务侧人工标记的 version 字段?一旦定义模糊,所有去重逻辑都会偏移。而且线上删数据前,没人会替你备份。