用户表里同一个手机号出现四条记录,运营要清掉重复的,开发写了句 delete 直接执行,结果把最新那条也删了。数据没备份,只能从日志里捞。

去重sql 的难点不是写 delete,是先决定留哪条、按什么判定重复。这一步想不清,删完一定后悔。

先查出来看清楚

按业务字段分组,数一下每组几条:

SELECT phone, COUNT(*) AS cnt FROM users GROUP BY phone HAVING cnt > 1;

把 phone 换成实际字段,能看出重复集中在哪。再看组里的 id、创建时间、状态,决定保留规则。一般留 id 最小或创建时间最早的那条,也有留状态最新的。

删除的三种写法

MySQL 里可以直接多表删除,保留每组最小 id:

DELETE u FROM users u JOIN (SELECT phone, MIN(id) AS keep_id FROM users GROUP BY phone) t ON u.phone = t.phone WHERE u.id <> t.keep_id;

PostgreSQL 用 ctid 或主键子查询;SQL Server 用 CTE 加 ROW_NUMBER。窗口函数更直观,先给每组打序号,再删序号大于 1 的:

SELECT id, ROW_NUMBER() OVER (PARTITION BY phone ORDER BY id) AS rn FROM users;

MySQL 8.0、PostgreSQL、SQL Server 2012 以后都支持。老版本 MySQL 没有窗口函数,只能用上面的 join 写法。

报 1093 是怎么回事

在 MySQL 里写 delete from users where id not in 子查询,会报 ERROR 1093,不能在同一个表里边查边删。解决办法是套一层派生表:

DELETE FROM users WHERE id NOT IN (SELECT keep_id FROM (SELECT MIN(id) AS keep_id FROM users GROUP BY phone) t);

派生表要有别名,否则 MySQL 一样报错。

删之前和删之后

先备份,或者把要删的行导到临时表,出问题还能回滚。执行时看 affected rows,数量跟预估差太多就停下来。删完加唯一索引,从源头堵住:

ALTER TABLE users ADD UNIQUE KEY uk_phone (phone);

历史脏数据多的话,唯一索引加不上,得先清洗再建。

结论:去重sql 先分组查、定保留规则、备份,再删;能加唯一索引就别靠定期清理。