row_number()需配合order by desc和外层where rn=1才能取最后一条;直接用max()或first_value()不可靠,mysql 8.0+推荐该方案,老版本需升级或用关联子查询。

用 ROW_NUMBER() 配合降序排序取最后一条
窗口函数本身不直接提供“取最后一条”的语义,必须靠排序+序号来定位。最稳妥的方式是用 ROW_NUMBER() 按业务时间或主键倒序编号,再过滤序号为 1 的行。
常见错误是误用 MAX(time) 或 LAST_VALUE():前者只能返回值,无法带回整行数据;后者依赖框架的 frame_clause,在 MySQL 或旧版 PostgreSQL 中不支持或行为不稳定。
- 确保排序字段(如
created_at、id)在分组内唯一,否则ROW_NUMBER()可能非确定性分配序号 - 若存在并列时间(如毫秒级相同),加一个次级排序字段(如
id DESC)保证结果稳定 - 示例:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC, id DESC) AS rn FROM orders ) t WHERE rn = 1;
FIRST_VALUE() 能不能直接拿最后一行?
不能。虽然名字叫 FIRST_VALUE(),但它只对当前窗口帧(frame)内按指定顺序排的第一条生效;默认 frame 是 UNBOUNDED PRECEDING TO CURRENT ROW,所以它拿不到“分组末尾”那条——除非你手动改 frame 到 UNBOUNDED PRECEDING TO UNBOUNDED FOLLOWING,再配合倒序排序。
- 写法冗长且易错:
FIRST_VALUE(*)不合法,必须逐字段写,比如FIRST_VALUE(id) OVER (...) - MySQL 8.0+ 支持完整 frame,但 PostgreSQL 默认 frame 就是全分组,仍需显式写
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - 性能上,
FIRST_VALUE()会为每行重复计算整个窗口,比ROW_NUMBER()+ 过滤更重
PostgreSQL 里用 DISTINCT ON 更简洁?
是的,但这是 PostgreSQL 特有语法,不是标准 SQL 窗口函数。如果你只跑 PG,且不需要兼容其他数据库,DISTINCT ON 更直观、通常也更快。
- 写法:
SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, updated_at DESC, id DESC;
- 注意:
ORDER BY必须以DISTINCT ON字段开头,后续字段决定“哪一条算 first” - 不能和普通聚合混用(比如同时
COUNT(*)),要聚合得套子查询 - 如果业务已用 ORM 或跨数据库部署,优先选
ROW_NUMBER()方案
MySQL 8.0 之前没窗口函数怎么办?
只能靠关联子查询或变量模拟,但风险高、难维护。强烈建议升级到 8.0+,否则容易踩坑。
- 变量方案(如
@row_number := IF(@prev=user_id, @row_number+1, 1))在复杂 JOIN 或优化器重排后结果不可靠 - 关联子查询性能差:
WHERE updated_at = (SELECT MAX(updated_at) FROM orders o2 WHERE o2.user_id = o1.user_id),遇到多条同时间记录会返回多行 - 如果必须兼容老版本,先用子查询去重时间戳(如取
MAX(id)),再关联回原表拿整行
实际用的时候,90% 场景直接走 ROW_NUMBER() + 倒序是最稳的选择。真正容易被忽略的是排序字段的确定性——尤其当业务用的是应用层生成的时间戳(可能有误差),或者分库分表后 id 不全局递增,这时候光靠 updated_at 排序就可能漏掉最新的一条。










