必须用 row_number() 窗口函数为重复组内行编号并删除序号>1的行,因 group by 无法保留行级上下文;需严格定义 partition by 和 order by,并先验证再执行删除。

用 ROW_NUMBER() 标记重复行再过滤
直接删“多余数据”不能靠 GROUP BY 或聚合函数,因为它们不保留行级上下文。必须用窗口函数给每组内的行编号,再筛掉编号 > 1 的行。核心是:先用 ROW_NUMBER() 按分组字段和排序依据生成序号,再在外层或 CTE 中排除序号为 1 以外的行。
-
ROW_NUMBER()是唯一能稳定编号的窗口函数;RANK()和DENSE_RANK()在值相同时会并列,无法区分“哪一行该保留” - 排序字段(
ORDER BY子句)必须明确——比如按时间戳取最新、按 ID 取最小,否则结果不可控 - 分组字段(
PARTITION BY)要严格对应你定义的“重复组”,例如:PARTITION BY user_id, order_date - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 支持,但旧版不支持窗口函数
DELETE 语句里不能直接嵌套窗口函数?用 CTE 或子查询绕过
绝大多数数据库(如 PostgreSQL、SQL Server)不允许在 DELETE 的 WHERE 子句中直接写窗口函数。必须把带 ROW_NUMBER() 的结果先固化,再删。
- 推荐用 CTE:先
WITH ranked AS (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t),再DELETE FROM t USING ranked WHERE t.id = ranked.id AND ranked.rn > 1(PostgreSQL) - MySQL 8.0+ 可用 CTE +
JOIN:用DELETE t1 FROM table_name t1 JOIN (SELECT id, ROW_NUMBER() OVER (...) rn FROM table_name) t2 ON t1.id = t2.id WHERE t2.rn > 1 - SQL Server 支持
DELETE FROM (SELECT *, ROW_NUMBER() OVER (...) rn FROM t) t WHERE rn > 1—— 这种派生表方式更简洁 - 别用
SELECT * FROM ... WHERE rn > 1然后手动复制 ID 再删,数据量大时极易出错或漏删
误删风险高?先用 SELECT 验证再删
窗口函数逻辑一旦写错(比如 PARTITION BY 漏字段、ORDER BY 用错方向),就会多删或少删。必须先查出待删行,人工核对 3–5 组典型数据。
- 执行类似
SELECT id, col1, col2, ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY updated_at DESC) rn FROM t,观察rn > 1的行是否确实是你要剔除的“多余”数据 - 特别注意 NULL 值:不同数据库对
NULL在PARTITION BY或ORDER BY中的处理不一致,可能导致分组错误 - 如果业务要求“保留创建时间最早的那条”,
ORDER BY created_at ASC;若要“保留最新的”,则用DESC—— 方向反了就全删错了 - 生产环境务必加事务包裹:
BEGIN; DELETE ...; SELECT COUNT(*) FROM t; ROLLBACK;(验证完再COMMIT)
性能差?避免在大表上无索引运行
ROW_NUMBER() 需要对每个分组做排序,若分组字段无索引,或排序字段未覆盖索引,会导致全表扫描 + 大量临时排序,几十万行就可能卡住。
- 确保
PARTITION BY字段有索引,最好和ORDER BY字段组成联合索引,例如:INDEX idx_dup_check (col1, col2, updated_at) - 不要在
ORDER BY里用表达式或函数(如ORDER BY UPPER(name)),会跳过索引 - 单次删除太多行(如百万级)可能锁表太久,可分批删:加
LIMIT(MySQL/PostgreSQL)或TOP(SQL Server),循环执行 - 如果只是临时去重导出,考虑
CREATE TABLE AS SELECT DISTINCT ...或INSERT INTO ... SELECT DISTINCT更快且安全
实际操作中最容易被忽略的是 PARTITION BY 字段是否真正表达了“重复”的业务含义——比如邮箱去重,email 字段含空格或大小写差异时,不预处理就直接分组,会导致本应合并的行被拆开。










