ROW_NUMBER()必须配合PARTITION BY和ORDER BY才能分组取最新记录,单独使用无效;需嵌套子查询或CTE筛选rn=1,且排序字段应建联合索引以保障性能。

ROW_NUMBER() 配合 PARTITION BY 和 ORDER BY 才能分组取最新
单独写 ROW_NUMBER() 没用,必须搭配 PARTITION BY 划分组、ORDER BY 定义“最新”的排序逻辑。常见错误是只写 ORDER BY created_at DESC 却漏掉 PARTITION BY group_id,结果整表排一遍序,不是每组内排序。
关键点:
-
PARTITION BY字段必须是你想“每组”的依据,比如user_id、product_id,不能写错或漏写 -
ORDER BY要按时间戳或版本号降序(DESC),否则ROW_NUMBER() = 1拿到的是最旧记录 - 如果时间字段有重复(比如同秒插入多条),建议加二级排序,例如
ORDER BY updated_at DESC, id DESC避免结果不稳定
WHERE rn = 1 必须套在子查询或 CTE 里
ROW_NUMBER() 是窗口函数,不能直接在 WHERE 中引用别名 rn——SQL 执行顺序决定 WHERE 在窗口函数计算前就执行了。所以必须先算出 rn,再过滤。
两种可靠写法:
- 用子查询:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) AS rn FROM orders) t WHERE t.rn = 1
- 用 CTE(更清晰):
WITH ranked AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at DESC) AS rn FROM products) SELECT * FROM ranked WHERE rn = 1
注意 NULL 值和时区对“最新”的干扰
如果排序字段(如 updated_at)含 NULL,默认会被排在最前面(PostgreSQL/SQL Server)或最后(MySQL 8.0+),导致 ROW_NUMBER() = 1 拿到的是 NULL 记录而非真实最新数据。
稳妥做法:
- 显式控制
NULL位置:ORDER BY updated_at DESC NULLS LAST(PostgreSQL/Oracle 支持) - MySQL 不支持
NULLS LAST,改用:ORDER BY updated_at IS NULL, updated_at DESC - 确认时间字段是 UTC 还是本地时区;跨时区系统中,用
created_at AT TIME ZONE 'UTC'统一基准再排序
性能差?检查是否命中索引
ROW_NUMBER() 分组排序很吃资源,尤其大表。没索引时可能全表扫描 + 文件排序。
索引要覆盖三个要素:
- 分区字段(
PARTITION BY列)做前导列 - 排序字段(
ORDER BY列)紧跟其后 - 如果 SELECT 中还查了其他字段,考虑包含索引(
INCLUDE)避免回表
例如:对 PARTITION BY user_id ORDER BY updated_at DESC,建索引:
CREATE INDEX idx_user_updated ON orders (user_id, updated_at DESC)。MySQL 8.0+ 支持降序索引,老版本只能建
(user_id, updated_at) 然后靠优化器处理。
实际跑之前,先 EXPLAIN 看执行计划里有没有 Using filesort 或 WindowAgg 走索引。

















