用group by+having可直接查出重复行:将所有业务字段(如name、email)分组,用having count()>1筛选重复组;注意不可含主键id,且mysql 8.0+禁用select 配合group by。

用 GROUP BY + HAVING 找出重复行
直接查重复记录,别想着先删再查。先确认哪些行真重复——关键是定义“完全重复”:所有字段值都一样才算。用 GROUP BY 把所有列打包分组,再用 HAVING COUNT(*) > 1 筛出重复组。
假设表叫 users,字段是 id, name, email,执行:
SELECT name, email, COUNT(*) FROM users GROUP BY name, email HAVING COUNT(*) > 1;
注意:id 不能放进 GROUP BY,否则每行都是唯一组;也不建议用 * 或 SELECT * 配合 GROUP BY,MySQL 8.0+ 严格模式会报错 ERROR 1055。
用 ROW_NUMBER() 留下一条、删其余(推荐 MySQL 8.0+)
有窗口函数就别硬写自连接。用 ROW_NUMBER() 给每组重复记录编号,只保留 rn = 1 的那条,其余干掉。
示例(删掉 users 中除每组首条外的所有重复项):
DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.name = t2.name AND t1.email = t2.email AND t1.id > t2.id;
但更稳妥的写法是先用 CTE 标记:
WITH duplicates AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY id) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM duplicates WHERE rn > 1);
这个写法清晰、可读性强,且避免了 DELETE ... JOIN 在某些隔离级别下的锁表现异常问题。
MySQL 5.7 及更早版本只能靠自连接或临时表
没 ROW_NUMBER() 就得靠自连接比大小,或者导出去重后再重建表。自连接方案容易误删,关键点在于确保只删“更大 id”的副本:
- 必须用
INNER JOIN,不是LEFT JOIN -
ON条件要覆盖所有业务字段(如name = name AND email = email) -
WHERE中必须有t1.id > t2.id这类偏序条件,否则会全删 - 执行前务必先
SELECT验证将删哪些id,例如:
SELECT t1.id FROM users t1 INNER JOIN users t2 ON t1.name = t2.name AND t1.email = t2.email WHERE t1.id > t2.id;
删除后记得检查主键和索引是否还合理
删完重复数据不等于万事大吉。如果原表靠 id 当主键,但重复行里 id 是分散的,删完可能留下空洞,不影响功能但影响心理预期;更关键的是,如果业务依赖 (name, email) 唯一性,现在该加唯一索引了:
ALTER TABLE users ADD UNIQUE KEY uk_name_email (name, email);
否则下次还会重复。另外,DELETE 操作在大表上会锁表或产生大量 binlog,生产环境务必避开高峰期,并确认备份已就绪。
真正麻烦的不是怎么删,而是怎么定义“重复”——字段要不要忽略空值?大小写算不算不同?这些逻辑必须跟业务方对齐,代码里没法自动猜。











