mysql 8.0.2+才支持row_number(),低版本需换方案;delete禁用同表窗口函数,须用cte或子查询封装;partition by定义重复依据,order by决定保留哪条,须确保字段非空、类型一致、有索引,并分批删除大表数据。

确认MySQL版本是否支持ROW_NUMBER()
不支持就直接报错,别浪费时间写完再发现跑不了。执行 SELECT VERSION();,输出必须是 8.0.2 或更高——8.0.1 及更早版本没有 ROW_NUMBER(),会提示 FUNCTION xxx.ROW_NUMBER does not exist。如果版本不够,得换方案:用自连接、临时表或导出再导入。
用子查询或CTE绕过ERROR 1093
MySQL禁止在DELETE语句里直接对同一张表做窗口函数查询并删它,否则报 ERROR 1093。必须把 ROW_NUMBER() 结果放在子查询或CTE里,再让外层DELETE引用。
- 推荐用CTE方式,逻辑清晰:
WITH ranked AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
- 子查询方式也行,但嵌套深一点:
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;
- 别漏掉
PARTITION BY后的字段——这是你定义“重复”的依据,可以多个,比如PARTITION BY user_id, status
ORDER BY 决定哪条数据被保留,不是可选项
ORDER BY 不光为了排序,它直接决定谁是 rn = 1(即最终留下那条)。没写或写错,结果不可控。
- 想留最新一条?用
ORDER BY created_at DESC,别用id DESC——id可能不连续或被跳过 - 字段含NULL?
created_at IS NULL的行会被排在最前(默认),导致rn = 1落到脏数据上。加WHERE created_at IS NOT NULL过滤,或用ORDER BY created_at DESC NULLS LAST(MySQL 8.0.22+ 支持) - 时间存的是字符串?比如
'2023-1-1',排序会错乱。先转成日期:STR_TO_DATE(created_at, '%Y-%m-%d'),再排序
删除前必须备份,且注意索引影响性能
一次误操作可能清空业务关键数据,而且大表没索引时,PARTITION BY 和 ORDER BY 字段全表扫描极慢。
- 备份命令要手敲一遍:
CREATE TABLE users_backup AS SELECT * FROM users;
- 检查是否建了复合索引:
CREATE INDEX idx_email_created ON users(email, created_at);——这对PARTITION BY email ORDER BY created_at DESC查询提速明显 - 如果表超500万行,别一次性删完。改用分批:加
LIMIT 10000到DELETE语句末尾,循环执行,避免长事务锁表
实际执行中最容易被忽略的是 ORDER BY 字段的确定性:时间字段为NULL、类型不一致、未加索引,都会让“保留最新一条”变成随机保留。宁可多查一次 SELECT ... ROW_NUMBER() 预览结果,也不要凭经验跳过验证。











