用row_number()删除重复记录并保留每组序号为1的行:先以partition by分组、order by排序编号,再通过cte或派生表删除rn>1的行,mysql 8.0+支持,需避免直接在delete中嵌套窗口函数。

用 ROW_NUMBER() 找出重复行再删除(MySQL 8.0+)
MySQL 8.0 开始支持窗口函数,ROW_NUMBER() 是最稳妥的去重方式——它能按指定字段分组编号,只保留每组序号为 1 的行,其余全删。
常见错误是直接 GROUP BY 后用 DELETE,MySQL 不允许对同一张表先查后删(报错 ERROR 1093)。必须用派生表或 CTE 绕过。
假设表 users 中 email 字段重复,要保留 id 最小的那条:
DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id;
但这个写法在大数据量时性能差、且无法控制“保留哪一条”。更推荐 CTE 方式:
WITH dupes 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 dupes WHERE rn > 1
);
-
PARTITION BY email表示按邮箱分组 -
ORDER BY id决定谁被保留:这里id小的排前面,rn = 1就是保留项 - 必须用
WITH包裹再DELETE,否则 MySQL 会拒绝执行
低版本 MySQL(5.7 及以下)用自连接删除
没有窗口函数时,只能靠自连接 + 比较主键来识别重复。核心逻辑是:对每条记录,找同字段值但主键更小的另一条;如果存在,说明当前这条不是“最早”的,该删。
仍以 users(email) 为例:
DELETE u1 FROM users u1 INNER JOIN users u2 WHERE u1.email = u2.email AND u1.id > u2.id;
- 这是最常用也最兼容的方案,但要注意:必须有可比较的唯一字段(如
id),否则无法确定“保留谁” - 若无主键或唯一索引,
u1.id > u2.id条件失效,可能误删或漏删 - 执行前务必加索引:
ALTER TABLE users ADD INDEX idx_email (email);,否则全表扫描极慢
用临时表重建去重数据(安全但耗资源)
当表结构复杂、外键多、或你不敢直接删线上数据时,重建是最保险的做法。本质是把去重后的数据导出到新表,再替换原表。
步骤如下:
CREATE TABLE users_dedup AS SELECT MIN(id) AS id, email, name, created_at FROM users GROUP BY email;
-
GROUP BY email是关键,但注意:非分组字段(如name)要用聚合函数包裹,否则 MySQL 5.7+ 会报错(sql_mode=only_full_group_by默认开启) - 如果
name可能不一致,用MAX(name)或ANY_VALUE(name)(需确认语义是否可接受) - 重建后记得恢复索引、外键、注释等元信息,原表
DROP前先RENAME备份
删除前必须做的三件事
去重操作不可逆,跳过验证大概率导致业务异常。
- 先查重复分布:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;看有多少组、最大重复几次 - 备份整表:
CREATE TABLE users_backup AS SELECT * FROM users;(或用mysqldump) - 在测试库跑通语句,并用
SELECT模拟删哪些行:SELECT u1.* FROM users u1 INNER JOIN users u2 WHERE u1.email = u2.email AND u1.id > u2.id;
真正麻烦的从来不是语法,而是没想清楚“重复”的定义:是整行相同?还是某几个字段组合相同?以及“保留哪一条”的业务规则是否明确。字段含义模糊、空值处理(NULL != NULL)、字符集排序规则差异,都可能让看似正确的 SQL 漏掉或误删数据。











