数据表里混进了重复记录,是MySQL日常维护中非常常见的问题。造成重复的原因有很多:业务代码并发写入没有做好幂等控制、导入数据时没有做唯一性校验、或者表结构本身缺少唯一索引约束,都可能让同一份数据被插入多次。重复记录的危害不只是浪费存储,更麻烦的是会让count统计翻倍、报表数据失真,甚至引发业务逻辑判断错误。这篇文章就来系统地讲一讲,如何用SQL语句定位并批量删除表中的重复数据,同时保留其中一条有效记录。

第一步:先定位哪些数据重复了
删除之前必须先把重复记录查出来,确认重复的范围和数量,否则盲目执行删除语句风险很大。判断重复的标准因业务而异,通常是根据某几个字段组合来判断,比如用户表中的手机号、订单表中的订单号加商品ID等。
最常用的定位方式是GROUP BY配合HAVING子句。假设有一张user表,我们以mobile字段作为唯一性判断依据,查询重复的语句如下:
SELECT mobile, COUNT(*) AS cnt FROM user GROUP BY mobile HAVING COUNT(*) > 1;
这条语句会列出所有出现次数大于1的手机号以及各自的重复次数。如果需要判断的是多个字段组合,比如name和mobile一起才算重复,把这两个字段都放进GROUP BY即可。拿到重复清单后,建议先用SELECT核对一遍数据,确认这些确实是需要清理的重复项,再进行下一步的删除操作。
另外提醒一点,判断重复的字段上最好有索引,否则在大表上执行GROUP BY会触发全表扫描加临时表排序,速度会非常慢,甚至拖垮线上数据库。可以在正式去重前先加上普通索引,等清理完成后再改建为唯一索引。
方案一:自连接删除法,通用性最好
自连接删除是最经典的去重写法,原理是让表和自己做JOIN,通过主键大小比较,保留重复组中ID最小的那条,删掉其余记录。这种方式不依赖MySQL版本特性,5.x和8.x都能用,是兼容性最好的方案。
DELETE t1 FROM user t1
INNER JOIN user t2
ON t1.mobile = t2.mobile
AND t1.id > t2.id;
这条语句的逻辑是:对于同一手机号的多条记录,id较大的那些行都能匹配到id更小的行,因此会被删除,只有每组id最小的记录因为找不到比自己id小的匹配项而保留下来。如果你想保留最新的一条,把条件反过来改成t1.id < t2.id即可。
需要注意的是,多字段联合判断时,JOIN条件要把所有判断字段都写上。如果判断字段中可能存在NULL值,等值连接是匹配不到NULL的,这类NULL重复记录需要单独用其他方式处理,比如先用UPDATE把NULL填充成默认值再执行删除。
方案二:窗口函数法,MySQL 8.0以上的首选
如果你的MySQL版本是8.0及以上,强烈推荐用窗口函数来处理去重。ROW_NUMBER()可以给每一组重复记录编号,然后删除编号大于1的行,思路清晰且执行效率高,尤其适合需要按特定规则保留某一条的场景。
DELETE FROM user
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY mobile ORDER BY id) AS rn
FROM user
) tmp
WHERE tmp.rn > 1
);
这里PARTITION BY mobile表示按手机号分组,ORDER BY id表示每组内按id升序编号,rn为1的就是要保留的那条,其余全部删除。外面再套一层临时派生表是因为MySQL不允许在DELETE语句中直接查询要删除的同一张表,必须通过派生表绕开这个限制。
窗口函数法的优势在于排序规则可以灵活定制。比如每组内想保留创建时间最早的那条,把ORDER BY改成create_time就行;想保留最新数据就改成ORDER BY create_time DESC。这种灵活性是自连接法不太好实现的。缺点是对老版本MySQL无效,且大表执行时派生表会占用较多临时空间,建议在业务低峰期操作。
方案三:临时表重建法,大数据量场景更稳妥
当表的数据量达到千万级别,直接在原表上执行DELETE会产生大量行锁和undo日志,主从延迟也会明显放大。这时更稳妥的做法是新建一张结构相同的临时表,把去重后的数据插入进去,然后整体替换。
CREATE TABLE user_new LIKE user;
INSERT INTO user_new (id, mobile, name, create_time)
SELECT id, mobile, name, create_time
FROM user t
WHERE id = (
SELECT MIN(id) FROM user WHERE mobile = t.mobile
);
RENAME TABLE user TO user_old, user_new TO user;
DROP TABLE user_old;
这个方案的核心是子查询只取每组id最小的记录插入新表。整个过程对原表只有读操作,不会有长时间持锁的问题。RENAME TABLE是原子操作,切换瞬间完成,业务几乎无感知。等确认新表数据没问题后,再删除旧表释放空间。
临时表法的额外好处是顺便完成了表空间整理,删除操作留下的大量空洞会被清理掉。要注意的是,替换期间新写入的数据不会进入新表,所以操作前最好停掉相关写入任务,或者选在写入极少的时段进行,并在切换后做好数据补录。
去重后的收尾:加上唯一索引防止复发
清理完重复数据只是治标,要防止问题再次出现,必须给判断重复的字段组合加上唯一索引。以手机号为例:
ALTER TABLE user ADD UNIQUE KEY uk_mobile (mobile);
加了唯一索引之后,后续再有重复插入会直接报错被拦截。如果业务上希望重复插入时静默忽略而不是报错,可以把INSERT语句改成INSERT IGNORE,遇到唯一键冲突时会自动跳过该条插入,不产生错误。也可以用ON DUPLICATE KEY UPDATE把插入转成更新,实现幂等写入效果。
最后强调几个操作规范:第一,任何去重操作执行前先备份,哪怕只是把表导出一份,出问题时可以快速恢复;第二,先在测试环境跑通整个流程,确认SQL的删除范围和预期一致;第三,大表操作放在低峰期,避免锁表影响线上业务;第四,去重SQL要记录下来存档,方便日后审计追溯。按这几步走下来,重复数据问题基本可以得到彻底解决。
mysql删除重复记录SQL去重批量去重语句修改时间:2026-09-12 20:38:33