用GROUP BY+MIN()先查出每组最小ID再删除其余记录,需处理NULL值并加索引;推荐LEFT JOIN自关联方式,逻辑清晰且对NULL友好;执行前必须备份并验证影响行数。

用 GROUP BY + MIN() 找出要保留的最小 ID
核心思路是:先按业务字段分组,每组取 MIN(id) 作为唯一保留的记录 ID。这比直接 DELETE 更安全,能先看清留哪些、删哪些。
假设表叫 users,重复依据是 email 字段(即相同 email 视为重复),那么查出每组要保留的最小 ID 就是:
SELECT MIN(id) AS keep_id FROM users GROUP BY email;
注意:如果 email 允许为 NULL,MySQL 默认把所有 NULL 归为同一组,可能误删——需要额外处理,比如加 HAVING COUNT(*) > 1 过滤真重复,或用 WHERE email IS NOT NULL 显式排除。
DELETE WHERE id NOT IN (子查询结果)
拿到要保留的 ID 列表后,删掉其余所有。但 MySQL 不允许直接在子查询中对同一张表做 DELETE,会报错 You can't specify target table 'users' for update in FROM clause。
绕过方法是把子查询再包一层派生表(临时表):
DELETE FROM users WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MIN(id) AS keep_id FROM users GROUP BY email
) AS tmp
);
- 必须给内层子查询加别名(如
AS tmp),否则语法错误 - 如果表很大,
NOT IN在遇到NULL时会整体返回空结果,导致一条不删——确保子查询结果不含NULL,可加WHERE email IS NOT NULL或用LEFT JOIN替代
用 LEFT JOIN 自关联删除(更稳定,推荐)
相比 NOT IN,自连接方式逻辑清晰、对 NULL 友好、执行计划通常更可控,适合生产环境。
思路:把表自连接,找“同 email 但 id 更小”的记录;没找到匹配的,说明当前行就是该 email 中最小的,应保留;反之则删除:
DELETE u1 FROM users u1 LEFT JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id WHERE u2.id IS NOT NULL;
-
u1.id > u2.id是关键:只匹配出“有更小 ID 存在”的行 -
WHERE u2.id IS NOT NULL表示这条u1记录不是最小的,可以删 - 如果重复字段是多个(比如
(email, phone)),只需在ON条件里补全:u1.email = u2.email AND u1.phone = u2.phone AND u1.id > u2.id
执行前务必备份 & 加索引
DELETE 是不可逆操作,尤其跨万级数据时,一次误操作可能丢失全部业务数据。
建议步骤:
- 先在测试库跑通语句,确认影响行数(用
SELECT COUNT(*)模拟) - 对用于分组的字段(如
email)建联合索引,否则JOIN或GROUP BY会全表扫描,慢且锁表久 - 大表操作避开高峰期,并考虑分批删(例如按 ID 范围加
WHERE id BETWEEN ...)
最容易被忽略的是 NULL 值行为和索引缺失——没索引时,一个 10 万行的表可能卡住整个数据库连接池。


















