MySQL 删除重复行不能直接用 DELETE + GROUP BY,因引擎禁止对目标表在子查询中使用 GROUP BY,会报错 ERROR 1093;需借助临时表先查出保留记录(如 MIN(id) 或 ROW_NUMBER()),再执行删除,确保安全与准确性。

MySQL 删除重复行时为什么不能直接 DELETE + GROUP BY
因为 MySQL 不允许在 DELETE 语句中对目标表同时做子查询(尤其涉及 GROUP BY 或聚合),会报错:ERROR 1093 (HY000): You can't specify target table 't' for update in FROM clause。这不是语法懒,是引擎层面的限制——避免在修改过程中读到未提交的中间状态。
临时表能绕过这个限制:先把去重后的主键或唯一标识查出来,存到一张新表里,再用它来驱动删除。本质是把“一边读一边删”的危险操作,拆成“先读稳了,再删干净”两步。
- 只适用于有明确判断重复逻辑的场景(比如按
email和name判重) - 临时表默认只在当前会话可见,断开连接自动销毁,安全但要注意别漏掉
DROP TEMPORARY TABLE - 如果原表数据量大,
CREATE TEMPORARY TABLE ... SELECT这一步可能锁表或耗内存,建议在低峰期操作
用临时表保留每组第一条记录(基于自增 ID)
这是最常见也最稳妥的做法:假设表 users 有自增主键 id,重复依据是 email 字段,目标是只留 id 最小的那条。
先建临时表存每组最小 id:
CREATE TEMPORARY TABLE tmp_keep AS SELECT MIN(id) AS id FROM users GROUP BY email;
再删掉不在这个集合里的所有行:
DELETE FROM users WHERE id NOT IN (SELECT id FROM tmp_keep);
-
MIN(id)是人为指定“保留哪一条”,换成MAX(id)就是留最新插入的 - 如果
email允许为NULL,GROUP BY email会把所有NULL归为一组,导致误删;需额外处理:GROUP BY IFNULL(email, CONCAT('null-', id)) - 确保
tmp_keep.id有索引(临时表默认没索引),否则NOT IN查询会很慢;可加:ALTER TABLE tmp_keep ADD INDEX idx_id (id);
没有自增 ID 怎么办:靠 ROW_NUMBER() 模拟(MySQL 8.0+)
如果表没主键、也没唯一字段,但 MySQL 版本 ≥ 8.0,可以用窗口函数定位重复组内的序号:
CREATE TEMPORARY TABLE tmp_keep AS SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) t WHERE rn = 1;
然后照常删:
DELETE u FROM users u LEFT JOIN tmp_keep t ON u.id = t.id WHERE t.id IS NULL;
- 必须用
LEFT JOIN+IS NULL,不能用NOT IN,因为NOT IN遇到NULL整个条件失效 -
ORDER BY id仍是为了确定“第一条”,若想按时间字段(如created_at)排序,就换掉它 - 低于 MySQL 8.0 的版本不支持
ROW_NUMBER(),强行用会报错:FUNCTION yourdb.ROW_NUMBER does not exist
删完之后要不要重建自增 ID 或优化表
删除只是移除数据行,不会自动收缩 AUTO_INCREMENT 值,也不会释放磁盘空间。如果后续要频繁插入,且 ID 已经跳得很大,可以考虑 ALTER TABLE ... AUTO_INCREMENT = N 重置起始值;但更关键的是执行 OPTIMIZE TABLE。
-
OPTIMIZE TABLE users会重建表、整理碎片、更新统计信息,对 InnoDB 表还可能回收部分空间(取决于innodb_file_per_table设置) - 该操作会锁表,线上大表慎用;替代方案是用
ALTER TABLE users ENGINE=InnoDB(效果类似,但更可控) - 删完务必验证:用
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1确认无残留重复
真正麻烦的不是怎么删,而是删之前没确认好“什么是重复”——字段组合选错、NULL 处理遗漏、边界数据没覆盖,都会导致误删。动手前先用 SELECT 把要删的行打出来看看,比什么都靠谱。










