最直接可靠的方式是DELETE + 子查询保留最小id:先用派生表查出每组重复字段(如name, email)的MIN(id),再删id不在其中的行;MySQL需套一层(SELECT...)绕过限制;窗口函数方案更直观但需数据库支持ROW_NUMBER()。

用 DELETE + 子查询保留最小 id
最直接可靠的方式是:对每组重复数据,查出除最小 id 外的所有行,然后删掉。关键在于子查询不能直接引用外层表(MySQL 5.7+ 会报错 You can't specify target table for update in FROM clause),必须套一层派生表。
- 先写一个子查询找出每组重复字段对应的最小
id(比如按name,email判重) - 再用外层
DELETE排除这些最小id,只删其余行 - MySQL 中必须把子查询包在
(SELECT ...)内作为临时表,绕过限制
DELETE FROM users
WHERE id NOT IN (
SELECT min_id FROM (
SELECT MIN(id) AS min_id
FROM users
GROUP BY name, email
) AS t
);
用窗口函数(PostgreSQL / MySQL 8.0+ / SQL Server)更清晰
如果数据库支持 ROW_NUMBER(),逻辑更直观:给每组重复数据按 id 排序,只保留序号为 1 的行。注意不能直接在 DELETE 中用窗口函数,得用 CTE 或子查询中转。
- CTE 中计算
ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY id) - 外层删掉
rn > 1的所有行 - MySQL 8.0+ 和 PostgreSQL 可直接用;SQLite 目前不支持窗口函数用于删除场景
WITH dupes AS (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY name, email ORDER BY id
) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM dupes WHERE rn > 1);
误删风险高,务必先备份或用 SELECT 验证
这类操作不可逆,尤其当表大、条件复杂时,很容易多删或少删。别跳过验证步骤。
- 执行前先用
SELECT模拟要删的行:SELECT * FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY name, email); - 确认结果集数量和样例数据符合预期,再执行
DELETE - 生产环境建议先
CREATE TABLE users_backup AS SELECT * FROM users; - 如果表有外键关联,
DELETE可能触发级联或报错,需检查约束行为
GROUP BY 字段选错会导致逻辑错误
判重依据必须是你业务上真正认为“重复”的字段组合。只按 name 判,可能把同名不同人删掉;漏掉 email 又可能留错数据。
- 常见错误:用
GROUP BY name但实际需要GROUP BY name, phone, address - 空值(
NULL)在GROUP BY中会被视为相同值,但有些场景下NULL表示未知,不应合并 —— 这时得加WHERE name IS NOT NULL AND email IS NOT NULL - 字符集或大小写敏感也会影响分组结果,比如
'Alice'和'alice'在utf8mb4_0900_as_cs下算不同值
实际执行前,id 是否有索引、重复比例多高、表是否锁表,都会影响耗时和阻塞程度。别在高峰期跑。










