最常用且稳妥的做法是用DELETE+子查询保留每组重复数据中ID最小的记录:DELETE FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY name, email),但需注意NOT IN在子查询含NULL时会失效,建议配合WHERE过滤空值或改用ROW_NUMBER()窗口函数。

用 DELETE + 子查询保留最小 ID 的记录
直接删掉重复行中 ID 更大的那些,只留每个重复组里 ID 最小的——这是最常用也最稳妥的做法。核心思路是:对每组重复数据(比如按 name 和 email 判重),找出其中 ID 不是最小的那些,然后删掉。
假设表叫 users,用 name 和 email 判断是否重复:
DELETE FROM users WHERE id NOT IN ( SELECT MIN(id) FROM users GROUP BY name, email );
注意:NOT IN 在子查询结果含 NULL 时会整个失效(返回空集),如果字段允许为空,得先过滤或改用 NOT EXISTS。
用窗口函数(MySQL 8.0+/PostgreSQL/SQL Server)更安全
当重复字段可能为 NULL,或你想更清楚地控制“分组依据”和“排序逻辑”时,ROW_NUMBER() 是更可靠的选择。它能明确给每组内的行编号,避免 NOT IN 的空值陷阱。
示例(以 PostgreSQL 为例):
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY name, email ORDER BY id
) AS rn
FROM users
) t
WHERE rn > 1
);-
PARTITION BY name, email定义重复组 -
ORDER BY id确保编号 1 对应最小id - 只删
rn > 1的行,即每组中除第一条外的所有行
执行前必须做三件事
删数据不是试错操作。哪怕语句看着简单,也容易误删。务必在真实执行前确认以下三点:
- 先用
SELECT模拟要删的行:SELECT * FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY name, email); - 检查是否有唯一索引或外键约束影响删除行为(比如级联删除意外触发)
- 在生产环境操作前,用
BEGIN TRANSACTION包裹(如支持),删完立刻SELECT COUNT(*)核对数量变化
为什么不能用 GROUP BY + HAVING 直接删?
很多人想写 DELETE FROM users GROUP BY name, email HAVING COUNT(*) > 1,这在标准 SQL 中不合法,MySQL 虽然部分版本允许但行为不可靠——它不会告诉你删了哪几条,也不保证留下的就是 MIN(id)。真正起作用的永远是显式指定保留条件的子查询或窗口逻辑。
最易被忽略的是 NULL 值参与分组的行为:不同数据库对 GROUP BY a, b 中 a IS NULL AND b IS NULL 是否算作同一组,处理方式不一致。建议提前用 WHERE name IS NOT NULL AND email IS NOT NULL 显式过滤,或统一用 COALESCE(name, '') 转换,否则窗口函数或子查询的结果可能和预期不符。

















