last_value默认只返回当前行值,因其隐式帧为rows between unbounded preceding and current row;要取分组末值须显式指定unbounded following,但更推荐用first_value配合desc排序获取最新值,或用row_number()取完整记录。

LAST_VALUE 默认窗口帧就是 CURRENT ROW
因为 SQL 标准规定,LAST_VALUE() 在没显式指定 ROWS BETWEEN 时,隐式使用 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。这个范围从分区开头一直延伸到当前行(含),所以“最后”永远是当前行自己——它根本看不到后面还没处理的行。
这不是某家数据库的 bug,PostgreSQL、SQL Server、MySQL 8.0+、BigQuery 全部一致。你写 LAST_VALUE(status) OVER (PARTITION BY user_id ORDER BY updated_at DESC),结果每行都等于本行 status,不是数据问题,是帧定义没改。
想取分组真正末值,必须显式写 UNBOUNDED FOLLOWING
要让 LAST_VALUE() 看到整组所有行,得手动补全窗口定义:
LAST_VALUE(col) OVER ( PARTITION BY g ORDER BY t ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING )
只加 PARTITION BY 或只改 ORDER BY 不够,缺了 ROWS BETWEEN 这句,就还是当前行。
-
ORDER BY方向决定语义:升序时最大时间才是“最后”,降序时最小时间反而是“最后”——别写反 - 旧版
MySQL(如 5.7)不支持UNBOUNDED FOLLOWING,该写法会报错或被静默忽略 -
SQLite完全不支持窗口帧子句,别在旧环境硬套
FIRST_VALUE + DESC 比 LAST_VALUE 更少出错
多数人真正想要的是“最新一条”,即“按时间倒序排的第一条”。FIRST_VALUE(col) OVER (PARTITION BY g ORDER BY updated_at DESC) 天然匹配这个语义,且默认帧就覆盖整组,不用额外写 ROWS 子句。
漏写帧定义的概率低,语义也更直白:“最新 = 时间最大 = 降序排第一”。
- 排序字段重复时,务必加二级排序,例如:
ORDER BY updated_at DESC, id DESC -
FIRST_VALUE()仍是单列函数,不能直接取整行;多个FIRST_VALUE(x)、FIRST_VALUE(y)可能来自不同物理行 - 若需完整记录,仍应走
ROW_NUMBER()路径
真正要取最新完整记录,别硬套 LAST_VALUE
LAST_VALUE() 是逐列计算的,不是行级操作。你写十个 LAST_VALUE(id)、LAST_VALUE(amount)、LAST_VALUE(status),它们可能来自不同原始记录——尤其当 ORDER BY 字段不唯一时,字段错位风险极高。
正确路径是:
ROW_NUMBER() OVER (PARTITION BY g ORDER BY updated_at DESC, id DESC) AS rn
再外层 WHERE rn = 1。
复合索引 (g, updated_at, id) 能显著提升性能;过滤条件(如 status = 'done')必须放外层或 CTE,不能写在窗口内。
最常被忽略的一点:即使你把 ROWS BETWEEN 写对了,只要 ORDER BY 缺乏确定性(比如没加二级排序),结果就不可复现——而这种错误在小数据集上根本暴露不出来。










