mysql 8.0.2+才支持row_number()去重,须用select version()确认版本,通过cte或子查询绕开error 1093,order by决定保留哪条数据,删除前必须预览、备份并检查null与索引。

能用,但必须绕开 ERROR 1093,且 ORDER BY 决定哪条被保留——不是“自动留第一条”,不显式指定就留错。
确认 MySQL 版本是否真支持 ROW_NUMBER()
执行 SELECT VERSION();,输出必须是 8.0.2 或更高(如 8.0.33)。8.0.0 和 8.0.1 不支持窗口函数,会报错 FUNCTION xxx.ROW_NUMBER does not exist。别信“8.0+”这种模糊说法,版本号得亲眼看见。
用 CTE + ROW_NUMBER() 删除重复行(推荐写法)
这是目前最可控、逻辑最清晰的方式,核心是把编号和删除拆成两步,避开 MySQL 对同一表的读写限制。
- 先写 CTE 查出每组重复数据中哪些该删:
WITH ranked AS (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users) - 再在外部 DELETE 中引用 CTE:
DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1); -
PARTITION BY email表示按邮箱分组;多个字段去重就写成PARTITION BY user_id, status -
ORDER BY id表示 ID 小的排前面,rn = 1就是保留项;想留最新一条就改ORDER BY created_at DESC - 如果表里有
NULL值,PARTITION BY会把所有NULL归为一组——要排除它们,加WHERE email IS NOT NULL到 CTE 内部
用子查询包装 ROW_NUMBER() 替代 CTE(兼容性稍强)
某些旧版客户端或 ORM 可能对 CTE 解析不稳定,这时可改用派生表:
DELETE t1 FROM users t1 INNER JOIN ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) t2 ON t1.id = t2.id WHERE t2.rn > 1;
注意点:
- 不能写成
DELETE FROM users WHERE id IN (SELECT ... ROW_NUMBER() ...),MySQL 仍会报 ERROR 1093 -
t2是临时结果集,不是原表别名,所以不触发限制 - 大表操作前务必在
email字段加索引:ALTER TABLE users ADD INDEX idx_email (email);,否则PARTITION BY扫描极慢
执行前必须做的三件事
窗口函数看着简洁,但删错不可逆:
- 先用
SELECT预览将删哪些行:SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users;看一眼rn > 1的是不是你真想删的 - 备份不能省:
CREATE TABLE users_backup AS SELECT * FROM users;—— 不是导出 SQL,是直接建表快照 - 检查外键和触发器:如果该表被其他表
FOREIGN KEY引用,删数据可能触发级联或报错;有BEFORE DELETE触发器也要提前评估影响
真正麻烦的从来不是语法,而是 ORDER BY 选错了字段,或者忘了 NULL 也被当一组分进去——这两处一错,删掉的就是本该留下的关键数据。











