用 group by + min() 可找出每组重复数据中最小的 id:先按 email 等业务字段分组,再用 min(id) 定位每组要保留的记录,having count(*) > 1 确保只筛选真正重复的组。

用 GROUP BY + MIN() 找出每组重复数据中最小的 id
核心思路是:先按业务字段(比如 email、phone)分组,再用 MIN(id) 拿到每组里要保留的那条记录的 id。这一步不删数据,只定位该留谁。
假设表叫 users,重复依据是 email 字段:
SELECT MIN(id) AS keep_id, email FROM users GROUP BY email HAVING COUNT(*) > 1;
注意 HAVING COUNT(*) > 1 是关键——只筛出真正重复的组,避免把唯一值也拉进来。
- 别用
WHERE COUNT(*) > 1:聚合函数不能在WHERE里用,会报错Invalid use of group function - 如果重复依据是多个字段(比如
name和phone),GROUP BY后要写全:GROUP BY name, phone -
MIN(id)前提是id是自增主键;如果不是,得确认它真能代表“最早插入”
用 DELETE + JOIN 删除其余重复行
拿到要保留的 id 列表后,不能直接 DELETE FROM ... WHERE id NOT IN (...) —— MySQL 不允许对同一张表先查后删(会报错 You can't specify target table for update in FROM clause)。必须用 JOIN 绕过。
安全写法(推荐):
DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id;
这条语句含义:对每一对 email 相同的记录,只要 u1.id > u2.id,就删掉 u1(即保留小 ID,删掉大 ID)。
- 务必确认
ON条件里包含所有去重字段,否则可能误删 - 执行前先用
SELECT模拟一遍:SELECT u1.* FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id; - 如果表很大,这个 JOIN 可能慢;考虑给
(email, id)加联合索引加速
用窗口函数(MySQL 8.0+)更直观地标记重复行
如果你用的是 MySQL 8.0 或更新版本,ROW_NUMBER() 能让逻辑更清晰:按 email 分组,按 id 排序,序号为 1 的就是要留的。
WITH ranked AS (
SELECT id, email,
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
);
注意:CTE 后的 DELETE 在 MySQL 8.0.19+ 才支持直接关联 CTE;低于此版本需两步走——先把 rn > 1 的 id 查出来存临时表,再删。
-
PARTITION BY email定义分组,ORDER BY id决定谁排第一 - 别用
RANK()或DENSE_RANK():它们对相同id会并列,而我们只需要严格按大小取最小 - 这个方案可读性高,但 CTE + 子查询在大数据量下可能比 JOIN 方案多一次扫描
删完之后记得检查和加固
删除操作不可逆,尤其线上环境。执行完不是结束,而是关键检查点。
- 立刻验证是否还有重复:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1; - 确认总行数变化合理,别因条件写错删多(比如漏了
AND u1.id > u2.id,会删光) - 后续应在业务层或数据库加唯一约束:
ALTER TABLE users ADD UNIQUE KEY uk_email (email);,避免重复再生 - 如果表有外键关联其他表,删前确认级联影响;必要时加事务包裹整个操作
真正麻烦的往往不是语法,而是没想清楚“重复”的定义是否覆盖所有业务场景——比如大小写敏感的 email、空格、NULL 值,这些都会让 GROUP BY 或 JOIN 行为和预期不一致。











