last_value默认返回当前行值是因为窗口帧为rows between unbounded preceding and current row,需显式指定unbounded following才能获取分组末值;用first_value配合order by desc更安全自然;取整行最新记录应使用row_number()。

LAST_VALUE 默认只返回当前行,不是分组末值——这是窗口帧没配对导致的,不是函数写错了。
LAST_VALUE 为什么总等于当前行的值
直接写 LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at),结果每行都显示自己的 amount,因为默认窗口帧是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。它只看“开头到当前行”,当前行就是这个范围里的“最后”,根本看不到后面的数据。
这不是数据库 bug,是 SQL 标准定义的行为。PostgreSQL、SQL Server、MySQL 8.0+ 都一样。
- ORDER BY 升序时,“最后”对应最大时间;降序时,“最后”反而是最早时间——语义容易反直觉
- 哪怕加了
PARTITION BY,只要没改窗口帧,照样只在滑动段内找“最后” -
IGNORE NULLS只影响 NULL 跳过逻辑,不解决帧范围问题
显式指定窗口帧才能拿到真末值
必须补上 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,让窗口覆盖整组所有行:
SELECT user_id, created_at, amount,
LAST_VALUE(amount) OVER (
PARTITION BY user_id
ORDER BY created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS latest_amount
FROM orders;
- ORDER BY 是升序,
LAST_VALUE才对应时间最晚那条记录的amount - 如果
created_at有重复,结果不确定——得加二级排序,比如ORDER BY created_at, id - SQLite 不支持窗口帧子句,MySQL 8.0 之前也不支持,别在旧环境硬套
用 FIRST_VALUE + 降序比 LAST_VALUE 更少出错
多数场景要的是“最新一条”,本质是“按时间倒序排的第一条”。写 FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at DESC) 更自然:
- 默认窗口帧就覆盖整组,不用手动补
ROWS子句,漏写的概率低 - 语义清晰:“最新 = 时间最大 = 降序排第一”,不容易绕晕
- 但注意:它仍只返回单个字段值,不能直接拿整行
想取整条最新记录,别硬套 LAST_VALUE
LAST_VALUE 是逐列计算的。你写 LAST_VALUE(name) 和 LAST_VALUE(status),它们可能来自不同行——尤其当排序键不唯一时,字段错位风险极高。
真正要拿最新那条完整记录,用 ROW_NUMBER():
SELECT user_id, order_id, status, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM orders
) t
WHERE rn = 1;
- 复合索引
(user_id, created_at, id)能直接走索引,性能比多次LAST_VALUE更好 - 过滤条件(如
status = 'done')必须放外层或 CTE,不能塞进窗口定义里 - 所有主流引擎(PostgreSQL / MySQL 8.0+ / SQL Server / Oracle / DuckDB)都支持
最容易被忽略的点:窗口帧不是可选项,是决定性因素;而取整行这件事,LAST_VALUE 从设计上就不支持——强行用,只会让数据错位更隐蔽。











