LAST_VALUE必须显式指定ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING并配合确定性ORDER BY(如DESC)才能取到分组尾行;默认帧下仅返回当前行值,易误用。

LAST_VALUE必须搭配ORDER BY和ROWS BETWEEN才能取到分组尾行
直接写 LAST_VALUE(col) 在 MySQL 8.0 中大概率返回当前行值,不是你想要的“分组最后一行”。因为窗口函数默认帧(frame)是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,它只看当前行及之前,而 LAST_VALUE 又默认从这小段里取末尾——自然就是当前行本身。
正确做法是显式声明帧范围:用 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,让整个分区都参与计算。同时 ORDER BY 必须存在,否则 MySQL 会报错或行为不可控。
- 不写
ORDER BY→ 报错Window 'w' with no ORDER BY is not allowed - 只写
ORDER BY不写ROWS→LAST_VALUE仍返回当前行 - 想按业务逻辑定“尾行”,
ORDER BY的字段和方向必须和你的分组意图一致(比如按时间倒序,最新那条才是尾)
分组尾行依赖PARTITION BY + 正确的ORDER BY方向
“分组尾行”本质是每个 PARTITION BY 子句定义的组内,按 ORDER BY 排序后位置最靠后的那条记录的指定字段值。注意:这个“最后”由 ORDER BY 决定,不是物理插入顺序。
例如你要取每个用户最新一条订单的金额,就得按订单时间 ORDER BY created_at DESC,再套 LAST_VALUE(amount);如果误写成 ASC,就会拿到最早一笔的金额。
-
PARTITION BY user_id定义分组边界 -
ORDER BY created_at DESC确保最新记录排在分区开头(对 LAST_VALUE 来说,“最后”就是排序后的末尾,所以 DESC 下末尾=最新) - 完整窗口定义形如:
OVER (PARTITION BY user_id ORDER BY created_at DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
替代方案:用FIRST_VALUE反向排序更安全
很多人发现 LAST_VALUE 行为绕、容易记混,其实用 FIRST_VALUE 配合反向 ORDER BY 更直观可靠。效果完全等价,且不易出错。
比如同样取每个用户的最新订单金额,可以写:FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)。这样逻辑清晰:“每组内按时间倒序排,取第一个”。
- 避免 LAST_VALUE 的帧理解负担
- MySQL 对 FIRST_VALUE 的默认帧兼容性更好,即使漏写
ROWS,结果也往往符合预期(但依然建议显式写出) - 若需兼容旧版 MySQL 或其他数据库,FIRST_VALUE 的行为一致性更高
性能与NULL值处理要提前验证
LAST_VALUE(或 FIRST_VALUE)本身不触发额外扫描,但窗口定义中的 ORDER BY 字段如果没有索引,会导致分组内排序变慢,尤其数据量大时。
另外,当分组中某列全为 NULL,LAST_VALUE(col) 返回 NULL —— 这是符合标准的,但容易被当成计算失败。如果业务上需要兜底,得配合 COALESCE,比如 COALESCE(LAST_VALUE(amount), 0)。
- 检查
PARTITION BY和ORDER BY字段是否有联合索引(如(user_id, created_at)) - 用
EXPLAIN ANALYZE看窗口计算是否走了 filesort - 测试分组内含 NULL 值的场景,确认返回结果符合业务预期
ROWS BETWEEN...,也不要依赖默认行为。MySQL 8.0 的窗口函数不难,但它的默认帧和其他数据库不一致,这点必须亲手试过才记得住。


















