row_number()通过partition by分组、order by排序为每组行分配1、2、3…序号,序号>1的即冗余行;必须用cte或子查询包裹后关联删除,不可直接在where中使用,且需注意null处理、索引优化与事务安全。

ROW_NUMBER() 怎么给重复行打序号?
关键不是直接删,而是先用 ROW_NUMBER() 标出哪些是“该留的”、哪些是“该删的”。它按指定字段分组(PARTITION BY),再在每组内按某列排序(ORDER BY),给每行分配 1、2、3… 这样的序号。重复数据会被分到同一组,序号 >1 的就是冗余行。
常见错误是 ORDER BY 选错列:比如用时间戳排序能保留最新一条,用主键排序可能留最老的;如果排序列有 NULL,不同数据库行为不一致(PostgreSQL 允许 ORDER BY col NULLS LAST,MySQL 8.0+ 才支持类似写法)。
- 必须搭配
PARTITION BY—— 否则整表只有一组,所有行序号都是 1 -
ORDER BY列建议选有业务意义的字段(如created_at或id),避免用无序字段如updated_at(可能全相同) - 不能在 DELETE 语句里直接嵌套
ROW_NUMBER()(MySQL 5.7 及更早版本会报错 “This is not allowed in stored function or trigger”)
DELETE 时怎么安全引用 ROW_NUMBER() 结果?
不能写 DELETE FROM t WHERE ROW_NUMBER() OVER (...) > 1 —— SQL 标准不允许在 WHERE 中用窗口函数。得把带序号的结果当临时表用,再关联删除。
推荐写法是用 CTE(Common Table Expression)或子查询包裹 ROW_NUMBER(),再对序号 >1 的行执行 DELETE。注意:CTE 在 PostgreSQL/SQL Server/MySQL 8.0+ 中可用,但 SQLite 和旧版 MySQL 不支持。
- MySQL 8.0+ 示例:
WITH ranked AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) rn FROM users ) DELETE u FROM users u INNER JOIN ranked r ON u.id = r.id WHERE r.rn > 1;
- PostgreSQL 写法更简洁:
DELETE FROM users WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) rn FROM users ) t WHERE rn > 1 ); - 务必提前备份或在事务中执行:
BEGIN; ... ROLLBACK;,尤其当表无主键或PARTITION BY字段有大量 NULL 时,容易误删
为什么不能只靠 GROUP BY + MIN/MAX ID 删除?
用 GROUP BY 找最小/最大 ID 再删其余行,看似简单,但实际漏掉两类情况:一是重复行所有字段完全一致(包括主键以外的字段),此时 GROUP BY 没法区分哪条该留;二是想保留“最新”而非“最早”的记录,MIN(id) 未必对应最新时间 —— ID 和时间可能不一致。
-
ROW_NUMBER()能精确控制保留逻辑(比如ORDER BY updated_at DESC留最新) - 当重复依据是多列(如
PARTITION BY name, phone, address)时,GROUP BY语句变长易出错,而ROW_NUMBER()语法不变 - 某些场景下,
GROUP BY删除需要两次扫描(一次找基准 ID,一次删),而 CTE +ROW_NUMBER()通常只需一次排序
性能和索引怎么配合?
ROW_NUMBER() 的开销主要在排序 —— 如果 PARTITION BY 和 ORDER BY 字段没索引,大表上会很慢,甚至触发磁盘临时表。
- 最优索引形如:
CREATE INDEX idx_dup_check ON users (email, created_at DESC);(把PARTITION BY列放前面,ORDER BY列放后面) - 如果重复率极高(比如 90% 行都重复),先用
SELECT COUNT(*)和COUNT(DISTINCT ...)估算比例,考虑是否值得删 —— 有时重建表更快 - 在从库或低峰期执行,避免锁表时间过长;InnoDB 行锁一般够用,但
DELETE ... JOIN在 MySQL 中可能升级为间隙锁
真正麻烦的是 PARTITION BY 字段存在大量 NULL —— 多数数据库把 NULL 视为相同值,导致本不该归一组的行被强行合并。遇到这种情况,得先用 COALESCE(email, CONCAT('null_', id)) 之类方式“隔离” NULL 值,否则删得不对。











