应使用 ROW_NUMBER() 标记重复行后删除,而非直接 DELETE;需先明确保留逻辑(如按 id 最小或时间最新),再通过 PARTITION BY 和 ORDER BY 打标记,仅删 ROW_NUMBER() > 1 的行。

用 ROW_NUMBER() 标记重复行再删,别直接 DELETE 多行
直接写 DELETE FROM table WHERE ... 删除“重复记录”大概率误删——因为没定义哪条该留、哪条该删。必须先明确保留逻辑(比如按 id 最小/最大,或按时间最新),再用窗口函数打标记。MySQL 8.0+、PostgreSQL、SQL Server 都支持 ROW_NUMBER(),这是最可控的方式。
常见错误是试图用 GROUP BY + HAVING COUNT > 1 直接删,但标准 SQL 不允许在 DELETE 中用聚合结果定位具体行;即使某些方言支持,也极易漏删或多删。
- 先确认重复依据字段,比如
email和name组合重复:PARTITION BY email, name - 指定排序规则决定留哪条:想留
id最小的,就用ORDER BY id ASC;想留最新插入的,且有created_at字段,则用ORDER BY created_at DESC - 只删
ROW_NUMBER() > 1的行,ROW_NUMBER() = 1的自动被保留
MySQL 5.7 或更老版本不能用窗口函数?改用自关联 JOIN
MySQL 5.7 及之前不支持 ROW_NUMBER(),但可以用 JOIN 自关联 + 子查询模拟。核心思路是:对每组重复数据,找出“该删的那几条”的 id,再用 IN 或 JOIN 删除。
注意:不能写成 DELETE t1 FROM table t1, table t2 WHERE t1.email = t2.email AND t1.id > t2.id 这种形式——它在某些 MySQL 版本会报错 You can't specify target table for update in FROM clause。
- 正确写法是用
JOIN语法:DELETE t1 FROM table t1 JOIN table t2 ON t1.email = t2.email AND t1.name = t2.name AND t1.id > t2.id - 确保
t1.id > t2.id中的t2是同一组中“要保留的那条”,所以t2必须带条件限制为每组最小id,否则可能删光整组 - 更稳妥的做法是先查出要删的
id列表:SELECT t1.id FROM table t1 JOIN (SELECT email, name, MIN(id) AS min_id FROM table GROUP BY email, name) t2 ON t1.email = t2.email AND t1.name = t2.name AND t1.id > t2.min_id,再套进DELETE ... WHERE id IN (...)
PostgreSQL 或 SQL Server 中用 CTE 做删除更清晰
CTE(公用表表达式)配合 ROW_NUMBER() 在 PostgreSQL 和 SQL Server 中可读性更好,也避免嵌套子查询带来的括号混乱。关键是 CTE 必须包含主键或唯一标识字段,否则 DELETE 无法定位行。
示例(PostgreSQL):
WITH dupes AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email, name ORDER BY id ASC) AS rn FROM users ) DELETE FROM users WHERE id IN (SELECT id FROM dupes WHERE rn > 1);
-
dupesCTE 必须选id(或其他唯一列),否则后续DELETE没法关联到原表 - 不要写
DELETE FROM dupes WHERE rn > 1——CTE 是虚表,不能直接删 - SQL Server 支持更简洁的写法:
DELETE FROM dupes WHERE rn > 1,但前提是 CTE 定义中引用的是基表且未做聚合/去重
删前务必备份,且用 SELECT 验证要删的行
没有事务保障的生产环境,或者没开 AUTOCOMMIT=OFF 的客户端,一次 DELETE 就不可逆。哪怕语句看起来很安全,也要先跑一遍等价的 SELECT 看结果。
比如你准备执行:DELETE FROM users WHERE id IN (SELECT id FROM dupes WHERE rn > 1),那就先执行:SELECT * FROM users WHERE id IN (SELECT id FROM dupes WHERE rn > 1),人工核对 3–5 条是否确实是该删的重复项。
- 如果表很大,加
LIMIT 10(MySQL/PostgreSQL)或TOP 10(SQL Server)快速抽样 - 涉及多字段重复判断时,
SELECT结果里一定要包含所有PARTITION BY字段和排序字段,否则看不出分组逻辑是否符合预期 - 线上操作建议在低峰期进行,并设置
WHERE条件进一步缩小范围,例如加上AND created_at 避免误触新数据
真正难的不是写出能删的语句,而是说清楚“为什么这条该删、那条该留”。字段含义、业务规则、时间先后、ID 生成方式,这些都得在 ORDER BY 和 PARTITION BY 里体现出来。漏掉一个字段,或者把 ASC 写成 DESC,结果就反了。

















