← 返回题目列表

如何用 SQL 删除重复数据?

高频 中等 第 6 / 28 题 更新于 2026/07/29
SQL去重DELETE窗口函数

简化版

删除重复数据要先定义重复标准和保留规则,常用窗口函数 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_NUMBERcreated_at DESC, id DESC 排序,删除 rn > 1
  • 追问:为什么删除时用主键? 主键能精准定位要删的行,避免条件扩大导致整组被删。

八、加强记忆

删除重复数据按四步走:定义重复键、定义保留规则、窗口函数标记、按主键删除。上线前再补两道保险:先查影响范围,清理后加唯一约束。真正成熟的答案不是会写 DELETE,而是知道如何不误删。