最稳删重复记录的方式是用ROW_NUMBER()窗口函数:先按重复字段分组并按ID升序编号,再删除序号>1的行;MySQL 8.0+、PostgreSQL、SQL Server、Oracle支持,但MySQL 5.7及以下不支持,需改用自连接方案。

用 ROW_NUMBER() 窗口函数定位重复行
直接删重复记录最稳的方式,是先给每组数据打上序号,再把序号大于 1 的删掉。MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持 ROW_NUMBER(),但旧版 MySQL(5.7 及之前)不支持——这点必须确认清楚,否则会报 ERROR 1064。
关键点在于 PARTITION BY 要覆盖所有判定“重复”的字段,ORDER BY 决定哪条被保留(通常选 id 或时间字段升序,保留最早那条):
DELETE t1 FROM users t1 INNER JOIN users t2 ON t1.email = t2.email AND t1.id > t2.id;
上面这种自连接写法在小表上可行,但大数据量时容易锁表、执行慢;而窗口函数方案更直观可控:
-
ROW_NUMBER() OVER (PARTITION BY email, phone ORDER BY id ASC)—— 按业务唯一性字段分组,按主键升序,确保最小id是第 1 行 - 别在
WHERE里直接写窗口函数,得套一层子查询或 CTE,否则报Window function is not allowed in WHERE clause - PostgreSQL 和 SQL Server 支持在
DELETE中用 CTE,MySQL 8.0+ 也支持,但语法略有差异:MySQL 要用DELETE FROM tbl USING cte形式
MySQL 5.7 兼容方案:依赖自连接 + 最小 ID 保留
老版本 MySQL 没窗口函数,只能靠自连接找“非最小 id 的重复行”。核心逻辑是:对每个重复组,找出所有比该组最小 id 大的记录,然后删掉。
常见错误是只写 GROUP BY 却忘了关联原表,导致删错或删不全:
DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.phone = u2.phone AND u1.id > u2.id;
- 必须确保
ON条件中包含全部判重字段(比如email和phone同时相同才算重复),漏一个就会漏删 - 如果表没加联合索引(如
(email, phone)),这个语句会全表扫描两次,几百万行可能跑十几分钟 - 执行前务必加
SELECT COUNT(*)验证要删多少条:SELECT COUNT(*) FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id;
用 GROUP BY + HAVING 查重复但不能直接删
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1 这类语句只能查出哪些值重复,不能用于删除——因为 GROUP BY 后的 SELECT 列受限,且标准 SQL 不允许在 DELETE 中嵌套带 GROUP BY 的子查询(MySQL 会报 You can't specify target table for update in FROM clause)。
- 想用聚合结果辅助清理,得先存到临时表,再关联删,多一步但安全:
CREATE TEMPORARY TABLE dup_emails AS SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1; - 注意临时表字段类型要和原表一致,尤其遇到
TEXT字段做分组时,MySQL 可能隐式截断,导致误判 - 这个方法适合“先看再动手”的场景,不适合自动化脚本,毕竟多了建表、删表两步 I/O
大表删除时的性能与事务风险
删几万行以上,别用单条 DELETE 包揽全部——容易锁表太久、日志暴涨、甚至触发 OOM 或超时中断。
- 分批删更稳妥:
DELETE FROM users WHERE id IN (SELECT id FROM (SELECT id FROM users WHERE email IN (...) ORDER BY id LIMIT 1000) t);,每次只处理 1000 条 - 避免在从库上直接删,尤其主从延迟高时,可能造成从库复制中断;建议在主库操作,等同步完成后再验证
- 如果表有外键引用,删之前确认级联行为,否则可能报
Cannot delete or update a parent row: a foreign key constraint fails
真正麻烦的不是语法,而是删完发现业务逻辑依赖了那些“重复但语义不同”的记录——比如两个同名用户,邮箱相同但手机号不同,却被当成重复删掉了。动手前,先搞清“重复”的业务定义比写 SQL 更重要。










