[SPARK-58201][SQL] Optimize sliding window MIN/MAX aggregate function using monotonic deque#57346
[SPARK-58201][SQL] Optimize sliding window MIN/MAX aggregate function using monotonic deque#57346pavan51 wants to merge 4 commits into
Conversation
7b7ba70 to
1691d2d
Compare
| * using monotonic deques. This provides O(N) time complexity instead of O(N * W) of | ||
| * [[SlidingWindowFunctionFrame]] or O(N log W) of [[SegmentTreeWindowFunctionFrame]]. | ||
| */ | ||
| private[window] final class SlidingWindowMinMaxFunctionFrame( |
There was a problem hiding this comment.
This adds a new default-on execution path with no correctness test. I think it might be worth considering adding a differential test.
There was a problem hiding this comment.
Thanks for the suggestion! I've added a new differential correctness test suite (MonotonicDequeWindowFunctionSuite ) verifying that the Monotonic Deque matches both the Segment Tree and Naive baselines across primitives, reference types, range/row frames, and null values.
| case BooleanType | ByteType | ShortType | IntegerType | LongType | FloatType | DoubleType | | ||
| DateType | TimestampType | TimestampNTZType => true |
There was a problem hiding this comment.
YearMonthIntervalType (Int) and DayTimeIntervalType (Long) are also primitive-backed and valid for MIN/MAX. Suggested Performance Fix:
| case BooleanType | ByteType | ShortType | IntegerType | LongType | FloatType | DoubleType | | |
| DateType | TimestampType | TimestampNTZType => true | |
| case BooleanType | ByteType | ShortType | IntegerType | LongType | FloatType | DoubleType | | |
| DateType | TimestampType | TimestampNTZType | _: YearMonthIntervalType | | |
| _: DayTimeIntervalType => true |
There was a problem hiding this comment.
Great catch! I've added YearMonthIntervalType and DayTimeIntervalType to the primitive match list to skip the row-copy overhead for interval columns. Thanks!"
|
Can you PR title complete? |
4ad13b9 to
3c3a8a3
Compare
Updated the title of the PR with complete info |
3c3a8a3 to
5fb7ba8
Compare
Ma77Ball
left a comment
There was a problem hiding this comment.
After a static review, the PR LGTM! I left two suggestions. cc @HyukjinKwon
| "This provides O(N) complexity instead of O(N * W) or O(N log W).") | ||
| .version("5.0.0") | ||
| .booleanConf | ||
| .createWithDefault(true) |
There was a problem hiding this comment.
This new optimization defaults to on, but the sibling spark.sql.window.segmentTree.enabled (same area, added in 4.2.0) defaults to off. Turning a brand-new execution path on by default for all sliding MIN/MAX window queries on its first release is risky; consider defaulting to false to match and let it bake before flipping on. Fix:
| .createWithDefault(true) | |
| .createWithDefault(false) |
There was a problem hiding this comment.
Unlike the segment tree, the monotonic deque has O(1) amortized cost and does not suffer from tree-building overhead on small windows. However, keeping it disabled by default for its initial release as a safer approach to mitigate risk. I've updated the default to false.
| val isMinMaxOnly = conf.windowMonotonicDequeEnabled && | ||
| functions.nonEmpty && functions.forall { | ||
| case _: Min | _: Max => true | ||
| case _ => false | ||
| } && aggFilters.forall(_.isEmpty) |
There was a problem hiding this comment.
The isMinMaxOnly guard is duplicated in the Moving Frame case below. Might be better to add it to a small local helper so the two copies don't drift.
There was a problem hiding this comment.
Done. I've refactored this and pulled isMinMaxOnly up to the outer scope of the match block so it is cleanly shared by both the moving and shrinking frame cases.
5ad3ec1 to
be2c3e4
Compare
|
Hi @HyukjinKwon All suggestions from @Ma77Ball 's review have been fully addressed (the feature is now disabled by default for safety, code duplication was refactored, and primitive interval optimization was added). The new correctness tests are passing locally and CI is green.Could you please take a look when you have a moment? Thanks! |
…s using monotonic deque
…terval primitives
… resolve Scalastyle checks
b237a9b to
3298021
Compare
|
Hi @peter-toth @Yicong-Huang @HyukjinKwon, Could any of you please review this PR when you get a chance. @Ma77Ball has reviewed and approved the changes, so it is ready for your final reviews! |
What changes were proposed in this pull request?
This PR proposes to optimize sliding window
MIN/MAXaggregate functions using a monotonic deque implementation (SlidingWindowMinMaxFunctionFrame).Key improvements:
MINand/orMAX, we maintain a double-ended queue (deque) of(value, index)pairs for each function. New values are admitted and elements that fall outside the sliding window boundary are dropped from the front in amortizedUnsafeRow) recycle memory, meaning reference types (like String, Decimal, Array) must be copied to avoid corruption. However, JVM primitives (like Int, Long, Double, Float, Boolean, Date, Timestamp) are passed by value and are immune to this. We introduce a type check that skips heap allocation/copying entirely for primitive types.WindowEvaluatorFactoryBaseto detect when the query uses onlyMIN/MAXand automatically instantiateSlidingWindowMinMaxFunctionFrame.spark.sql.window.monotonicDeque.enabled(default:true) to allow toggling this optimization.Why are the changes needed?
Currently, sliding window aggregates in Spark are executed using:
SlidingWindowFunctionFrame): Re-scans all rows in the sliding window buffer on every row, resulting in O(N x W) time complexity.SegmentTreeWindowFunctionFrame): Built over partition blocks, offering O(N x log W) complexity but with significant block-caching and tree-construction overhead.While the Segment Tree is an improvement over Naive for large windows, it has three major deficiencies:
By implementing a Monotonic deque, we achieve true linear O(N) time complexity (amortized O(1) operations per row regardless of window size) and a minimal memory footprint.
As a result:
Does this PR introduce any user-facing change?
No user-facing behavior changes. It is a purely physical plan performance optimization.
There is a new configuration:
spark.sql.window.monotonicDeque.enabled(Default:true) to enable/disable the monotonic deque optimization.How was this patch tested?
SegmentTreeWindowFunctionSuiteandDataFrameWindowFramesSuite(all 80 tests succeeded), and added a new differential correctness suiteMonotonicDequeWindowFunctionSuiteverifying monotonic deque outputs against Naive and Segment Tree baselines across multiple data types (including date/timestamp/interval types), range/row frames, and null values.WindowBenchmarkmatch the baseline exactly.WindowBenchmarksuite.The updated benchmark results under
OpenJDK 23.0.1 on Mac OS X 15.1.1 (Apple M1)are:MIN Sliding Window (W=1001, 256K rows)
MAX Sliding Window (W=1001, 256K rows)
MIN Sliding Window Scaling (W=100001, 2M rows)
MIN/MAX Small Window Scaling (W=11, 2M rows - Pareto Loss Zone Check)
In small windows, the naive sliding window scan O(W) is fast, whereas the segment tree has high setup and block query overhead O(log W), causing it to be slower than naive. Monotonic Deque dominates both.
Was this patch authored or co-authored using generative AI tooling?
Yes, co-authored by Google DeepMind Coding Assistant