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

   ## Describe the bug
   
   A grouped `first_value(x ORDER BY ts)` / `last_value(x ORDER BY ts)` where 
`ts` is
   `Timestamp(_, Some(tz))` does not scale with `target_partitions`; it gets 
slower as partitions are
   added. The same query ordered by a timestamp without a time zone (same 
instants) scales normally.
   At 10 partitions the time zone aware version is about 5x slower here, and up 
to 14x on a
   latest-state dedup query.
   
   `DISTINCT ON (k, ts) ... ORDER BY k, ts, ...` is planned as one 
`first_value(col ORDER BY k, ts, ...)`
   per output column, so every `DISTINCT ON` with a time zone aware key is 
affected.
   
   Found by a benchmark (digibench, a TPC-style benchmark on synthetic 
digital-economy data that
   runs the same analytical queries on several engines): a `DISTINCT ON 
(session_id, event_name, client_ts)`
   dedup query spent 92% of its DataFusion CPU in these aggregates and was 25x 
slower than DuckDB.
   
   ## To Reproduce
   
   `datafusion-cli` (release build of `main` at f058e2d), data generated in 
SQL: 2M rows, 1M groups of
   2 rows whose order in `ts` is scrambled (the second row wins in about two 
thirds of the groups);
   `ts` is `Timestamp(µs, "UTC")`, `tsn` the same instants without a time zone.
   
   ```sql
   COPY (
     SELECT value AS id,
            value / 2 AS k1,
            (value / 2) % 3 AS k2,
            (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;
   
   -- slow
   SELECT count(*), sum(a) FROM (SELECT k1, k2, first_value(v ORDER BY ts) a 
FROM t GROUP BY k1, k2);
   -- fast
   SELECT count(*), sum(a) FROM (SELECT k1, k2, first_value(v ORDER BY tsn) a 
FROM t GROUP BY k1, k2);
   
   -- same for DISTINCT ON
   SELECT count(*), sum(v) FROM (SELECT DISTINCT ON (k1) v FROM t ORDER BY k1, 
ts, o);
   SELECT count(*), sum(v) FROM (SELECT DISTINCT ON (k1) v FROM t ORDER BY k1, 
tsn, o);
   ```
   
   Median of 5 hot runs after a warm-up, ms (Apple M2 Pro, 10 cores, otherwise 
idle):
   
   | target_partitions | first_value ORDER BY ts (UTC) | ORDER BY tsn (no tz) | 
slowdown | last_value ts / tsn | DISTINCT ON ts / tsn |
   |---|---|---|---|---|---|
   | 1  | 193 | 190 | 1.0x | 191 / 190 | 266 / 264 |
   | 4  | 324 | 106 | 3.1x | 337 / 105 | 443 / 164 |
   | 10 | 491 | 98  | 5.0x | 485 / 97  | 558 / 154 |
   
   The time zone aware version gets slower with every partition added, the 
naive one faster.
   A latest-state-per-order query (10M events, `DISTINCT ON (order_id) ... 
ORDER BY order_id,
   ingest_ts DESC` with a UTC `ingest_ts`, default partitions) takes 6.8 s 
against 0.5 s with the
   same instants as a naive timestamp.
   
   ## Expected behavior
   
   Ordering by a time zone aware timestamp costs the same as ordering by a 
naive one: the time zone
   does not take part in the comparison (the values are UTC instants).
   
   Time zone aware timestamps are the common case for lakehouse data: Parquet 
written by Spark, Trino,
   DuckDB (`TIMESTAMPTZ`) or pandas with `isAdjustedToUTC = true`, Delta 
`timestamp` and Iceberg
   `timestamptz` columns all arrive as `Timestamp(_, Some("UTC"))`. "Latest row 
per key" over such
   data (`DISTINCT ON (key) ... ORDER BY key, ts DESC`, last price per 
instrument, CDC dedup) hits
   this path whenever DataFusion runs with more than one partition (the 
default).
   
   ## Additional context
   
   `FirstLastGroupsAccumulator` keeps the ordering values of each group's 
current winner as
   `orderings: Vec<Vec<ScalarValue>>`
   
([first_last.rs#L387](https://github.com/apache/datafusion/blob/f058e2d5367f761fc17ce62879339853e141f654/datafusion/functions-aggregate/src/first_last.rs#L387)).
   A `ScalarValue::Timestamp*(v, Some(tz))` holds the time zone as an 
`Arc<str>`, and that `Arc` is the
   one from the column's `DataType`, shared by every partition. Per updated 
group and batch it is cloned
   and dropped several times:
   
   - `extract_row_at_idx_to_buf` → `ScalarValue::try_from_array` clones the tz 
from the array's data type
     
([common/src/utils/mod.rs#L84-L99](https://github.com/apache/datafusion/blob/f058e2d5367f761fc17ce62879339853e141f654/datafusion/common/src/utils/mod.rs#L84-L99),
     
[scalar/mod.rs#L4379](https://github.com/apache/datafusion/blob/f058e2d5367f761fc17ce62879339853e141f654/datafusion/common/src/scalar/mod.rs#L4379)),
     and drops the previous row's values when the buffer is cleared;
   - `update_state` drops the old winner and clones the new one: `clear()` + 
`extend_from_slice()`
     
([first_last.rs#L538-L542](https://github.com/apache/datafusion/blob/f058e2d5367f761fc17ce62879339853e141f654/datafusion/functions-aggregate/src/first_last.rs#L538-L542));
   - `resize_states` clones `default_orderings` (a `Timestamp*(None, 
Some(tz))`) for every new group
     
([first_last.rs#L509-L510](https://github.com/apache/datafusion/blob/f058e2d5367f761fc17ce62879339853e141f654/datafusion/functions-aggregate/src/first_last.rs#L509-L510)),
     and all of them are dropped on emit.
   
   Each clone or drop is an atomic increment or decrement of the same reference 
count, so with N
   partitions N threads hammer one cache line. With 1M groups that is several 
million contended atomics
   per aggregation phase; the per-row work is otherwise small, so the 
contention dominates.
   
   The non-grouped `FirstValueAccumulator` / `LastValueAccumulator` (used 
without GROUP BY, or for
   value types the groups accumulator does not support) extract their ordering 
row once per
   accumulator and batch rather than once per updated group, so they are much 
less exposed.
   
   Possible fixes:
   
   1. Narrow: keep the ordering values in the accumulator without the time zone 
and put it back on the
      arrays returned by `state()`. No change to results, state schema or 
plans. I have a PR for this.
   2. Broader (follow-up): stop storing per-group orderings as `ScalarValue`s 
at all, e.g. in the
      `arrow::row` format or as typed columns. That would also remove the 
remaining per-group
      `ScalarValue` work for every ordering type and could share infrastructure 
with the coalescing work in #23682.
   
   This also matters for #23603 / #23824 (rewriting `row_number() = 1` dedup to 
`first_value`): on a time zone
   aware key the rewrite would currently route the query into this slow path.
   
   Environment: DataFusion `main` at f058e2d (2026-11-02), also reproduced on 
55.0.0
   (Python bindings), macOS 15.3.1, Apple M2 Pro (10 cores), Rust 1.99.1.
   
   ## Willingness to contribute
   
   I plan to contribute a fix for this bug independently
   


-- 
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