用 group by + max() 先定位每组重复数据中要保留的最大 id,再通过 join 安全删除其余重复行;注意 having count(*) > 1 过滤单条记录,null 值需单独处理。

用 GROUP BY + MAX() 找出每组重复数据中最大的 id
核心思路是:先按业务字段(比如 email、phone)分组,再用 MAX(id) 拿到每组里要保留的那条记录的 id。这步不删数据,只是定位目标。
假设表叫 users,重复依据是 email 字段:
SELECT MAX(id) AS keep_id, email FROM users GROUP BY email HAVING COUNT(*) > 1;
注意 HAVING COUNT(*) > 1 是必须的——它过滤掉那些其实不重复的组,避免误选单条记录的 id。
用 JOIN 或子查询删除其余重复行
直接 DELETE FROM users WHERE id NOT IN (...) 很危险:MySQL 不允许对同一张表先查后删(会报错 You can't specify target table 'users' for update in FROM clause)。得绕开。
推荐用 JOIN 写法,安全且高效:
DELETE u1 FROM users u1
INNER JOIN users u2
ON u1.email = u2.email AND u1.id <p>这条语句的意思是:对每一对 <code>email</code> 相同的记录,只要 <code>u1.id</code> 小于 <code>u2.id</code>,就删掉 <code>u1</code>。最终每组只留下 <code>id</code> 最大的那条。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill2334" title="MySQL"><img
src="https://img.php.cn/upload/skill/000/000/081/178900927846657.jpg" alt="MySQL" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill2334" title="MySQL" class="overflowclass">MySQL</a>
<p class="overflowclass">编写正确的MySQL查询,避免字符集、索引和锁方面的常见陷阱。</p>
</div>
<a rel="nofollow" href="/xiazai/skill2334" title="MySQL" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>如果你习惯子查询,可以这样包装一层(但性能略差):</p><pre class="brush:php;toolbar:false;">DELETE FROM users
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM users GROUP BY email
) AS t
);执行前务必加 WHERE 条件限制影响范围
线上表动辄百万行,一个没加条件的 DELETE 可能锁表几分钟甚至更久。别跳过这步验证:
- 先用
SELECT模拟要删的数据:SELECT * FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id
- 确认无误后,再跑
DELETE,并加上LIMIT控制单次操作量(尤其大表):DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id
- 如果表有主键以外的唯一索引(比如
UNIQUE(email)),删完记得检查是否冲突——因为删除过程可能让原本被覆盖的重复值“浮上来”。
事务 + 备份是底线,不是可选项
哪怕测试环境也别裸删。真实场景下,DELETE 语句一旦执行,binlog 虽能恢复,但中间状态不可逆。最稳妥做法:
开启事务:
BEGIN; DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id 1; -- 没问题再提交 COMMIT;
如果发现删错了,立刻 ROLLBACK。另外,执行前用 mysqldump 导出关键数据,比任何补救都快。
真正麻烦的不是语法,而是没意识到:重复逻辑是否覆盖了所有业务场景(比如 NULL 值在 GROUP BY 中会被归为一组,但 JOIN 条件里 u1.email = u2.email 会跳过 NULL)——这点常被忽略。










