ROW_NUMBER()是去重核心方法:先用PARTITION BY分组、ORDER BY排序生成序号,再删rn>1的行;MySQL 8.0+需CTE中转,老版本用自连接;务必先SELECT验证逻辑并确保有唯一标识列。

用 ROW_NUMBER() 给每组数据编号再删
SQL 本身没有直接“按分组去重”的 DELETE 语法,必须借助窗口函数生成序号,再筛掉重复组里的非首行。核心思路是:对每个分组(比如 user_id、email)按某字段排序(如 created_at 或 id),用 ROW_NUMBER() 标出第 1、2、3… 行,然后只保留序号为 1 的记录。
常见错误是直接写 DELETE FROM t GROUP BY x —— 这在标准 SQL 和主流数据库(MySQL 8.0+、PostgreSQL、SQL Server、Oracle)里都报错,语法不合法。
实操建议:
- 确保目标表有唯一标识列(如
id),否则无法安全区分“哪条该留” -
ORDER BY子句必须明确:你想保留最新的一条?最小id?还是其他业务优先级? - 先用
SELECT验证逻辑,例如:SELECT id, user_id, email, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) AS rn FROM users;
确认rn = 1确实是你想保留的那条 - 真正删除时,多数数据库要求用子查询或 CTE 包裹,不能直接在
DELETE中调用窗口函数(MySQL 8.0+ 支持 CTE,PostgreSQL 和 SQL Server 同样推荐用 CTE)
MySQL 8.0+ 删除示例(CTE + ROW_NUMBER)
MySQL 8.0 开始支持 CTE 和窗口函数,这是最清晰的做法。注意:不能在 DELETE 的 FROM 子句中直接引用同一个表的别名(会报 ERROR 1093),所以必须用 CTE 中转。
假设要按 email 去重,保留 id 最小的记录:
WITH ranked AS ( SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) DELETE u FROM users u INNER JOIN ranked r ON u.id = r.id WHERE r.rn > 1;
关键点:
-
PARTITION BY email定义分组依据 -
ORDER BY id决定谁是“第一”——这里最小id得rn = 1 - 必须用
JOIN关联原表和 CTE,不能写成DELETE FROM ranked WHERE rn > 1(CTE 不是真实表)
PostgreSQL / SQL Server 兼容写法(无需 CTE 中转)
PostgreSQL 和 SQL Server 允许在 DELETE 中直接使用子查询或 USING(PostgreSQL)/ FROM(SQL Server)语法关联窗口结果,但仍有细节差异。
PostgreSQL 示例(用 USING):
DELETE FROM users
USING (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
) ranked
WHERE users.id = ranked.id AND ranked.rn > 1;
SQL Server 示例(用 FROM 扩展语法):
DELETE u FROM users u INNER JOIN ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) ranked ON u.id = ranked.id WHERE ranked.rn > 1;
注意:
- SQL Server 的
DELETE ... FROM是扩展语法,不是标准 SQL,但稳定可用 - PostgreSQL 的
USING必须显式写出WHERE关联条件,漏掉会导致全表删除 - 所有方案都依赖主键或唯一列(
id)做精确匹配;如果只有业务字段(如只有email和name),且完全重复,需改用DISTINCT ON(PostgreSQL)或临时表导出再重建
没窗口函数的老版本 MySQL(5.7 及更早)怎么办?
MySQL 5.7 不支持 ROW_NUMBER(),也没 CTE,只能靠自连接或变量模拟序号,风险高、性能差,仅作兜底。
典型自连接去重(按 email 留最小 id):
DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id;
说明:
- 这句意思是:“删掉所有存在另一个同
email且id更小的记录的行” - 优点:兼容性极好,5.0 起就支持
- 缺点:没有索引时可能锁表、慢;若
email列空值多,NULL = NULL不成立,会导致漏删 - 务必在
email上建索引,否则执行可能卡死 - 无法灵活控制“留最新”还是“留最早”,因为比较的是
id大小;如果想留最新时间,得确保时间字段能映射到有序整型(如 UNIX 时间戳),否则得先加辅助列
真正难的不是写哪条语句,而是想清楚:重复的定义是什么?保留的依据是否覆盖所有边界情况?比如 email 为空、大小写混用、前后空格、时区导致的时间精度差异——这些都不会被 PARTITION BY 自动归一化,得前置清洗。

















