LAST_VALUE() 默认窗口帧为 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,故结果恒为当前行值;要获取分组真正末值,须显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。

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(UNBOUNDED FOLLOWING,该写法会报错或被静默忽略
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)可能来自不同物理行
真正要取最新完整记录,别硬套 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,不能塞进窗口定义里
复杂点在于:窗口函数的行为不只取决于 ORDER BY,还强依赖帧定义;而“最后一条记录”这个业务目标,常需要排序确定性、去重兜底、以及行级筛选三者配合——不能只靠一个函数名就以为搞定了。

















