用GROUP BY+HAVING查完全重复行需将所有字段(含主键)纳入GROUP BY,再用HAVING COUNT(*)>1筛选;但该法仅返回分组摘要,不包含ID等原始信息,无法直接删除,须配合窗口函数或自连接定位具体行。

用 GROUP BY + HAVING 找出重复行
直接查出哪些数据完全重复,核心是把所有字段当分组依据,再统计每组出现次数。注意:所有字段都得写进 GROUP BY,包括主键(如果存在)——否则 MySQL 8.0+ 会报错 Expression #1 of SELECT list is not in GROUP BY clause。
示例(假设表叫 users,字段为 name, email, age):
SELECT name, email, age, COUNT(*) AS cnt FROM users GROUP BY name, email, age HAVING cnt > 1;
这个结果告诉你哪些组合重复了、重复几次。但注意:它不返回原始的 id,所以不能直接用于删除——这是初学者最容易卡住的地方。
用自连接或窗口函数定位要删的具体行
要删,就得知道删哪几条记录。推荐用 ROW_NUMBER() 窗口函数(MySQL 8.0+ 支持),按重复字段分组后给每行编号,只保留序号 = 1 的行,其余全删:
DELETE t1 FROM users t1 INNER JOIN users t2 ON t1.name = t2.name AND t1.email = t2.email AND t1.age = t2.age AND t1.id > t2.id;
或者更可控的方式(适合大表):
WITH duplicates AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY name, email, age
ORDER BY id
) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM duplicates WHERE rn > 1);
⚠️ 注意:ORDER BY id 决定了“留哪条”,换成 created_at DESC 就能保留最新的一条。
没有主键或唯一字段时怎么处理
如果表真没主键、也没任何能区分行的字段(比如全是 TEXT 或 JSON 字段),id 不存在,就只能靠 MySQL 的隐式行标识——但不可靠。稳妥做法是先加个临时自增列:
- ALTER TABLE users ADD COLUMN tmp_id INT AUTO_INCREMENT FIRST, ADD PRIMARY KEY (tmp_id);
- 执行完去重逻辑后,再
DROP COLUMN tmp_id
别试图用 SELECT DISTINCT 导出再清空重建——这在生产环境风险极高,且丢失自增计数、索引、外键约束等元信息。
删除前必须做三件事
重复数据清理是高危操作,跳过验证几乎等于删库跑路:
- 先在测试库跑通语句,确认影响行数和保留逻辑是否符合预期
- 对原表执行
SELECT备份关键重复组(比如导出GROUP BY后的全部原始行) - 确保事务开启(
START TRANSACTION),删完立刻SELECT验证,没问题再COMMIT
特别提醒:如果表上有触发器、外键级联或正在被应用实时写入,务必停写或加锁,否则可能删掉刚插入的新数据,或引发死锁。


















