如何用 SQL 删除重复数据?
简化版
删除重复数据要先定义重复标准和保留规则,常用窗口函数 ROW_NUMBER() OVER (PARTITION BY 重复键 ORDER BY 保留规则) 标记重复行,再删除 rn > 1 的记录。
详细版
“重复”必须先讲清楚:是手机号重复、邮箱重复,还是 (user_id, product_id) 组合重复?还要定义保留哪条:最早创建、最新创建、id 最小,还是状态最完整。
常见写法是先查:
SELECT phone, COUNT(*)
FROM users
GROUP BY phone
HAVING COUNT(*) > 1;
再用窗口函数标记:
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY phone
ORDER BY created_at DESC, id DESC
) AS rn
FROM users;
最后删除 rn > 1。真实生产环境要先备份、先 SELECT 验证影响行、分批删除,并补唯一约束防止重复再次产生。
完整版教学
一、删除前先定义什么叫重复
重复不是数据库自动知道的概念,而是业务规则。两条用户记录名字相同未必重复,手机号相同可能重复,手机号和租户 id 同时相同才可能重复。
例如 SaaS 系统中,同一个手机号在不同租户下可能允许存在:
tenant_id | phone
1 | 13800000000
2 | 13800000000
如果只按 phone 去重,就可能误删租户 2 的合法用户。正确重复键也许是 (tenant_id, phone)。
记忆钩子:删除重复数据前,先问“按什么字段算重复,保留哪一条”。
二、先查重复,不要直接删
第一步应该是统计重复键和重复数量。
SELECT phone, COUNT(*) AS cnt
FROM users
WHERE phone IS NOT NULL
GROUP BY phone
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 20;
如果结果是:
phone | cnt
13800000000 | 5
13900000000 | 3
说明至少有 2 组重复。这里过滤 phone IS NOT NULL 是因为空手机号是否算重复要按业务判断,很多场景下多个 NULL 不应该互相删除。
这一步还能估算影响范围,避免一上来执行大范围 DELETE。
三、用 ROW_NUMBER 标记保留行
窗口函数可以为每个重复组编号。编号规则就是保留规则。
SELECT id, phone, created_at,
ROW_NUMBER() OVER (
PARTITION BY phone
ORDER BY created_at DESC, id DESC
) AS rn
FROM users
WHERE phone IS NOT NULL;
含义是:同一个手机号一组,最新创建且 id 最大的记录排第 1,其余排第 2、3、4。
示例:
id | phone | created_at | rn
8 | 13800000000 | 2026-07-29 | 1
5 | 13800000000 | 2026-07-20 | 2
2 | 13800000000 | 2026-07-01 | 3
删除时保留 rn=1,删除 rn>1。
四、删除语句要按数据库方言调整
不同数据库删除 CTE 或派生表的语法略有差异,但思路一致:先得到要删的主键,再按主键删除。
通用思路:
WITH duplicated AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY phone
ORDER BY created_at DESC, id DESC
) AS rn
FROM users
WHERE phone IS NOT NULL
)
DELETE FROM users
WHERE id IN (
SELECT id FROM duplicated WHERE rn > 1
);
如果目标数据库不支持这种写法,可以先把要删的 id 写入临时表,再执行删除。关键是删除条件使用主键,避免误删整组。
五、生产环境要有安全步骤
删除数据是高风险操作,面试中说出安全流程会很加分。
1. 明确重复键和保留规则
2. SELECT 查重复数量
3. SELECT 查将被删除的 id
4. 备份或落临时表
5. 小批量 DELETE
6. 校验重复是否清理完成
7. 增加唯一约束或唯一索引
例如先检查将删除多少行:
WITH marked AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY phone
ORDER BY created_at DESC, id DESC
) AS rn
FROM users
WHERE phone IS NOT NULL
)
SELECT COUNT(*)
FROM marked
WHERE rn > 1;
如果预期删除 100 行,结果显示 100000 行,就应该立即停下来复查条件。
六、清理后要防止再次重复
只删历史重复不够,还要让重复不再产生。常见做法是加唯一约束或唯一索引。
CREATE UNIQUE INDEX uk_users_phone
ON users(phone);
如果是多租户唯一:
CREATE UNIQUE INDEX uk_users_tenant_phone
ON users(tenant_id, phone);
若业务允许 NULL 多次出现,要确认数据库对唯一索引和 NULL 的处理规则。应用层也要处理并发写入,不能只依赖“先查再插”。
七、常见误区与追问
- 误区:重复数据直接按某个字段
DELETE就行。 必须先定义重复键和保留规则,否则很容易误删。 - 误区:
DISTINCT可以删除表里的重复行。DISTINCT只影响查询结果,不会修改原表。 - 误区:所有
NULL都应该算重复删除。 空值是否重复取决于业务,不能默认处理。 - 误区:清完历史数据就结束了。 不加唯一约束或写入控制,重复还会再次出现。
- 追问:保留最新一条怎么写? 用
ROW_NUMBER按created_at DESC, id DESC排序,删除rn > 1。 - 追问:为什么删除时用主键? 主键能精准定位要删的行,避免条件扩大导致整组被删。
八、加强记忆
删除重复数据按四步走:定义重复键、定义保留规则、窗口函数标记、按主键删除。上线前再补两道保险:先查影响范围,清理后加唯一约束。真正成熟的答案不是会写 DELETE,而是知道如何不误删。