LAST_VALUE 与默认帧的坑
LAST_VALUE 用来取窗口内的最后一行,但默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW——窗口右边界就是当前行,所以它返回的往往是当前行自己的值,看起来像「函数没生效」。要取整个分区的最后一行,必须显式把右边界扩展到分区末尾:ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。这个行为在 MySQL 8 与 PostgreSQL 上完全一致,是窗口函数最常被误解的地方。
基本信息
| 适用平台 | MySQL 8.0+ / PostgreSQL 11+ |
|---|---|
| 默认帧 | 带 ORDER BY 且未写帧子句时是 RANGE UNBOUNDED PRECEDING 到 CURRENT ROW |
| 取分区末行 | 显式写 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
| 无 ORDER BY 时 | 默认帧即覆盖整个分区,但行顺序不保证 |
| 与 FIRST_VALUE 对比 | FIRST_VALUE 在默认帧下就能正确返回分区首行 |
典型使用场景
- 把每组最后一行的值广播到组内每一行
- 明细行附带「该组的最新状态」或「期末余额」
- 与 FIRST_VALUE 组合查看组内首尾对比
- 时间序列里取区间最后一个采样值
- 排查 LAST_VALUE 返回值等于当前行的原因
默认帧与显式帧的差别
默认帧与显式帧的差别
-- 默认帧下 LAST_VALUE 基本等于当前行的值
SELECT d, amount,
LAST_VALUE(amount) OVER (PARTITION BY shop_id ORDER BY d) AS last_effectively_current
FROM daily_stats;
-- 正确写法:把右边界扩展到分区末尾
SELECT d, amount,
FIRST_VALUE(amount) OVER (PARTITION BY shop_id ORDER BY d
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS first_ok,
LAST_VALUE(amount) OVER (PARTITION BY shop_id ORDER BY d
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_ok
FROM daily_stats;不同帧下的返回值
| 写法 | 帧范围 | LAST_VALUE 返回 |
|---|---|---|
| 默认(带 ORDER BY) | 分区首行到当前行 | 当前行的值 |
| ... AND UNBOUNDED FOLLOWING | 整个分区 | 分区最后一行的值 |
| 无 ORDER BY | 整个分区 | 分区最后一行,但行顺序不确定 |
| 显式以 CURRENT ROW 结尾 | 到当前行为止 | 当前行的值 |
同一段 SQL 里 FIRST_VALUE 表现正确而 LAST_VALUE「失效」,是最常见的困惑组合:原因在于默认帧的右边界是当前行,对首行来说恰好覆盖到分区开头,对末行取值就完全不起作用。
加 UNBOUNDED FOLLOWING 会让窗口覆盖整个分区,排序与内存开销随之上升;大分区场景先用 WHERE 或预聚合压小数据量,再计算首尾值。
「每行都带上该组的期末值」这类需求有两种实现:完整帧的 LAST_VALUE,或先用聚合函数算出组端点值再用窗口广播;后者在只需要一个统计量时开销通常更小。
分区要写清楚:不写 PARTITION BY 时整个结果集算一个分区,首尾值会变成全局首尾;按门店、用户等维度分组统计时,必须显式写出分区键。