最稳方式是用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 )
- 注意:PostgreSQL 中
GROUP BY要求SELECT列必须在GROUP BY中或被聚合,所以不能直接SELECT *——得用子查询或JOIN
SQLite 没有 ROW_NUMBER() 怎么办
SQLite 3.25.0+ 支持窗口函数,但老版本(尤其嵌入式场景常见)不支持。这时只能靠自关联或临时表。
- 安全做法(兼容所有 SQLite 版本):
<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;</p>
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 处理。











