核心思路是用GROUP BY+MIN(id)识别重复组并保留最小id行:先SELECT MIN(id) FROM users GROUP BY email HAVING COUNT(*)>1锁定保留行,再用DELETE JOIN(MySQL/PG)或CTE+ROW_NUMBER()(SQL Server)删除其余行,执行前须备份并验证。

用 GROUP BY + MIN(id) 找出每组重复数据的保留行
核心思路是:先识别重复逻辑(比如 email 或 username 字段相同),再为每组选一个要留下的主键(通常是 id 最小的那条)。不能直接 DELETE,得先锁定保留行。
假设表叫 users,按 email 判重,id 是自增主键:
SELECT MIN(id) AS keep_id, email FROM users GROUP BY email HAVING COUNT(*) > 1;
这条语句能列出所有重复 email 组中该保留的 id。注意 HAVING COUNT(*) > 1 不可省略,否则会把唯一值也拉进来。
- 如果字段允许 NULL,
GROUP BY email会把所有NULL归为一组——这可能不是你想要的,需额外处理 - 若用多个字段联合判重(如
name和phone),GROUP BY后要写全:GROUP BY name, phone - 别在没加索引的字段上跑这个查询,大表会极慢;建议对判重字段建联合索引
用 DELETE JOIN 删除非保留行(MySQL / PostgreSQL)
找到保留行后,下一步是删掉同组里其他行。最稳妥的方式是用自连接或子查询关联删除,避免误删。
MySQL 写法(推荐):
DELETE u1 FROM users u1 INNER JOIN users u2 WHERE u1.email = u2.email AND u1.id > u2.id;
这条语句的意思是:对每对相同 email 的记录,只删 id 更大的那个。等价于“每组留最小 id”。
- PostgreSQL 不支持这种
DELETE ... FROM ... JOIN语法,得改用USING: DELETE FROM users u1 USING users u2 WHERE u1.email = u2.email AND u1.id > u2.id;
- SQL Server 要用
CTE + ROW_NUMBER(),后面会提到 - 执行前务必先备份,或在事务里测试:
BEGIN TRANSACTION; ... ROLLBACK;
SQL Server 必须用 CTE + ROW_NUMBER() 实现去重
SQL Server 不支持基于多表的 DELETE 语法,也不能在 WHERE 子句里直接嵌套聚合,所以必须借助 CTE 和窗口函数。
示例(按 email 去重,留 id 最小的):
WITH dupes AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM dupes WHERE rn > 1);关键点在于 PARTITION BY email ORDER BY id:按 email 分组,组内按 id 升序编号,rn = 1 就是该组要留下的那条。
- 如果想留最新插入的(即
id最大的),把ORDER BY id改成ORDER BY id DESC -
ROW_NUMBER()比RANK()更安全,因为不会跳号,确保每行编号唯一 - 大表执行时注意日志空间,
DELETE是完整日志操作;可考虑分批删(例如加TOP 10000循环)
误删风险最高的三个操作习惯
真正出问题的往往不是 SQL 写错,而是执行时疏忽。
- 没加
WHERE条件就运行DELETE FROM users——哪怕只是手抖少敲一个字母,后果都是灾难性的 - 在生产库直接跑去重脚本,不先用
SELECT验证要删哪些行(比如先SELECT * FROM users WHERE id IN (SELECT ...)) - 忽略字符集和大小写敏感性:MySQL 默认
utf8mb4_general_ci是不区分大小写的,John@example.com和john@example.com会被当成重复——而你可能以为它们是不同用户
重复数据清理没有“一键安全方案”,关键判断永远落在“哪条该留”——这个逻辑必须和业务方对齐,而不是只看 id 大小。

















