ROW_NUMBER()仅编号不删数据,真正去重需用PARTITION BY定义重复组、ORDER BY确保排序稳定、WHERE rn=1筛选保留行,且必须配合子查询或CTE,禁用RANK()等并列函数。

ROW_NUMBER() 本身不删数据,只编号;真正去重必须用它标记“哪行该留”,再配合 WHERE rn = 1 或 DELETE 筛掉其余行。
为什么 PARTITION BY 漏不得
PARTITION BY 定义“哪些行算一组重复”,是去重逻辑的业务基础。漏掉它,ROW_NUMBER() 就对整张表从 1 开始连续编号,结果只剩第一行——这不是去重,是随机截断。
-
PARTITION BY user_id, order_no:表示同一用户、同一单号的多条记录才视为重复 - 若只写
PARTITION BY user_id,但实际重复由user_id + product_id共同决定,就会漏判重复组 - 大数据量下,
PARTITION BY字段没索引 → 排序开销陡增,建议建复合索引如(user_id, order_no, updated_at)
ORDER BY 怎么写才稳定不翻车
ORDER BY 决定每组内谁拿 rn = 1,一旦不稳定,同一条语句多次执行可能保留不同行——线上环境这是事故前兆。
- 必须包含至少一个唯一字段兜底,例如
ORDER BY updated_at DESC, id DESC,避免时间相同时结果不可控 - NULL 值默认排序行为因数据库而异(PostgreSQL 默认
NULLS FIRST,MySQL 8.0+ 支持NULLS LAST,老版本需用COALESCE(updated_at, '1970-01-01')) - 字符串时间字段(如
'2024-01-01'和'2024/01/01'混存)不能直接ORDER BY,要先CAST(event_time AS TIMESTAMP),否则排序错乱
WHERE rn = 1 必须套在子查询或 CTE 里
你不能在 SELECT 主句里直接写 WHERE ROW_NUMBER() OVER (...) = 1,语法不支持,数据库会报错 ERROR 1064 或类似提示。
- 正确姿势是用 CTE 或子查询生成带
rn列的结果集,外层再WHERE rn = 1 - 删除场景下,MySQL 8.0+ 可用 CTE:
WITH ranked AS (SELECT id, ROW_NUMBER() OVER (...) AS rn FROM t) DELETE FROM t WHERE id IN (SELECT id FROM ranked WHERE rn > 1) - MySQL 5.7 及更早不支持 CTE 中 DELETE,得改用临时表或自连接,且务必先跑
SELECT COUNT(*) FROM ranked WHERE rn > 1确认影响行数
别用 RANK() 或 DENSE_RANK() 替代 ROW_NUMBER()
只要目标是“每组严格只留一条”,RANK() 和 DENSE_RANK() 都会失败——它们遇到 ORDER BY 值相同时并列赋值,导致多个 rn = 1。
-
ROW_NUMBER()强制唯一编号:哪怕updated_at和id全一样,也会按数据库内部顺序分出 1、2、3… -
RANK()同值同名次、跳号(1,1,3),DENSE_RANK()同值同名次、不跳号(1,1,2),两者都可能让WHERE rn = 1留下多行 - 如果误用,查出来的“去重结果”行数反而比原表还多,问题更隐蔽
最易被忽略的是排序字段的隐式依赖:你以为 ORDER BY created_at DESC 就够了,但积压数据里常有 NULL、字符串混排、时区未归一等问题——这些不会报错,但会让 rn = 1 每次指向不同行。上线前必须用真实脏数据验证一次排序结果是否可重现。

















