最稳方式是用ROW_NUMBER()按业务字段分组并ORDER BY指定保留规则,再删rn>1的行;MySQL 8.0+等主流数据库支持,但需CTE或子查询中转,不可直接在DELETE中嵌套窗口函数。

用 ROW_NUMBER() 找出重复行再删
直接 DELETE 重复数据而不留痕迹,最稳的方式是借助窗口函数标序,只保留每组第一条。MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持 ROW_NUMBER(),但语法细节有差异。
核心思路:按业务唯一键(比如 email、user_id)分组,用 ORDER BY id 或 created_at 确保“保留哪一条”可控;然后删掉序号 > 1 的行。
- MySQL 示例(需 8.0+):
DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id;
这个写法不依赖窗口函数,但要求有主键/唯一排序字段,且性能在大数据量下可能变差 - 通用安全写法(推荐):
DELETE FROM users WHERE id NOT IN ( SELECT min_id FROM ( SELECT MIN(id) AS min_id FROM users GROUP BY email ) AS keep );注意:如果email允许NULL,GROUP BY会把所有NULL归为一组,导致只留一个NULL记录——这通常是预期行为,但容易被忽略
GROUP BY + HAVING COUNT(*) > 1 只查不删
这条语句不能删数据,但必须先跑一遍,确认重复逻辑是否符合预期。很多人跳过这步,结果删错了业务关键数据。
- 查哪些邮箱重复了:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;
- 如果要看到重复的完整记录(方便人工核对):
SELECT * FROM users WHERE email IN ( SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1 )</li><li>注意:PostgreSQL 中 <code>GROUP BY</code> 要求 <code>SELECT</code> 列必须在 <code>GROUP BY</code> 中或被聚合,所以不能直接 <code>SELECT *</code> ——得用子查询或 <code>JOIN</code></li></ul><H3>SQLite 没有 <code>ROW_NUMBER()</code> 怎么办</H3><p>SQLite 3.25.0+ 支持窗口函数,但老版本(尤其嵌入式场景常见)不支持。这时只能靠自关联或临时表。</p><ul><li>安全做法(兼容所有 SQLite 版本):<pre class="brush:php;toolbar:false;"><pre class="brush:php;toolbar:false;">CREATE TEMP TABLE dedup AS SELECT MIN(rowid) AS keep_rowid FROM users GROUP BY email; <p>DELETE FROM users WHERE rowid NOT IN (SELECT keep_rowid FROM dedup);</p><p>DROP TABLE dedup;
rowid 是 SQLite 默认隐式主键,只要没定义 <code>INTEGER PRIMARY KEY,就可用;但如果表显式定义了主键,就得换成那个字段名- 别用
DELETE FROM users WHERE rowid IN (...)直接删重复项——容易误删唯一值,因为没排除“最小的那个”
删完必须验证,尤其涉及外键时
删除操作不可逆,而外键约束会让 DELETE 失败或级联删掉其他表数据。不是所有环境都开 FOREIGN_KEY_CHECKS 或 PRAGMA foreign_keys。
- 先查外键依赖:
-- MySQL SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'users';
- 生产环境务必在事务里操作:
BEGIN; DELETE FROM users WHERE ...; -- 立即查剩余行数、抽样验证 SELECT COUNT(*), COUNT(DISTINCT email) FROM users; ROLLBACK; -- 确认无误再 COMMIT
- 真正麻烦的是时间点问题:删的过程中有新插入重复数据,会导致漏删。高并发场景建议加应用层锁或用
SELECT ... FOR UPDATE(MySQL/PostgreSQL 支持)
实际执行前,email 字段有没有索引?没有的话,GROUP BY email 和 JOIN 都会很慢。还有,空字符串 '' 和 NULL 算不算重复——数据库默认认为它们不相等,这点常被当成 bug 处理。

















