因为first_value和last_value默认窗口帧为rows between unbounded preceding and current row,仅覆盖分区开头至当前行,故last_value常返回当前行值而非分组末尾;必须显式指定rows between unbounded preceding and unbounded following并配合确定order by才能取整组首尾值。

为什么FIRST_VALUE和LAST_VALUE返回的不是分组内首尾记录
因为FIRST_VALUE和LAST_VALUE是窗口函数,它们默认的窗口帧是ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是说LAST_VALUE在每一行只看“从开头到当前行”,不是整个分组。结果就是LAST_VALUE经常返回当前行的值,而不是分组末尾那条。
实操建议:
- 必须显式指定窗口帧为
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,才能让两个函数作用于完整分组 - 排序(
ORDER BY)不可省略,否则结果不确定;即使只取首尾,也要明确按什么字段排(如时间、ID) - 若分组内有并列排序值(如相同
created_at),FIRST_VALUE和LAST_VALUE可能任意选一个——这不是bug,是标准行为
怎么写才真正拿到每个分组的第一条和最后一条完整记录
FIRST_VALUE/LAST_VALUE只能返回某列的值,不能直接返回整行。要拿完整记录(比如整条用户订单数据),得配合ROW_NUMBER()或子查询。
实操建议:
- 用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col)给每组内行编号,再在外层WHERE rn = 1 OR rn = MAX(rn)——但注意MAX(rn)需先聚合,通常改用ROW_NUMBER() ... DESC取倒序第一 - 更稳妥:两次
ROW_NUMBER(),一次升序一次降序:rn_asc = ROW_NUMBER() OVER (PARTITION BY type ORDER BY id),rn_desc = ROW_NUMBER() OVER (PARTITION BY type ORDER BY id DESC),然后WHERE rn_asc = 1 OR rn_desc = 1 - 如果只要单列值(如最新价格、最早状态),
FIRST_VALUE(price) OVER (PARTITION BY product_id ORDER BY updated_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)就足够,且性能通常优于ROW_NUMBER()+ 过滤
MySQL 8.0+ 和 PostgreSQL 的行为差异点
PostgreSQL 对LAST_VALUE的默认帧更宽松,但依然推荐显式声明;MySQL 8.0 严格遵循标准,默认帧就是CURRENT ROW范围,不写ROWS BETWEEN...基本得不到想要的“分组末尾”值。
实操建议:
- MySQL 中漏写窗口帧是高频错误,执行后
LAST_VALUE列几乎全等于当前行对应列——检查方法:对同一分组,看该列是否全部相同 - PostgreSQL 允许用
RANGE帧,但涉及时间/数值类型时可能意外合并相邻值;统一用ROWS帧更可控 - 两者都不支持在
GROUP BY语句里直接用FIRST_VALUE——它必须出现在SELECT或ORDER BY中,且窗口定义不能引用外部GROUP BY别名
替代方案:什么时候不该硬用FIRST_VALUE/LAST_VALUE
当目标只是取分组极值记录,且表有合适索引时,关联子查询或LATERAL(PostgreSQL)/ JOIN + GROUP BY聚合可能更直观、更容易调试。
实操建议:
- 例如查每个部门薪资最高的人:
SELECT * FROM emp e1 WHERE salary = (SELECT MAX(salary) FROM emp e2 WHERE e2.dept = e1.dept),逻辑直白,可读性强 - 若存在多个同最高薪者,
FIRST_VALUE只返回一个,而子查询会返回全部——根据业务需求选:要“任一”还是“全部” - 大数据量下,
FIRST_VALUE+ 窗口帧需要全分组排序缓存,而某些子查询能利用索引跳过排序;执行前务必看EXPLAIN
实际写的时候,最容易被忽略的是窗口帧的显式声明——哪怕在PostgreSQL里测试通过了,换到MySQL或升级版本后可能突然失效。还有就是混淆“取值”和“取行”,以为FIRST_VALUE(<em>)</em>存在,其实它不接受,只能逐列写。










