akupchinskiy opened a new issue, #27101:
URL: https://github.com/apache/datafusion/issues/27101

   ## Describe the bug
   
   Some window queries over a `Timestamp(_, Some(tz))` column get slower as 
`target_partitions`
   grows, while the same queries over the same instants without a time zone 
scale normally. At 1
   partition the two take about the same time, and the profiles show clone/drop 
of `ScalarValue` and
   `DataType` as hotspots. That points to contention on the time zone's 
`Arc<str>` refcount: every
   partition shares it, and these paths clone and drop it about once per row.
   
   It looks like the same mechanism as #27046, on window code paths.
   
   ## To Reproduce
   
   `datafusion-cli` from `main` (238ae32), release build, Apple M2 Pro (10 
cores, on AC power).
   2M rows; `ts` is `Timestamp(µs, "UTC")`, `tsn` holds the same instants 
without a time zone:
   
   ```sql
   COPY (
     SELECT value / 2 AS k1,
            (value * 7919) % 1000003 AS o,
            value % 977 AS v,
            arrow_cast(to_timestamp_micros(1735689600000000 + ((value * 
2654435761) % 1000000007) * 1000), 'Timestamp(Microsecond, Some("UTC"))') AS ts,
            to_timestamp_micros(1735689600000000 + ((value * 2654435761) % 
1000000007) * 1000) AS tsn
     FROM generate_series(0, 1999999)
   ) TO 'fvtz.parquet' STORED AS PARQUET;
   CREATE EXTERNAL TABLE t STORED AS PARQUET LOCATION 'fvtz.parquet';
   
   SET datafusion.execution.target_partitions = 10;   -- and 1
   SELECT max(r) FROM (SELECT max(ts) OVER (PARTITION BY k1 % 1000 ORDER BY o) 
r FROM t);
   SELECT max(r) FROM (SELECT max(tsn) OVER (PARTITION BY k1 % 1000 ORDER BY o) 
r FROM t);
   ```
   
   Each query ran 4 times per setting; the table shows the median of the last 
3, in ms:
   
   | query (`x` = `ts` or `tsn`) | `ts`, 1 partition | `tsn`, 1 partition | 
`ts`, 10 partitions | `tsn`, 10 partitions | `ts` / `tsn` at 10 |
   |---|---|---|---|---|---|
   | `max(x) OVER (PARTITION BY k1 % 1000 ORDER BY o)` | 500 | 497 | 1091 | 117 
| **9.3x** |
   | `lag(x) OVER (PARTITION BY k1 ORDER BY o)` | 635 | 635 | 1020 | 465 | 
**2.2x** |
   | `row_number() OVER (PARTITION BY x ORDER BY o)` | 699 | 690 | 895 | 612 | 
1.5x |
   | `sum(v) OVER (PARTITION BY k1 ORDER BY x)` | 1018 | 1008 | 862 | 602 | 
1.4x |
   | `first_value(x) OVER (PARTITION BY k1 ORDER BY o)` | 800 | 776 | 656 | 610 
| 1.1x |
   | `GROUP BY x` (control) | 61 | 60 | 23 | 23 | 1.0x |
   
   The running `max(ts)` takes 2.2x longer with 10 partitions than with 1.
   
   ## Where the time goes
   
   I sampled `main` (c120619, which has the same window code as 238ae32; 
`release-nonlto` build for
   symbols) while each query ran repeatedly at 10 partitions. I counted the 
non-idle samples whose top
   frame is a clone or drop of `ScalarValue` or `DataType`. That is where the 
refcount updates are
   inlined, though not every such sample is contention.
   
   | query | share of samples (`ts` / `tsn`) | where the per-row scalars come 
from |
   |---|---|---|
   | running `max(ts)` | 41% / 4% | 
`PlainAggregateWindowExpr::get_aggregate_result_inside_range` 
(`physical-expr/src/window/aggregate.rs`) slices the input as the frame 
advances, and each slice clones the `DataType`. `MaxAccumulator::update_batch` 
(`functions-aggregate-common/src/min_max.rs`) replaces its scalar and 
`evaluate` clones it. The results go through `ScalarValue::iter_to_array`. |
   | `lag(ts)` | 32% | The stateful evaluator in 
`functions-window/src/lead_lag.rs` builds each output with 
`ScalarValue::try_from_array`, then `iter_to_array`. (`evaluate_all` already 
shifts arrays.) |
   | `row_number() … PARTITION BY ts` | 38% | `compute_partition_permutation` 
in `physical-plan/src/windows/bounded_window_agg_exec.rs` builds a 
`Vec<ScalarValue>` key per row (`get_row_at_idx`) to compare with the partition 
keys (`PartitionKey`, `physical-expr/src/window/window_expr.rs`). |
   | `sum(v) … ORDER BY ts` | 23% / 8% | The `RANGE` frame bounds: 
`WindowFrameStateRange::calculate_index_of_row` → `search_in_slice` builds 
comparison scalars per probe (`extract_row_at_idx_to_buf`). |
   
   The query that runs the most of these per row, the running aggregate, 
degrades the most.
   
   ## Possible fixes
   
   - **Fewer per-row scalars on these paths.** The running aggregate could 
build its output without
     cloning the accumulator state for every row, `lead`/`lag` could take 
values by index, and
     partition keys and `RANGE` bounds could be compared on arrays or in row 
format (`arrow::row`)
     instead of as `ScalarValue`s. These are separate, partial changes, but 
they would save
     allocations for every data type, not just timestamps.
   - **One time zone `Arc<str>` per execution partition.** If each partition 
cloned the time zone
     string into its own `Arc` once and used it for the arrays and scalars it 
creates, partitions
     would no longer share a refcount, and the time zone and semantics would 
stay the same. That goes
     at the contention itself and would help other operators too, but it needs 
a point where arrays
     enter a partition to swap the `DataType`.
   - **Dropping the time zone where it can't matter**, as #27047 does for the 
ordering state of
     `FirstLastGroupsAccumulator`: partition keys, say, or the values of 
built-in functions like
     `lag` and `max`. This can't be applied to the window operator as a whole. 
`RANGE` frames with
     interval offsets add intervals to the timestamp, which depends on the time 
zone (DST, calendar
     days), and user-defined window or aggregate functions may look at it.
   
   ## Expected behavior
   
   Window queries over tz-aware timestamps scale with partitions like the same 
queries over naive
   timestamps.
   
   ## Additional context
   
   I found these while comparing engines on a benchmark of my own (event data 
with UTC timestamps).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to