[ 
https://issues.apache.org/jira/browse/SPARK-59658?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ASF GitHub Bot updated SPARK-59658:
-----------------------------------
    Labels: pull-request-available  (was: )

> Rewrite sliding window frames as prefix differences
> ---------------------------------------------------
>
>                 Key: SPARK-59658
>                 URL: https://issues.apache.org/jira/browse/SPARK-59658
>             Project: Spark
>          Issue Type: Improvement
>          Components: SQL
>    Affects Versions: 4.4.0
>            Reporter: James Xu
>            Priority: Major
>              Labels: pull-request-available
>
> Spark executes a bounded moving window frame by re-aggregating every row 
> currently inside the frame on every output row: 
> {{SlidingWindowFunctionFrame}} resets the aggregate buffer and re-folds the 
> whole frame for each row produced. For a window operator with nested sliding 
> frames over one (PARTITION BY, ORDER BY) key, the cost is the sum of the 
> frame widths (Σw) aggregate-update evaluations per output row, and the cost 
> is structural — every added horizon multiplies the work. There is no 
> incremental path: the window aggregate processor only supports 
> {{{}initialize{}}}/{{{}update{}}}/{{{}evaluate{}}}, with no {{{}remove{}}}, 
> so a moving frame cannot be advanced row-by-row.
> The pattern is common in user-facing metric tables that compute several 
> rolling horizons per measure over a daily partition ordering. A realistic 
> example:
> {code:sql}
> SELECT
>     seller_user_id,
>     sum(cnt) OVER (PARTITION BY seller_user_id ORDER BY receive_date
>                 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)   AS cnt_3d,
>     sum(cnt) OVER (PARTITION BY seller_user_id ORDER BY receive_date
>                 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)   AS cnt_7d,
>     sum(cnt) OVER (PARTITION BY seller_user_id ORDER BY receive_date
>                 ROWS BETWEEN 29 PRECEDING AND CURRENT ROW)  AS cnt_30d,
>     sum(cnt) OVER (PARTITION BY seller_user_id ORDER BY receive_date
>                 ROWS BETWEEN 89 PRECEDING AND CURRENT ROW)  AS cnt_90d,
>     sum(cnt) OVER (PARTITION BY seller_user_id ORDER BY receive_date
>                 ROWS BETWEEN 179 PRECEDING AND CURRENT ROW) AS cnt_180d
>     -- ... repeated for several more measures
> FROM daily_aggregates{code}
> This shape measures 5 horizons, i.e. Σw = 3+7+30+90+180 = 310 
> aggregate-update evaluations per output row per measure family.
> In a production workload measured against this shape (a 3,437 CPU-hour 
> application), 45% of the CPU sits inside the window operator, and the window 
> stage's own input-sort spill reaches 14.8 TiB on the largest query.
> h3. Root cause
> {{SlidingWindowFunctionFrame.write}} re-runs the aggregate update for every 
> row in the frame on every output row. Growing frames are incremental and 
> offset frames are O(1), so the sliding frame is the only path with no O(1) 
> evaluation. The work factor is exactly Σw (sum of frame widths) per group per 
> row.
> h3. Proposed change
> Add an optimizer rule that rewrites each group of qualifying sliding frames 
> as one running aggregate plus offset differences:
> {code:java}
> -- Running total, reused as the base for every sliding window
> C = sum(x) OVER (PARTITION BY k ORDER BY d
>                 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
> -- n-row sliding sum == running total minus the running total n+1 rows back
> sum(x) OVER (PARTITION BY k ORDER BY d
>             ROWS BETWEEN n PRECEDING AND CURRENT ROW)
>     = C - coalesce(lag(C, n + 1) OVER (PARTITION BY k ORDER BY d), 0){code}
> For integral accumulators with ANSI off this is provably bit-identical: the 
> original fold already computes with wrapping addition, and modular reduction 
> commutes with subtraction, so the running prefix — a magnitude no bounded 
> frame can reach — overflows harmlessly. Floating-point accumulators 
> (non-associative addition) and decimal accumulators (overflow becomes a 
> silent NULL) are excluded, and a nullable measure carries a running count so 
> an all-NULL frame still returns NULL. The rewrite reuses the window 
> operator's own partition and order spec, so no new exchange or sort is 
> introduced.
> The rule is conservative: only non-distinct, unfiltered {{sum}} over integral 
> measures with literal {{n PRECEDING AND CURRENT ROW}} frames qualifies, 
> groups below a minimum total frame width are skipped, and it is gated behind 
> an opt-in configuration flag, default off.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to