在业务数据库的日常运转中,表中出现重复记录是十分常见的数据质量问题。重复可能来自程序bug、多次导入、缺乏唯一约束等。要解决它,核心目标只有两个:一是把重复数据查出来,也就是过滤;二是把多余的副本删掉,也就是删除。下面我们以几种主流数据库为例,详细拆解具体操作。

一、什么是表中的重复记录
通常所说的重复记录,是指表中若干行在业务意义上完全一样,或者在某些关键列(如用户名、邮箱、订单号)上取值相同,而数据库因为没有唯一索引允许它们共存。例如下面这张用户表,name和email都相同,但id不同,这就是典型的重复:
CREATE TABLE user_info ( id INT PRIMARY KEY, name VARCHAR(50), email VARCHAR(100) ); INSERT INTO user_info VALUES (1, '张三', 'zhangsan@ipipp.com'), (2, '张三', 'zhangsan@ipipp.com'), (3, '李四', 'lisi@ipipp.com'), (4, '李四', 'lisi@ipipp.com');
上面的数据中,id为1和2的行是重复,id为3和4的行也是重复。我们需要保留其中一行,删除另一行。如果不过滤和清理,后续做COUNT统计或关联查询时就会出现数据翻倍。
需要注意的是,重复记录的判定维度由业务决定。有时整行一致才算重复,有时仅某几列一致就算重复。因此在写SQL前,必须先明确重复的定义,否则容易误删有效数据。
二、使用GROUP BY过滤重复记录
过滤重复最直观的办法是使用GROUP BY配合聚合函数,把重复键聚合起来,只展示每组的一条。以下语句可以查出每个name和email组合中最小的id,也就是我们想保留的那一行:
SELECT MIN(id) AS keep_id, name, email FROM user_info GROUP BY name, email;
这条语句不会改动表,只是把重复组压缩成一行,常用于先确认重复范围。如果你想看到哪些id是多余的,可以用NOT IN反向查询:
SELECT * FROM user_info WHERE id NOT IN ( SELECT MIN(id) FROM user_info GROUP BY name, email );
上面这个结果集就是所有应该被删除的重复副本。GROUP BY方式简单易懂,适合数据量不大、重复逻辑清晰的场景。但它的局限在于只能保留聚合出来的那一行,无法在删除时灵活控制保留哪一行(比如保留时间最新的)。
三、在MySQL中删除重复记录
MySQL不支持直接在子查询里DELETE同一张表,因此常用DELETE JOIN来绕过限制。下面语句会删除掉每组重复中id不是最小的那一行:
DELETE u1 FROM user_info u1 JOIN user_info u2 ON u1.name = u2.name AND u1.email = u2.email AND u1.id > u2.id;
逻辑上,u2取每组中id最小的那条,u1是其余id更大的副本,通过JOIN把副本找出来并删除。执行后表中每个name和email只留最小id的一条。
这种写法效率较高,因为利用了等值连接。但如果表很大,建议先给name和email加联合索引,否则JOIN会做全表扫描。另外,操作前务必用前面的SELECT语句备份或确认待删数据,防止误删。
四、使用窗口函数删除重复记录
PostgreSQL、SQL Server、Oracle等支持窗口函数,可以用ROW_NUMBER给同组记录编号,再删掉编号大于1的。示例如下:
DELETE FROM user_info
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY name, email
ORDER BY id
) AS rn
FROM user_info
) t
WHERE rn > 1
);
内层查询按name和email分区,同组内按id排序,rn=1是保留行,rn>1是重复副本。外层DELETE把这些副本删掉。窗口函数比GROUP BY灵活,ORDER BY可以换成创建时间等字段,从而保留最新的一条。
这种写法逻辑清晰、可控性强,适合复杂重复规则。但要注意,部分旧版本MySQL不支持窗口函数,此时仍需用JOIN方案。无论哪种方式,删除前都应在事务里先SELECT验证。
五、防止重复记录再次产生
删除只是补救,根本解决是加约束。可以在表上建唯一索引,让数据库拒绝重复写入:
ALTER TABLE user_info ADD CONSTRAINT uk_name_email UNIQUE (name, email);
有了唯一索引后,再次插入相同name和email会报错,从源头阻断重复。如果历史数据已存在重复,需先清理再建索引,否则建索引会失败。
此外,在应用层写入前做查询校验、使用UPSERT语法(如MySQL的INSERT ... ON DUPLICATE KEY UPDATE)也是常用手段。把数据库约束和应用逻辑结合起来,才能长期保持数据干净。
六、总结对比
不同方案各有适用面,可用下表快速对照:
| 方案 | 适用库 | 优点 | 缺点 |
|---|---|---|---|
| GROUP BY过滤 | 所有SQL库 | 简单直观 | 仅查不删,删需配合子查询 |
| DELETE JOIN | MySQL | 删除效率高 | 语法不通用 |
| 窗口函数 | PG、SQL Server等 | 灵活保留指定行 | 旧MySQL不支持 |
实际处理时,先明确重复定义,再选过滤与删除语句,最后补上唯一约束,才能稳妥解决重复记录问题。