Repository navigation
perf: drain a large ordered aggregate range once instead of in batch-size steps - #18
Merged
Merged
Conversation
…size steps An aggregate over input sorted on a prefix of its grouping keys runs in DataFusion's ordered partial or ordered final stream. Groups become ready only when the sorted prefix changes or the input ends, and the ready range was then emitted with EmitTo::First(batch_size) per output batch. Each such emit walks the whole group hash table and copies every remaining key and state, so a range of N groups took O(N^2 / batch_size). COUNT(DISTINCT) after a window partitioned by the same prefix hits this: on push_ranking_results one window partition held ~30M groups and its Partial and PartialMerge aggregates took ~830 s each where Spark needs 89 s for the whole query. When a ready range is larger than batch_size and the stream outputs states (Partial and PartialReduce), emit the range once into a materialized batch and hand it out in batch_size slices, as the unordered hash streams already do. The undelivered slices count towards the table's memory and emptiness, and are prepended to the state batch taken under memory pressure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Collaborator
Author
GCP measurement (push_ranking_results query, same input 2026-10-10..11, 10 executors x 8 cores, c4a-lssd, prod thrift Comet config)
The original model is nondeterministic: Local: vendor datafusion-physical-plan 1769/0; rust workspace 1929/0; CometAggregateSuite + CometWindowExecSuite + CometPartitionAggregateWindowSuite 301/0 (2 canceled, pre-existing); agg fuzz full 31/31; fmt/clippy clean. 🤖 Generated with Claude Code |
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.
Problem
push_ranking_results(prod thrift dbt) runs ~5x slower on Comet than on Spark, and slower every night as its input grows: 1473 s on 1.1-8 vs a vanilla median of 305 s. Reproduced on GCP with the same input (10 executors, c4a-lssd):window.partitionAggregate.enabled=falseSo it is not a 1.1-8 regression. Per-operator metrics: the time is in the
COUNT(DISTINCT ranking_id)hash aggregates: Partial ~829 s and PartialMerge ~813 s, both in one task holding the largest window partition (~30M groups).Root cause
The aggregates follow
MAX(...) OVER (PARTITION BY partition_date, winner_type), so their input is sorted on a prefix of the grouping keys and DataFusion runs them in the ordered partial / ordered final streams. Groups become ready only when the sorted prefix changes or input ends. The ready range was then emitted withEmitTo::First(batch_size)per output batch. Each such emit walks the whole group hash table (map.retain) and copies every remaining key and accumulator state, so draining N groups costs O(N² / batch_size).Change
aggregate_hash_table/common_ordered.rsin vendoreddatafusion-physical-plan: when the ready range is larger thanbatch_sizeand the stream outputs states (Partial, PartialReduce), emit the range once into a materialized batch and hand it out inbatch_sizeslices, as the unordered hash streams already do (MaterializedAggregateOutput). The undelivered slices count towards the table's memory and emptiness. Under memory pressure they are prepended to the state batch. Final-value output (is_final) keeps the old path.Tests
ordered_drain_tests.rs(new):CometAggregateSuite: COUNT(DISTINCT) over a window-sorted input with one huge ordered key range,checkSparkAnswerAndOperatorwith batch size 64.GCP measurement with a test image and a data compare follow in a comment.
🤖 Generated with Claude Code