ROW_NUMBER()配合PARTITION BY和ORDER BY DESC加子查询或CTE过滤rn=1是最通用方案;因窗口函数在SELECT阶段执行而WHERE早于SELECT,故rn在WHERE中不可见;ORDER BY需加确定性字段如id兜底防重复;无索引时性能骤降,须建PARTITION BY列与ORDER BY列的联合索引。

直接用 ROW_NUMBER() 配合 PARTITION BY 和 ORDER BY ... DESC,再套一层子查询或 CTE 过滤 rn = 1,是目前最通用、最可控的方案。 其他方法(如 DISTINCT、GROUP BY + 子查询、TOP 1)要么拿不到整行数据,要么只适用于单条全局最新,无法分组取最新。
为什么不能在 WHERE 里直接写 rn = 1
窗口函数是在 SELECT 阶段计算的,而 WHERE 执行早于 SELECT,所以 rn 在 WHERE 中根本不可见——会报错 Invalid column name 'rn'。必须先让窗口结果落地。
- 正确做法:用子查询包裹整个
SELECT *, ROW_NUMBER() OVER (...) AS rn,外层再WHERE rn = 1 - 或者用 CTE:
WITH cte AS (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t) SELECT * FROM cte WHERE rn = 1 - 别写成
SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t WHERE rn = 1——语法直接报错
ORDER BY 里只写时间字段可能漏数据
如果业务中存在多笔交易同秒甚至同毫秒发生(比如批量导入、高并发下单),仅靠 ORDER BY created_at DESC 会导致 ROW_NUMBER() 分配序号不确定,每次执行结果可能不一致,甚至把本该保留的记录筛掉。
- 必须补一个确定性字段兜底,常见组合:
ORDER BY created_at DESC, id DESC或ORDER BY created_at DESC, ctid(PostgreSQL) - SQL Server 可用
%%physloc%%,但生产环境慎用,它依赖物理存储位置,迁移或重建索引后可能变化 - MySQL 8.0+ 建议优先用主键
id,只要id是自增且严格递增,就能保证排序唯一
性能卡点:没索引时窗口函数很慢
ROW_NUMBER() 的 ORDER BY 字段若无索引,数据库大概率要对每组数据做内部排序,大表上可能触发磁盘临时表,耗时陡增。
- 关键索引建议:在
PARTITION BY列 +ORDER BY列上建联合索引,例如:CREATE INDEX idx_user_created ON orders(user_id, created_at DESC) - 注意:MySQL 8.0+ 支持降序索引,但老版本(如 5.7)会忽略
DESC,只当升序建;SQL Server 和 PostgreSQL 则原生支持 - 如果表有几十万行以上,又没加索引,这个查询很容易从毫秒级变成秒级甚至超时
删重复数据时,CTE 必须带别名才能 DELETE
想直接删旧记录留最新?用 CTE 写法时,很多开发者忘了给 CTE 起别名,导致 DELETE FROM cte 报错。
- 正确写法:
WITH cte AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn FROM orders) DELETE FROM cte WHERE rn > 1 - 错误写法:
WITH cte AS (...) DELETE FROM cte—— SQL Server 要求被删对象必须是基表或可更新视图,CTE 默认不可更新;加了别名后,引擎才允许把它当作临时可更新结果集 - MySQL 不支持对 CTE 执行 DELETE,得换用子查询 + JOIN 方式,或分两步:先查出要删的
id,再DELETE FROM ... WHERE id IN (...)
真正容易被忽略的是排序字段的“确定性”和索引的覆盖范围——不是只要写了 ORDER BY created_at DESC 就万事大吉,时间字段重复 + 没兜底字段 + 无索引,三者凑齐,这条语句就随时可能在生产环境翻车。

















