MySQL 8.0+才支持ROW_NUMBER()去重,低版本如5.7不支持该函数;需先用SELECT VERSION()确认版本≥8.0.2,再通过PARTITION BY分组、ORDER BY指定保留规则,结合CTE或子查询删除rn>1的重复行,注意索引优化与NULL处理。

MySQL 8.0+ 才能用 ROW_NUMBER() 去重
低版本 MySQL(比如 5.7)根本没 ROW_NUMBER() 函数,强行写会报错 FUNCTION xxx.ROW_NUMBER does not exist。确认版本最直接的方式是执行:
SELECT VERSION();只有输出类似
8.0.25 或更高,才能继续往下走。如果版本不够,得换思路——比如用自连接、临时表或导出再导入。
用 ROW_NUMBER() 标记重复行并删除
核心逻辑是:按业务唯一字段分组(如 email、user_id),给每组内行编号,只保留 ROW_NUMBER() = 1 的那条,其余删掉。注意不能在同一个语句里对同一张表既查又删,必须用派生表或 CTE 绕过 ERROR 1093。
实操建议如下:
- 先用
SELECT预览哪些会被删:SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users;
- 真正删除时,用子查询包装
ROW_NUMBER()结果: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;
- 如果主键不是
id,记得把所有出现的id换成你实际的唯一标识字段;PARTITION BY后面填真正用来判断“重复”的列,可以多个,用逗号隔开,比如PARTITION BY user_id, created_date - 删除前务必备份:
CREATE TABLE users_backup AS SELECT * FROM users;
ORDER BY 决定哪条数据被保留
ROW_NUMBER() 的 ORDER BY 子句不光影响排序,它直接决定谁是 rn = 1——也就是最终留下的那条。比如:ORDER BY created_at DESC 会保留最新的一条;ORDER BY id ASC 会保留 ID 最小的那条。别默认以为“不写就按插入顺序”,MySQL 不保证物理顺序,必须显式指定。
常见错误场景:
- 忘了
ORDER BY,导致ROW_NUMBER()行为不可预测,可能每次执行留下不同的记录 - 用
ORDER BY updated_at却发现某些行updated_at全是NULL,结果这些 NULL 被排在前面,rn = 1落到不该留的数据上 - 想按时间保留最新,但字段类型是
VARCHAR存的时间字符串(如'2023-1-1'),排序会错乱,应先转成DATE或确保格式统一为'2023-01-01'
大表删除要防锁表和超时
如果表有几百万行,上面那个 DELETE ... JOIN 语句可能跑很久,期间锁住整张表,阻塞业务写入。更稳妥的做法是分批删:
先加索引(如果还没建):
ALTER TABLE users ADD INDEX idx_email (email);然后用循环方式,每次删 1000 条:
DELETE FROM users WHERE id IN ( SELECT id FROM ( SELECT id FROM users WHERE email IN ( SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1 ) AND id NOT IN ( SELECT MIN(id) FROM users GROUP BY email ) ORDER BY id LIMIT 1000 ) AS tmp );
这个写法虽然略绕,但能避免单次操作太久。另外记得检查 innodb_lock_wait_timeout 和 max_execution_time,防止中途被中断。
真正难的不是语法,是搞清“重复”的定义是否覆盖所有边界情况——比如空字符串和 NULL 算不算相同?大小写敏感吗?这些都会让 PARTITION BY 的结果和你预期不一样。


















