LAST_VALUE默认不返回分组末行,因其窗口帧为ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,仅覆盖当前行及之前行;需显式指定UNBOUNDED FOLLOWING并配合稳定排序(如ORDER BY hire_date, id)才能正确获取分组末行值。

LAST_VALUE 为什么默认不返回分组末行
因为 LAST_VALUE 默认窗口帧是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,它只看当前行及之前的数据,所以绝大多数情况下返回的是当前行的值,不是分组最后一行。哪怕加了 PARTITION BY,帧范围没改,照样“卡”在当前行。
常见错误现象:LAST_VALUE(name) OVER (PARTITION BY dept_id ORDER BY hire_date) 返回的全是当前员工的名字,而不是该部门最新入职员工的名字。
- 必须显式指定窗口帧为
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -
ORDER BY子句不可省略——即使你只想要末行,SQL 仍需明确“末”是按什么顺序定义的 - 如果
ORDER BY字段有重复值(比如多个员工同一天入职),LAST_VALUE可能返回任意一个(取决于排序稳定性与数据库实现),建议补上唯一键如id做二级排序
正确写法:显式帧 + 稳定排序
要真正拿到每个分组的末行值,得把窗口定义写全:
SELECT
dept_id,
name,
hire_date,
LAST_VALUE(name) OVER (
PARTITION BY dept_id
ORDER BY hire_date, id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_hired_name
FROM employees;
注意这里用了 hire_date, id 而非单字段排序,避免因时间相同导致结果不可预测;ROWS BETWEEN ... UNBOUNDED FOLLOWING 才让函数“看到”整个分区所有行。
- PostgreSQL、Oracle、SQL Server 都支持该写法;MySQL 8.0+ 同样可用,但旧版不支持窗口函数
- 若只需末行整条记录(不止一个字段),
LAST_VALUE不够用,应改用ROW_NUMBER() OVER (... ORDER BY ... DESC)配合WHERE rn = 1 - 性能上,全帧扫描比默认帧开销大,大数据量分组时要注意执行计划是否走索引覆盖
替代方案:用 FIRST_VALUE 配合逆序更安全
很多人忽略这点:把 ORDER BY 逆过来,用 FIRST_VALUE 实际上等价且更直观——毕竟“倒序后的第一行”就是原序的末行,而且 FIRST_VALUE 默认帧虽也是当前行前,但逆序后它的行为更符合直觉,不易漏写 UNBOUNDED FOLLOWING。
SELECT
dept_id,
name,
hire_date,
FIRST_VALUE(name) OVER (
PARTITION BY dept_id
ORDER BY hire_date DESC, id DESC
) AS last_hired_name
FROM employees;
这个写法不用手动指定帧范围,数据库自动按默认帧计算,但因为排序已反转,FIRST_VALUE 天然就取到逻辑末行。
- 语义清晰,少一个出错点;尤其适合新手或快速脚本
- 和
LAST_VALUE+ 全帧相比,执行计划通常一致,优化器能识别这种等价转换 - 但如果业务逻辑本身依赖“正序末行”,逆序写法可能增加理解成本,需配合注释说明
容易被忽略的 NULL 处理和空分组问题
当某分组所有 name 都为 NULL,或者分组为空(比如 PARTITION BY dept_id 中某个 dept_id 在数据中不存在),LAST_VALUE 会返回 NULL,但不会报错,也无提示——这在报表或下游 ETL 中容易埋雷。
- 用
COUNT(*) OVER (PARTITION BY dept_id)先检查分组行数,过滤掉空分组 - 对关键字段加
COALESCE(LAST_VALUE(...), 'N/A')显式处理 NULL,避免前端渲染异常 - 某些数据库(如 SQL Server)支持
IGNORE NULLS修饰符:LAST_VALUE(name) IGNORE NULLS OVER (...),但 PostgreSQL 和 MySQL 不支持,跨库迁移时要留意
实际用的时候,别光盯着函数名想当然——LAST_VALUE 的“最后”是窗口帧决定的,不是分区决定的;而帧范围不写全,它就永远只认得当前行。










