INSERT IGNORE仅跳过插入时的重复冲突,不清理历史重复数据;真正去重需先定位完全重复行(如GROUP BY或窗口函数),再删除多余记录并添加唯一约束。

为什么 INSERT IGNORE 不能直接清理已有重复数据
INSERT IGNORE 只在插入时跳过违反唯一约束的行,它不扫描、不删除已有数据。想靠它“去重”是误解了它的作用场景——它防重复写入,不治历史脏数据。
常见错误现象:INSERT IGNORE INTO t SELECT * FROM t 看似能“重插去重”,但实际会报错(主键/唯一键冲突),或毫无效果(没定义唯一索引时根本不起作用)。
真正要删重复,得先定位哪些行是“完全重复”(所有字段值都相同),再保留其中一条、删其余。关键前提是:表必须有主键或至少一个能标识单行的列(比如自增 id)。
用 GROUP BY + MIN(id) 找出每组重复中的“保留项”
假设表 t 有字段 a, b, c,且存在完全重复记录,id 是主键。目标是只留每组重复中 id 最小的那条。
先验证重复情况:
SELECT a, b, c, COUNT(*) FROM t GROUP BY a, b, c HAVING COUNT(*) > 1;
再生成待删除的 id 列表(即每组中非最小的那些):
SELECT id FROM t WHERE (a, b, c) IN ( SELECT a, b, c FROM t GROUP BY a, b, c HAVING COUNT(*) > 1 ) AND id NOT IN ( SELECT MIN(id) FROM t GROUP BY a, b, c HAVING COUNT(*) > 1 );
- 注意括号内子查询必须用
(a, b, c)元组形式匹配,不能拆成多个IN - MySQL 5.7+ 支持该写法;8.0+ 更推荐用窗口函数(见下一条)
- 若字段含
NULL,GROUP BY会把它们归为一组,但=判断不成立,需额外处理
用窗口函数(MySQL 8.0+)更安全地识别重复行
对含 NULL 或结构复杂的数据,ROW_NUMBER() 比 GROUP BY 更可控:
WITH ranked AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY a, b, c ORDER BY id) AS rn FROM t ) SELECT id FROM ranked WHERE rn > 1;
这个结果就是所有该删的 id。配合 DELETE 使用:
DELETE t1 FROM t AS t1 INNER JOIN t AS t2 ON t1.a = t2.a AND t1.b = t2.b AND t1.c = t2.c AND t1.id > t2.id;
- 这种
JOIN删除法不依赖id是否连续,也不怕NULL - 但性能随重复量增长而下降;大数据量建议加复合索引
(a, b, c, id) - 务必先备份或在事务中执行:
BEGIN; DELETE ...; SELECT ROW_COUNT(); ROLLBACK;
加唯一约束前必须确保无现存重复
ALTER TABLE t ADD UNIQUE KEY uk_abc (a,b,c) 会失败,只要有一组重复就报 ERROR 1062 (23000): Duplicate entry ... for key 'uk_abc'。
所以流程只能是:查重 → 删重 → 加约束。没有捷径。
加完约束后,后续插入重复数据才会触发 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 的行为。
容易被忽略的一点:唯一约束对 NULL 的处理是宽松的(多个 NULL 不视为重复),如果业务上 NULL 也应算作相同值,得提前用 COALESCE(a, '') 转换,或改用生成列+唯一索引。











