MySQL 5.7 及更早版本不支持直接子查询删重,需用多层嵌套子查询加别名(如 AS keep)或 LEFT JOIN 自关联;推荐后者,兼容性好且稳定,但务必加联合索引并先 SELECT 验证再分批删除。

用子查询配合 ROW_NUMBER() 标识重复行
直接用 DELETE 加子查询删重复,MySQL 5.7 及更早版本不支持在同一个语句中对目标表进行子查询引用(报错 You can't specify target table 'xxx' for update in FROM clause)。现代写法得靠窗口函数或临时中间结果。PostgreSQL 和 MySQL 8.0+ 支持 ROW_NUMBER(),可先给每组重复数据编号,再删掉编号 > 1 的行。
假设表 users 中 email 字段重复,保留 id 最小的记录:
DELETE FROM users
WHERE id NOT IN (
SELECT min_id FROM (
SELECT MIN(id) AS min_id
FROM users
GROUP BY email
) AS keep
);这个写法绕开了 MySQL 的限制,本质是把子查询结果包一层别名(AS keep),让优化器认为是“派生表”而非原表引用。
MySQL 5.7 或更低版本必须用多层嵌套子查询
如果直接写 SELECT MIN(id) FROM users GROUP BY email 作为子查询,MySQL 5.7 会报错。必须再套一层:
- 外层子查询不能直接引用原表,所以要先
GROUP BY出保留的id,再从这结果里取值 - 不能用
IN (SELECT ...)直接套,必须加AS xxx别名,哪怕只是AS t - 如果表很大,
IN子查询可能变慢;可改用LEFT JOIN自关联替代(见下一条)
用 LEFT JOIN 自关联比子查询更稳定
对大表或旧版 MySQL,LEFT JOIN 方式更可靠,也更容易理解逻辑:
DELETE u1 FROM users u1 LEFT JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id WHERE u2.id IS NOT NULL;
它表示:对每条记录 u1,找同 email 但 id 更小的另一条 u2;如果找到了(即 u2.id IS NOT NULL),说明 u1 是该组里的“更大者”,应删除。
- 这个写法在所有 MySQL 版本都可用,不依赖窗口函数
- 注意连接条件中的
u1.id > u2.id—— 写反了会删错,或删光 - 务必加索引:
ALTER TABLE users ADD INDEX idx_email_id (email, id);,否则全表扫描极慢
误删风险高,操作前必须备份或加 LIMIT
删重复不是原子操作,一旦出错很难回滚。生产环境严禁裸跑 DELETE:
- 先用
SELECT模拟要删的行:SELECT * FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY email); - 确认无误后,在真实
DELETE后加LIMIT 100分批执行,尤其当预估重复量大时 - 如果表有外键、触发器或正在被应用读写,删之前停写或锁表(
LOCK TABLES users WRITE;),避免中间状态不一致
真正麻烦的从来不是语法,而是删完发现少了一半用户——因为重复判断逻辑漏了 NULL 值,或没考虑大小写敏感的字段比对。

















