row_number()不能直接在delete的where中使用,必须通过cte或子查询先生成序号结果,再关联删除;mysql 8.0+支持该函数,需配合partition by和order by分组排序,且须避免error 1093。

ROW_NUMBER() 不能直接删除数据,必须配合子查询或临时表
MySQL 8.0 支持 ROW_NUMBER() 窗口函数,但它只能用于查询排序和编号,**不能出现在 DELETE 语句的 WHERE 子句中**。直接写 DELETE FROM t WHERE ROW_NUMBER() OVER (...) > 1 会报错 ERROR 1064 (42000):语法错误,因为窗口函数不允许在 DML 的条件部分使用。
实际可行路径只有两条:
- 用
ROW_NUMBER()构造带序号的中间结果,再通过JOIN或子查询定位重复行 - 把编号结果存入临时表或 CTE,再基于该结果执行
DELETE
用 CTE + JOIN 删除重复行(推荐)
这是最清晰、可读性高且避免锁表风险的方式。假设表 users 中按 email 判重,保留每组中 id 最小的记录:
WITH ranked AS (
SELECT id, email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
)
DELETE u FROM users u
INNER JOIN ranked r ON u.id = r.id
WHERE r.rn > 1;
注意点:
-
PARTITION BY email定义重复逻辑;ORDER BY id决定哪条被保留(最小id排第一) - 必须用
DELETE ... FROM多表语法,不能写成DELETE FROM users WHERE id IN (SELECT ...),否则 MySQL 会报错ERROR 1093:不能在子查询中指定目标表 - CTE 在 MySQL 8.0+ 中支持递归和非递归,但仅限于单个语句作用域,不能跨语句复用
用自连接模拟 ROW_NUMBER()(兼容旧版思路,不推荐)
如果误以为 ROW_NUMBER() 是唯一解,可能忽略更轻量的替代方案。其实对简单去重,用自连接也能达到类似效果,且语义更直白:
DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id;
这个语句等价于“删掉所有邮箱相同但 ID 更大的记录”,效果和上面 CTE 方案一致,但:
- 没有窗口函数开销,执行计划通常更优
- 不依赖 MySQL 8.0,5.7 也能跑
- 但如果去重维度多(比如
(email, phone))、或需要按时间倒序保留最新记录,自连接条件会迅速变复杂,反而不如ROW_NUMBER()清晰
WHERE 条件里误用 ROW_NUMBER() 的典型报错
新手常写的错误语句:
DELETE FROM users WHERE id NOT IN ( SELECT MIN(id) FROM users GROUP BY email );
这看似能去重,但一旦 email 有 NULL 值,GROUP BY email 会让所有 NULL 归为一组,MIN(id) 可能返回 NULL,导致整个 NOT IN 判定为 UNKNOWN,结果一条不删 —— 这是隐式类型转换和三值逻辑埋的坑。
更隐蔽的问题是:如果执行前没加 WHERE 限定范围,又没开事务,一不小心就删光了表。务必先用 SELECT 验证:
SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users HAVING rn > 1;
—— 注意这里不能用 WHERE rn > 1,必须用 HAVING,因为 rn 是窗口计算字段,不属于原始行数据。
真正动手删之前,永远先备份或在事务里试删几条。











