Repository navigation
perf: avoid Arc<str> refcount contention in grouped first_value/last_value ordered by a time zone aware timestamp - #27047
Open
akupchinskiy wants to merge 1 commit into
Conversation
…value ordered by a time zone aware timestamp FirstLastGroupsAccumulator stores each group's winning ordering values as ScalarValues. A time zone aware ScalarValue::Timestamp* holds the time zone as an Arc<str> shared by all partitions, so cloning and dropping it per updated group made the threads contend on its reference count: the query got slower with more partitions (5x slower than a naive timestamp at 10). The time zone does not take part in the comparison, so strip it from the ordering columns on input (a relabel, not a cast) and put it back on the arrays returned by state(). Closes apache#27046.
akupchinskiy
marked this pull request as ready for review
November 3, 2026 09:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Rationale for this change
A grouped
first_value(x ORDER BY ts)/last_value(x ORDER BY ts)withtsaTimestamp(_, Some(tz))gets slower astarget_partitionsgrows, and so does everyDISTINCT ONwith a time zone aware key (it is planned asfirst_valueper column). At 10partitions it is 3.5-14x slower than the same query ordered by a naive timestamp. We found it with
a benchmark (digibench) where a
DISTINCT ON (session_id, event_name, client_ts)dedup was 25xslower than DuckDB.
The cause is in
FirstLastGroupsAccumulator: it keeps each group's winning ordering values asVec<Vec<ScalarValue>>. A time zone awareScalarValue::Timestamp*holds its time zone as anArc<str>, and thatArccomes from the column'sDataType, so it is shared by all partitions.Extracting a winner row, replacing a group's orderings (
clear()+extend_from_slice()), andgrowing the groups (
resize(n, default_orderings.clone())) each clone or drop it, i.e. severalatomic operations on one cache line per updated group, from every thread at once.
The time zone does not take part in the comparison (the values are UTC instants), so the
accumulator does not need it.
What changes are included in this PR?
In
FirstLastGroupsAccumulator(datafusion/functions-aggregate/src/first_last.rs):try_newrecords the time zone of each ordering column (ordering_timezones) and buildsdefault_orderingswithout it.update_batchandmerge_batchrelabel the time zone aware ordering columns of their input astime-zone-free before anything else (
PrimitiveArray::with_timezone_opt(None): no copy, no valuechange, unlike a cast). From there on the stored
ScalarValues carryNoneand cloning them is aplain copy. When no ordering column has a time zone the input is passed through unchanged
(
Cow::Borrowed).stateputs the original time zone back on the ordering columns it emits, so the intermediatestate schema is unchanged.
Results, the state schema, plans and the non-timestamp paths are unchanged. Size accounting now
reports slightly less per stored ordering value, because a time-zone-free
ScalarValue::size()no longer counts the (shared) time zone string.
Not changed: the non-grouped
FirstValueAccumulator/LastValueAccumulatorextract theirordering row once per accumulator and batch rather than once per updated group, so they are much
less exposed;
nth_valuekeepsScalarValueordering rows too and may have a milder form of thesame contention; ordered
array_aggalready stores encoded rows. The general fix, keepingper-group orderings in the
arrow::rowformat or as typed columns instead ofScalarValues(which would also remove the remaining per-group
ScalarValuework for every ordering type, andcover time zone aware timestamps nested in structs), is a larger refactor and is left as a
follow-up.
What is the testing strategy for this PR?
test_group_acc_timezone_ordering(first and last): the stored orderings carry no timezone,
size_of_orderingsstays consistent, thestate()ordering column has the originalTimestamp(µs, "+02:00")type also when the state is emitted in parts (EmitTo::First, thenEmitTo::All), and merging those states with another partial state gives the right values.aggregate.slt:first_value/last_valueordered by aTimestamp(µs, "+05:00")column over aUNION ALLof two tables (multi-partition, partial state merged in the final stage), with NULLordering values (
NULLS FIRST/NULLS LAST), ties broken by a second key, the time zone awarecolumn as the second ordering key,
DESC, the time zone aware value itself as the result(
arrow_typeofkeeps the time zone, displayed values are not shifted),DISTINCT ONwith atime zone aware key, and second, millisecond and nanosecond timestamps with three different time
zones as ordering keys, with and without
IGNORE NULLS.first_lastunit tests and theaggregate,first_last_*,distinct_onandordered_aggregate_*sqllogictests pass.Benchmark (
datafusion-cli --release, 2M rows in 1M groups of 2 rows whose order intsisscrambled, data generated in SQL; queries in the issue). Median of 5 hot runs after a warm-up, ms,
two interleaved rounds (main, branch, main, branch) shown as
round 1 / round 2, Apple M2 Pro,10 cores, otherwise idle:
Timestamp(µs, "UTC"))first_value(v ORDER BY ts)GROUP BY k1, k2last_value(v ORDER BY ts)GROUP BY k1, k2DISTINCT ON (k1) ... ORDER BY k1, ts, oWith this PR the time zone aware queries run at the speed of the same queries ordered by a naive
timestamp (10 partitions: 97-99 ms for
first_value/last_value, 153-161 ms forDISTINCT ON,on both builds), i.e. about 5x faster for
first_value/last_valueand 3.6x forDISTINCT ONat10 partitions and unchanged at 1. A latest-state-per-order query (10M events, 2M orders,
DISTINCT ON (order_id) ... ORDER BY order_id, ingest_ts DESC, default partitions) goes from6.77-6.92 s to 0.47-0.52 s (about 14x), the same as with a naive
ingest_ts(0.50-0.54 s). Queryresults are identical between main and the branch (and, for the latest-state query, with DuckDB).
Are there any user-facing changes?
No, only performance.