DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

PostgreSQL LAST_VALUE 没有返回分组最后一个值

LAST_VALUE默认窗口帧只到当前排序同值组,可能跟随当前行变化。明确完整分区帧,并区分窗口计算、每组一行和最后非空值的不同要求。

使用 LAST_VALUE() 给每条记录附上所在组的末值,结果却跟着当前行变化。加上 PARTITION BY 后,返回行数也没有缩成每组一行。这里需要分开看两个问题:窗口帧决定函数能看到哪些行,窗口函数本身则不会合并输出行。

本文针对 PostgreSQL 的 LAST_VALUE()。这里的“最后”是按窗口 ORDER BY 排序后的最后一行,不代表物理存储顺序或最后一次写入;如果业务要最新状态,排序列必须对应实际事件顺序。

默认窗口帧还没有到达分区末尾

下面的只读示例保留每条事件,同时比较默认帧与整个分区的结果:

WITH events(group_id, event_id, status) AS (
    VALUES (1, 10, 'created'),
           (1, 20, 'running'),
           (1, 30, 'finished'),
           (2, 40, 'queued')
)
SELECT group_id, event_id, status,
       LAST_VALUE(status) OVER (
           PARTITION BY group_id ORDER BY event_id
       ) AS default_last,
       LAST_VALUE(status) OVER (
           PARTITION BY group_id ORDER BY event_id
           ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
       ) AS partition_last
FROM events
ORDER BY group_id, event_id;

PARTITION BY group_id 把事件分组,但带 ORDER BY 时,默认帧只从分区起点延伸到当前行的最后一个排序同值行。例子中 event_id 唯一,所以第一组的 default_last 依次为 created、running、finished;显式覆盖整个分区后,partition_last 才会在这三行都返回 finished。

关键是补齐 UNBOUNDED FOLLOWING,使帧终点到达分区末尾。只写 ROWS UNBOUNDED PRECEDING 仍默认结束于当前行,不能达到同样效果。

不要把默认规则简化为“永远只到当前行”。如果排序键重复,默认帧还包含与当前行排序值相同的其他行。为了确定究竟哪条记录是最后一条,应增加业务允许的稳定决胜键,例如时间之后再按非空唯一事件编号排序。

修改窗口帧不会减少结果行数

如果需要保留每条事件并附上末状态,上面的完整分区帧已经合适。若只想取得每组一个末值,先计算,再只对组键与该末值去重:

WITH annotated AS (
    SELECT group_id,
           LAST_VALUE(status) OVER (
               PARTITION BY group_id ORDER BY event_id
               ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
           ) AS final_status
    FROM events
)
SELECT DISTINCT group_id, final_status
FROM annotated
ORDER BY group_id;

这里的 events 是已有业务表的占位名称,且假设组内 event_id 唯一并表达所需顺序。保留原有租户、权限及业务筛选,不能直接拿片段替换完整查询。

如果在 DISTINCT 的输出中又加入每条事件自己的编号、时间,行仍然不同,自然不会只剩一行。需要返回末条记录的所有字段时,应设计明确的整行选择查询,不能随意把其他行的字段与末状态拼在一起。

同样,MAX(status) 取的是状态值的最大值,不是排序最后那条事件的状态。只有业务本来就在求最大值时,才应换成普通聚合。

排序、过滤和空值分别限定

窗口内部的 ORDER BY 决定计算顺序,不保证最终结果的展示顺序;需要稳定展示时,仍应在最外层查询明确排序。

窗口计算处理的是当前查询筛选后留下的行。若先用 WHERE 排除了某些事件,完整分区帧也只能覆盖剩下的事件。查询意图是“在完整历史中取末值,再筛选展示行”时,需要把这两步分到内外层,不能把过滤前后的结果混为一谈。

PostgreSQL 18 的 LAST_VALUE() 会保留空值语义,不提供 IGNORE NULLS 选项。若按给定顺序最后一行的状态为 NULL,完整帧得到 NULL 是正常结果;它不是“向前找到最后一个非空值”的通用填充工具。排除空值或调整排序都会改变要选的记录,应先确定业务要求。

验证时覆盖多组、单行组、重复时间以及末行空值,并核对组内排序键。修改只涉及查询时,可保留原查询以便回退;如果结果还会写入报表或状态表,应先预览并核对目标记录,回退 SQL 不会恢复此前已经写入的值。