LAST_VALUE()默认返回当前行值,因其窗口帧为RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,仅覆盖分区开头至当前行;需显式指定ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING并配合确定性ORDER BY才能取到分组末值。

LAST_VALUE() 总返回当前行,不是整个分区的最后值
这是窗口函数里最常踩的坑:你写了 LAST_VALUE(x) OVER (ORDER BY ts),结果每次都是取到当前行的 x,而不是按 ts 排序后分区里的真正最后一个值。
原因在于默认窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW——“最后”只在当前行及之前生效,不是整个分区。
解决方法只有显式改写窗口帧:
- 要取整个分区最后一行:用
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - 要取排序后最后一行(但不一定是物理最后一行):仍需
ROWS+UNBOUNDED FOLLOWING,RANGE在有重复排序值时会出错 - 别依赖
RANGE做位置敏感操作,它按值分组,不是按行序
ROWS 和 RANGE 的行为差异直接影响累计计算结果
ROWS 按物理行位置定义窗口边界,RANGE 按排序值的逻辑范围定义。两者在排序字段有重复值时表现完全不同。
例如 SUM(sales) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 严格按时间先后逐行累加;而换成 RANGE 后,如果某天有多条记录,它们会被归为同一“值组”,SUM 会一次性把当天所有 sales 加进去,再跳到下个日期。
常见影响场景:
-
LAG()/LEAD()必须用ROWS,RANGE无法定义“上一行” -
FIRST_VALUE()在RANGE下可能返回同组内任意一行(优化器决定),不保证首行 - 带
CURRENT ROW的累计函数,若排序字段有重复,RANGE会让“当前行”实际覆盖多行,结果膨胀
ORDER BY + ROWS BETWEEN 在无索引时性能雪崩
窗口函数带 ORDER BY 且指定 ROWS BETWEEN(比如滚动平均),数据库必须对每个分区内部严格按序处理——这意味着必须先完成全分区排序,再划窗计算。
如果 PARTITION BY a ORDER BY b 对应的索引缺失或顺序错位(如建了 (b, a) 而非 (a, b)),就会触发磁盘外部排序(External Sort),I/O 直接成为瓶颈。
- 检查执行计划:
EXPLAIN中Extra出现Using filesort就是危险信号 - 复合索引必须严格按
PARTITION BY字段在前、ORDER BY字段紧随其后的顺序建立 - 避免在
ORDER BY中用表达式(如ORDER BY DATE(created_at)),否则普通索引无效,得建函数索引(MySQL 8.0+ 才支持)
不写 ORDER BY 时 ROW_NUMBER() 为什么有时还能用
ROW_NUMBER() OVER (PARTITION BY x) 不写 ORDER BY,数据库可能跳过排序直接编号——但这完全依赖优化器实现,且结果不可预测:同一查询多次执行,编号顺序可能不同。
一旦你显式写了 ORDER BY y 却没给 y 建索引,数据库反而会强制排序,比不写还慢。
- 真不需要稳定顺序?那就别写
ORDER BY,但别依赖编号规律 - 需要可重现顺序?必须确保
ORDER BY字段有对应索引,且类型不触发隐式转换(比如字符串字段用COLLATE统一规则) - 想兼顾性能和确定性?用
ORDER BY y, id(id是主键),让无重复y时也能靠id稳定收尾
窗口帧的语义细节藏在默认行为里,不显式声明 ROWS 或 RANGE,就等于把控制权交给数据库——而它选的往往不是你要的那一个。

















