最稳删重复记录的方式是用ROW_NUMBER()窗口函数:先按重复字段分组并按ID升序编号,再删除序号>1的行;MySQL 8.0+、PostgreSQL、SQL Server、Oracle支持,但MySQL 5.7及以下不支持,需改用自连接方案。

直接用GROUP BY删不了重复行,得靠子查询或窗口函数
SQL标准里GROUP BY本身不支持DELETE操作——它只是聚合数据的工具,不是定位重复行的删除手段。想删重复记录,必须先识别哪些是“该删的”,再用DELETE配合子查询、ROW_NUMBER()或临时主键来实现。
用ROW_NUMBER()标记并删除重复行(推荐,兼容多数现代数据库)
核心思路:按判定重复的字段分组,给每组内的行编号,只保留ROW_NUMBER() = 1的那条,其余删掉。注意ORDER BY决定哪条被保留(比如按时间戳取最新/最旧)。
示例(PostgreSQL / SQL Server / MySQL 8.0+):
DELETE FROM users
WHERE id NOT IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY email, name ORDER BY created_at DESC
) AS rn
FROM users
) t
WHERE rn = 1
);
关键点:
-
PARTITION BY email, name定义“重复”的依据字段 -
ORDER BY created_at DESC确保最新记录被保留;换成ASC则保留最早的一条 - 外层
NOT IN删掉所有非首行——但注意:id不能为NULL,否则NOT IN会失效;更安全写法是用NOT EXISTS或CTE
MySQL 5.7 或无窗口函数时:用自连接或GROUP BY + MIN/MAX ID
老版本MySQL不支持ROW_NUMBER(),常见做法是先用GROUP BY找出每组要保留的MIN(id)或MAX(id),再删掉不在这个集合里的行。
示例(安全删除邮箱重复的记录,保留ID最小的):
DELETE u1 FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.name = u2.name AND u1.id > u2.id;
说明:
- 这种自连接方式避免了
GROUP BY后无法直接DELETE的问题 -
u1.id > u2.id保证只删“更大ID”的重复行,留下最小ID那条 - 务必在
email和name上建联合索引,否则性能极差 - 执行前先用
SELECT验证:SELECT u1.* FROM users u1 INNER JOIN users u2 ON ... AND u1.id > u2.id;
误删风险高,必须加事务和备份
这类操作本质是“根据业务逻辑判断哪条算‘重复’”,而GROUP BY本身不带删除语义——所以真正危险的不是语法,而是对“重复”定义的理解偏差。比如:email相同但phone不同,是否算重复?created_at相差1秒是否代表两条独立注册?
实操底线:
- 删之前一定
BEGIN TRANSACTION(或START TRANSACTION),确认结果后再COMMIT - 生产环境严禁直接跑
DELETE,先CREATE TABLE users_dedup AS SELECT ...导出清洗后数据做比对 -
WHERE条件里必须包含明确的重复判定字段,禁止只写GROUP BY email就动手删——这会导致逻辑错位
最常被跳过的一步:没确认NULL值如何参与分组。例如email IS NULL的记录在GROUP BY email中会被归为同一组,但多数业务里空邮箱不该和其他空邮箱去重。这点不手工过滤,很容易批量误删。











