Spark中使用未定义Frame的WindowSpec是否安全?
你观察到的现象完全没问题——在只指定orderBy的WindowSpec里,Spark的默认Frame规则确实和你显式写的rowsBetween(Window.unboundedPreceding, Window.currentRow)效果一致,所以你的累积和计算结果才会完全重合。但这并不代表所有场景下不指定Frame都是安全的,Spark 2.x里确实存在一些不指定Frame就会踩坑的情况,下面给你详细拆解:
1. 先搞懂Spark的默认Frame规则
Spark对WindowSpec的Frame默认行为是分场景的:
- 当你只定义了orderBy(没有分区):默认Frame是
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。在你的例子里,排序键是唯一的整数,RANGE和ROWS的效果完全一样,所以结果没差别。 - 当你同时定义了partitionBy和orderBy:默认Frame同样是
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。 - 当你只有partitionBy,没有orderBy:这时候默认Frame会变成
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING——简单说就是聚合整个分区的所有行,这是最容易出问题的情况!
2. Spark 2.x中未指定Frame会出问题的典型场景
场景一:只有分区,没有排序,默认聚合整个分区
假设你想计算每个分组内当前行之前的平均值,但不小心没加orderBy,也没指定Frame:
import org.apache.spark.sql.expressions.Window val df = Seq( ("A", 1), ("A", 2), ("A", 3), ("B", 4), ("B", 5) ).toDF("group", "value") // 只分区,没排序,也没指定Frame val windowSpec = Window.partitionBy($"group") df.select( $"group", $"value", avg($"value").over(windowSpec).as("avg_value") ).show()
输出结果会是:
+-----+-----+--------+ |group|value|avg_value| +-----+-----+--------+ | A| 1| 2.0| | A| 2| 2.0| | A| 3| 2.0| | B| 4| 4.5| | B| 5| 4.5| +-----+-----+--------+
这里的avg_value是整个分组的平均值,而不是你可能预期的“从分组开头到当前行的平均值”。如果业务逻辑是后者,那这个结果就完全不符合要求了。
场景二:排序键有重复值,默认RANGE和ROWS的差异
当排序键存在重复时,默认的RANGE Frame会把所有和当前行排序键相等的行都包含进来,而ROWS是按物理行位置计算的。比如:
val df = Seq(1,1,2,2,3).toDF("i") df.select( $"i", sum($"i").over(Window.orderBy($"i")).as("range_sum"), sum($"i").over(Window.orderBy($"i").rowsBetween(Window.unboundedPreceding, Window.currentRow)).as("rows_sum") ).show()
输出结果:
+---+---------+--------+ | i|range_sum|rows_sum| +---+---------+--------+ | 1| 2| 1| | 1| 2| 2| | 2| 6| 4| | 2| 6| 6| | 3| 9| 9| +---+---------+--------+
看前两行,range_sum直接把两个1的和算出来了,而rows_sum是逐行累积的。如果你的业务逻辑需要按物理行顺序累积,不指定Frame(用默认RANGE)就会得到错误结果。
3. 总结:什么时候必须显式指定Frame?
- 当你的WindowSpec没有orderBy时:一定要显式指定Frame,否则默认会聚合整个分区;
- 当排序键存在重复值,且你需要基于物理行位置做聚合时:必须显式写
rowsBetween,不能依赖默认的RANGE规则; - 当你需要非默认的窗口范围(比如滑动窗口:当前行前后3行):那肯定得自己指定Frame。
回到你的例子,因为排序键是唯一的整数,且你要的是从开头到当前行的累积和,所以默认Frame和显式指定的效果一致,结果正确。但在上面提到的场景里,不指定Frame就会踩坑,所以建议在生产环境中,只要用到Window函数,最好都显式指定Frame——这样代码更清晰,也能避免潜在的逻辑错误。
内容的提问来源于stack exchange,提问作者Raphael Roth

