订阅更新

SQL LAST_VALUE 为什么取不到最后一条?窗口边界怎么写?

面试问 271 约 4 分钟读完

本文目录
  1. 面试怎么回答
  2. 名字里的 LAST,到底是谁的最后?
  3. 用同一组数据,对比两种写法
  4. 排序不完整,改了窗口也不够
  5. 最后一条为空,要不要跳过?
  6. 一个典型场景:最后状态没有统一
  7. 最终值也可能是未来信息
  8. 参考资料

面试怎么回答

我会先区分分区和窗口帧。LAST_VALUE 返回当前帧最后一行,不自动代表整个分组最后一条。有排序时,默认帧通常止于当前行的同序值组;要取全组末值,应显式写到 UNBOUNDED FOLLOWING,并补足确定性排序。最后一行是 NULL 时也要明确是否保留。用于历史特征时还应防止引用未来数据。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

同一个用户有三次状态:新建、支付、完成。想让每一行都带上最终状态,写了 LAST_VALUE,结果三行却各不相同。先别怀疑数据乱序:你想取的是整个分组的末值,函数实际看的可能只是当前窗口帧。

LAST_VALUE 取的是当前窗口帧的末行;取全组最终值,要明确排序、完整帧和空值规则。

名字里的 LAST,到底是谁的最后?

PARTITION BY 决定哪些行属于同一组,ORDER BY 决定组内次序,窗口帧决定当前这一行计算时能看到哪些行。这三件事分工不同。

LAST_VALUE 取的是帧内末行的表达式值,不会因为名字叫“最后”,就自动越过当前帧看到整个分区末尾。FIRST_VALUE 看起来经常符合直觉,是因为默认帧通常从分区开头开始,恰好容易掩盖边界问题。

本文按 PostgreSQL 17 说明。有 ORDER BY 且不显式指定帧时,默认范围到当前行的最后一个同序值行。排序键唯一时,末行往往就是当前行;存在并列时,又不能简单说“永远返回当前行”。

用同一组数据,对比两种写法

下面假设 user_id、event_time、event_id 共同提供确定的组内顺序,状态变化为新建、支付、完成:

SELECT user_id, event_time, event_id, status,
       LAST_VALUE(status) OVER (
         PARTITION BY user_id
         ORDER BY event_time, event_id
       ) AS frame_last,
       LAST_VALUE(status) OVER (
         PARTITION BY user_id
         ORDER BY event_time, event_id
         ROWS BETWEEN UNBOUNDED PRECEDING
                  AND UNBOUNDED FOLLOWING
       ) AS final_status
FROM order_events;

第一列计算沿默认帧前进,第二列明确看完整个分区。对这组三条且顺序唯一的数据,frame_last 依次是新建、支付、完成,final_status 则三行都是完成。

关键关系图

注意,窗口函数不会自动把三行压成一行。需求如果是“每个用户只保留最后一条事件”,可以按倒序排名再选第一行;那是结果粒度的选择,与给每一行补最终状态不同。

排序不完整,改了窗口也不够

两条事件可能有相同时间。只按 event_time 排序,谁最后并没有被业务规则唯一确定。执行计划变化后,结果可能改变,但这不一定是引擎随机算错。

应补充能表达先后关系的序列、版本或业务事件号。不能随意拿一个字段凑成唯一排序,却没有解释它是否代表真实先后。

事件时间为空也要单独决定处理方式:排除异常、隔离修复,还是明确放在前后。将未知时间默认为最新,可能把一条脏记录推成全组最终状态。

最后一条为空,要不要跳过?

如果末条事件的 status 是 NULL,完整帧的 LAST_VALUE 可以返回 NULL。它不是自动寻找最后一个非空值。

PostgreSQL 17 没有实现这个函数的 IGNORE NULLS 选项;跨引擎复制语法前要先核对能力。业务要取最后非空记录,可以先在独立子查询里筛出有效状态并排名,再关联回原始明细。

但在主查询直接过滤 status IS NOT NULL,会同时改变保留的明细行。对于“明细一行都不能丢”的需求,修复函数时不能顺便改掉结果范围。

一个典型场景:最后状态没有统一

比如验收发现,同一个订单的不同行带着不同“最终状态”,最初以为事件乱序或写入重复。检查后事件顺序完全正常,问题是省略了完整窗口帧。

旧样例每个订单只有一条记录,当前帧末行和全组末行相同,所以错误写法照样通过。新的测试需要覆盖多条变化、相同时间、末行空值,以及两个不同分组。

修复后还应核对输出行数与原明细一致。最终状态统一,不代表允许重复关联或误删行;语义正确和粒度正确要同时成立。

最终值也可能是未来信息

做历史分析时,完整分组里的“完成”可能发生在几天之后。如果为每个历史时点生成特征,把这个最终状态补到过去,就引入了当时尚不可知的信息。

因此要区分“今天回顾完整生命周期”和“还原某时刻已知状态”。前者可以看全组,后者要先限定可用事件范围和时间条件,再决定窗口。

面试里最有价值的不是背出 UNBOUNDED FOLLOWING,而是说清:你想保留多少行、排序代表什么、当前计算允许看多远。

参考资料

刷题进度保存在当前浏览器