last_value()默认窗口帧是rows between unbounded preceding and current row,因此结果恒为当前行值;需显式指定rows between unbounded preceding and unbounded following并配合确定性order by才能取真正末行值。

因为默认窗口帧是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,它只包含当前行及之前所有行——所以“最后”永远是当前行自己。
LAST_VALUE() 默认窗口帧是什么
几乎所有主流 SQL 引擎(PostgreSQL、MySQL 8.0+、SQL Server、Oracle)对 LAST_VALUE() 的默认窗口范围都是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 或等效的 ROWS 版本。这不是 bug,是 SQL 标准定义。
- 这个范围意味着:每处理一行,窗口就从分区开头“滑动”到当前行,
LAST_VALUE()只在这个滑动段里找“最后一个”,自然就是当前行的值 - 即使加了
PARTITION BY和ORDER BY,只要没显式改帧,结果仍是逐行截断计算 - 现象上表现为:整列
LAST_VALUE(col)和原始col完全一致,像没生效
怎么写才能取到真正最后一行的值
必须显式指定覆盖整组的窗口帧,并确保 ORDER BY 顺序与业务语义匹配。
- 核心补全:在
OVER子句中加上ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - 排序方向很重要:
ORDER BY created_at(升序)时,LAST_VALUE()对应最大时间;若用ORDER BY created_at DESC,则对应最小时间——容易反直觉,慎选 - 重复值风险:如果
created_at有大量重复,数据库可能任意决定行序,导致“最后一行”不稳定;务必加二级排序,例如ORDER BY created_at, id - 示例正确写法:
LAST_VALUE(amount) OVER ( PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING )
为什么 FIRST_VALUE() + 降序更常用也更安全
多数人要的其实是“最新一条”,本质是“按时间倒排后的第一条”。用 FIRST_VALUE() 配合 ORDER BY ... DESC 能绕过帧陷阱,语义更直接。
-
FIRST_VALUE()默认帧虽也是CURRENT ROW,但降序后,“第一个”天然对应时间最晚的那条,无需手动扩帧 - 漏写
ROWS子句也不影响结果,出错概率低 - 兼容性更好:某些旧版引擎(如 MySQL 5.7、SQLite)不支持窗口帧子句,但基本都支持
FIRST_VALUE()+ORDER BY DESC - 注意空值:若
created_at可为 NULL,建议显式写ORDER BY created_at DESC NULLS LAST(PostgreSQL)或用COALESCE(created_at, '1970-01-01')(MySQL)
想取整行最新记录,别硬套 LAST_VALUE()
LAST_VALUE() 是单字段函数。你写十个 LAST_VALUE() 去取不同列,它们可能来自不同物理行——尤其当排序键不唯一时,id、status、amount 会错位。
- 真正要拿完整最新行,请用
ROW_NUMBER() OVER (PARTITION BY x ORDER BY ts DESC, id DESC)然后WHERE rn = 1 - 该方案原子性强、各字段来源确定、跨引擎兼容好
- 如果只是补空值(比如向下填充),才考虑
LAST_VALUE(col IGNORE NULLS) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),这时反而要保留默认帧
最容易被忽略的一点:窗口函数的结果稳定性完全依赖 ORDER BY 的确定性。哪怕只少一个 id 作为二级排序,大表分页或多次执行时,LAST_VALUE() 就可能指向任意一条同时间戳的记录——这不是数据问题,是排序未定义导致的必然行为。










