典型写法是用row_number()按分组字段和时间字段降序排序后取rn=1;必须order by created_at desc(或id desc防并发),不可省略;需子查询或cte,因窗口函数不能直接用于where;相比max+关联,它避免漏数据且语义明确。

用 ROW_NUMBER() 取每组最新一条记录的典型写法
核心就是按分组字段排序后标序号,再筛出序号为 1 的行。关键在 ORDER BY 子句里必须包含能定义“最后一次”的字段,比如 created_at 或 id(假设自增)。
常见错误是只按分组字段 PARTITION BY user_id,却忘了 ORDER BY created_at DESC —— 这会导致序号分配无意义,取出来的不一定是最新那条。
- 必须用
DESC降序,否则ROW_NUMBER() = 1拿到的是最早一条 - 如果时间精度到秒,且存在并发写入,建议补上
id DESC做二级排序,避免并列第一时结果不稳定 -
ROW_NUMBER()是窗口函数,不能直接出现在WHERE中,得套一层子查询或 CTE
SELECT user_id, amount, created_at
FROM (
SELECT user_id, amount, created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM transactions
) t
WHERE rn = 1;
为什么不用 MAX(created_at) 关联查?
看似直观:先算每组最大时间,再关联原表取整行。但实际容易漏数据——当多个交易在同一秒发生,MAX(created_at) 能取到,但关联时可能只命中其中一条(取决于连接条件和索引),尤其没加 id 去重时。
ROW_NUMBER() 天然支持“取任意一条”语义,只要排序规则明确,结果就确定;而聚合 + 关联本质是两步操作,中间丢失了原始行上下文。
- 聚合方案在 MySQL 5.7 或旧版 PostgreSQL 中更常见,但逻辑绕、易错
- 如果只要最新时间值(不要整行),用
MAX()更轻量;但要整行信息,窗口函数更直接可靠 - 某些数据库(如 SQLite)不支持窗口函数,这时才被迫用关联或自连接
ROW_NUMBER() 和 RANK() 在“最后一次”场景的区别
当同组内多条记录的排序字段完全相同时(比如同一秒创建的多笔交易),ROW_NUMBER() 仍会强制给唯一序号(1,2,3…),而 RANK() 会给并列者相同排名(1,1,3…)。这意味着:
- 用
RANK()+WHERE rank = 1可能返回多行,业务上是否允许?得看需求 - 用
ROW_NUMBER()则保证每组只出一行,但哪一行是“第一”取决于二级排序字段(如id DESC) - 如果业务要求“只要时间最新,不管几条都得返回”,该用
RANK();如果要求“只取一条确定的”,必须用ROW_NUMBER()并补全排序字段
性能和索引怎么配才不慢?
ROW_NUMBER() 窗口计算本身开销不大,瓶颈通常在排序阶段。如果没有合适索引,数据库要对整个分区做内存或磁盘排序。
- 理想索引:复合索引
(user_id, created_at DESC, id DESC)—— 覆盖分区、排序、去重三要素 - 如果只建
(user_id, created_at),当有时间重复时,仍可能触发额外排序 - 大表慎用
PARTITION BY字段基数过低的情况(比如只有 3 个状态值),会导致单个分区过大,排序压力集中 - PostgreSQL 14+、MySQL 8.0+ 对窗口函数做了较多优化,但老版本建议先
EXPLAIN看执行计划里有没有WindowAgg或Using filesort
updated_at 但更新逻辑不严谨,或前端传的时间没做时区归一化,这时候窗口函数再准也没用。










