Skip to content

Commit 02636dd

Browse files
committed
add spilling for single mode aggregation
1 parent 88365dd commit 02636dd

9 files changed

Lines changed: 662 additions & 184 deletions

File tree

datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ pub(in crate::aggregates) struct AggregateHashTable<AggrMode> {
8282
/// Output schema: group columns followed by aggregate state or final values.
8383
pub(super) output_schema: SchemaRef,
8484

85+
/// Intermediate-state schema used when memory pressure requires the table
86+
/// to spill its current state.
87+
pub(super) state_schema: SchemaRef,
88+
8589
/// Maximum rows per emitted output batch, from config `batch_size`.
8690
pub(super) batch_size: usize,
8791

@@ -97,6 +101,7 @@ impl<AggrMode> AggregateHashTable<AggrMode> {
97101
agg: &AggregateExec,
98102
partition: usize,
99103
output_schema: SchemaRef,
104+
state_schema: SchemaRef,
100105
batch_size: usize,
101106
filters: Vec<Option<Arc<dyn PhysicalExpr>>>,
102107
) -> Result<Self> {
@@ -133,6 +138,7 @@ impl<AggrMode> AggregateHashTable<AggrMode> {
133138
group_by_metrics: GroupByMetrics::new(&agg.metrics, partition),
134139
input_schema,
135140
output_schema,
141+
state_schema,
136142
batch_size,
137143
state: AggregateHashTableState::Building(AggregateHashTableBuffer {
138144
group_by: Arc::clone(&agg.group_by),
@@ -282,11 +288,48 @@ impl<AggrMode> AggregateHashTable<AggrMode> {
282288
}
283289
}
284290

291+
pub(in crate::aggregates) fn group_by_metrics(&self) -> GroupByMetrics {
292+
self.group_by_metrics.clone()
293+
}
294+
285295
/// Returns the number of distinct groups accumulated so far.
286296
pub(in crate::aggregates) fn building_group_count(&self) -> usize {
287297
self.state.building().group_values.len()
288298
}
289299

300+
/// Takes every intermediate aggregate state and resets the table so it can
301+
/// continue accumulating raw input.
302+
///
303+
/// Unlike normal single aggregation output, this materializes intermediate
304+
/// states rather than final values. The states can therefore be merged after
305+
/// spilling without finalizing the same group more than once.
306+
pub(in crate::aggregates) fn take_state_batch(
307+
&mut self,
308+
) -> Result<Option<RecordBatch>> {
309+
let state_schema = Arc::clone(&self.state_schema);
310+
let state = self.state.building_mut();
311+
if state.group_values.is_empty() {
312+
return Ok(None);
313+
}
314+
315+
let mut output = state.group_values.emit(EmitTo::All)?;
316+
for acc in &mut state.accumulators {
317+
output.extend(acc.state(EmitTo::All)?);
318+
}
319+
320+
let batch = RecordBatch::try_new(state_schema, output)?;
321+
debug_assert!(batch.num_rows() > 0);
322+
323+
// `emit(EmitTo::All)` resets accumulator state. Explicitly shrink the
324+
// key/index buffers too so the memory reservation can be released
325+
// before the batch is sorted for spilling.
326+
state.group_values.clear_shrink(0);
327+
state.batch_group_indices.clear();
328+
state.batch_group_indices.shrink_to_fit();
329+
330+
Ok(Some(batch))
331+
}
332+
290333
pub(in crate::aggregates) fn is_building(&self) -> bool {
291334
matches!(self.state, AggregateHashTableState::Building(_))
292335
}

datafusion/physical-plan/src/aggregates/aggregate_hash_table/final_table.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use std::sync::Arc;
19+
1820
use arrow::datatypes::SchemaRef;
1921
use arrow::record_batch::RecordBatch;
2022
use datafusion_common::Result;
@@ -41,6 +43,7 @@ impl AggregateHashTable<FinalMarker> {
4143
agg,
4244
partition,
4345
output_schema,
46+
Arc::clone(&agg.input().schema()),
4447
batch_size,
4548
vec![None; agg.aggr_expr.len()],
4649
)

datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_reduce_table.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use std::sync::Arc;
19+
1820
use arrow::datatypes::SchemaRef;
1921
use arrow::record_batch::RecordBatch;
2022
use datafusion_common::Result;
@@ -34,6 +36,7 @@ impl AggregateHashTable<PartialReduceMarker> {
3436
Self::new_with_filters(
3537
agg,
3638
partition,
39+
Arc::clone(&output_schema),
3740
output_schema,
3841
batch_size,
3942
vec![None; agg.aggr_expr.len()],

datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_table.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ impl AggregateHashTable<PartialMarker> {
5050
Self::new_with_filters(
5151
agg,
5252
partition,
53+
Arc::clone(&output_schema),
5354
output_schema,
5455
batch_size,
5556
agg.filter_expr.iter().cloned().collect(),
@@ -87,6 +88,7 @@ impl AggregateHashTable<PartialMarker> {
8788
group_by_metrics: self.group_by_metrics.clone(),
8889
input_schema: Arc::clone(&self.input_schema),
8990
output_schema: Arc::clone(&self.output_schema),
91+
state_schema: Arc::clone(&self.state_schema),
9092
batch_size: self.batch_size,
9193
state: AggregateHashTableState::Building(AggregateHashTableBuffer {
9294
group_by: Arc::clone(&state.group_by),

datafusion/physical-plan/src/aggregates/aggregate_hash_table/single_table.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ impl AggregateHashTable<SingleMarker> {
3535
agg: &AggregateExec,
3636
partition: usize,
3737
output_schema: SchemaRef,
38+
state_schema: SchemaRef,
3839
batch_size: usize,
3940
) -> Result<Self> {
4041
Self::new_with_filters(
4142
agg,
4243
partition,
4344
output_schema,
45+
state_schema,
4446
batch_size,
4547
agg.filter_expr.iter().cloned().collect(),
4648
)

datafusion/physical-plan/src/aggregates/mod.rs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,12 +1274,7 @@ impl AggregateExec {
12741274
&& self.group_by.is_single()
12751275
}
12761276

1277-
fn should_use_single_hash_stream(&self, context: &TaskContext) -> bool {
1278-
// TODO: implement memory-limited path and remove this limitation
1279-
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
1280-
return false;
1281-
}
1282-
1277+
fn should_use_single_hash_stream(&self, _context: &TaskContext) -> bool {
12831278
matches!(
12841279
self.mode,
12851280
AggregateMode::Single | AggregateMode::SinglePartitioned
@@ -3767,7 +3762,7 @@ mod tests {
37673762
let aggregates_v0: Vec<Arc<AggregateFunctionExpr>> =
37683763
vec![Arc::new(test_median_agg_expr(Arc::clone(&input_schema))?)];
37693764

3770-
// use fast-path in `grouped_hash_stream.rs`.
3765+
// Use the fast path in `single_stream.rs`.
37713766
let aggregates_v2: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
37723767
AggregateExprBuilder::new(avg_udaf(), vec![col("b", &input_schema)?])
37733768
.schema(Arc::clone(&input_schema))
@@ -3800,7 +3795,7 @@ mod tests {
38003795
assert!(matches!(stream, StreamType::GroupedHash(_)));
38013796
}
38023797
2 => {
3803-
assert!(matches!(stream, StreamType::GroupedHash(_)));
3798+
assert!(matches!(stream, StreamType::SingleHash(_)));
38043799
}
38053800
_ => panic!("Unknown version: {version}"),
38063801
}
@@ -4135,15 +4130,14 @@ mod tests {
41354130
Ok(())
41364131
}
41374132

4138-
/// Spilling behavior is not implemented for single hash stream yet, so fall
4139-
/// back to the existing `GroupedHashAggregateStream`.
4133+
/// Single hash aggregation supports finite memory.
41404134
#[tokio::test]
41414135
async fn single_aggregate_with_memory_limit_planning() -> Result<()> {
41424136
let single = single_test_aggregate()?;
41434137
let task_ctx = new_finite_memory_migrated_hash_ctx(2, 1024 * 1024)?;
41444138

41454139
let stream = single.execute_typed(0, &task_ctx)?;
4146-
assert!(matches!(stream, StreamType::GroupedHash(_)));
4140+
assert!(matches!(stream, StreamType::SingleHash(_)));
41474141

41484142
Ok(())
41494143
}
@@ -5664,9 +5658,11 @@ mod tests {
56645658
Field::new("b", DataType::Float64, false),
56655659
]));
56665660

5661+
let group_keys = [2, 3, 4, 4].repeat(1_000);
5662+
let values = [1.0, 2.0, 3.0, 4.0].repeat(1_000);
56675663
let batches = vec![
5668-
create_record_batch(&schema, (vec![2, 3, 4, 4], vec![1.0, 2.0, 3.0, 4.0]))?,
5669-
create_record_batch(&schema, (vec![2, 3, 4, 4], vec![1.0, 2.0, 3.0, 4.0]))?,
5664+
create_record_batch(&schema, (group_keys.clone(), values.clone()))?,
5665+
create_record_batch(&schema, (group_keys, values))?,
56705666
];
56715667
let plan: Arc<dyn ExecutionPlan> =
56725668
TestMemoryExec::try_new_exec(&[batches], Arc::clone(&schema), None)?;
@@ -5766,9 +5762,9 @@ mod tests {
57665762
#[tokio::test]
57675763
async fn test_aggregate_with_spill_if_necessary() -> Result<()> {
57685764
// test with spill
5769-
run_test_with_spill_pool_if_necessary(2_000, true).await?;
5765+
run_test_with_spill_pool_if_necessary(20_000, true).await?;
57705766
// test without spill
5771-
run_test_with_spill_pool_if_necessary(20_000, false).await?;
5767+
run_test_with_spill_pool_if_necessary(200_000, false).await?;
57725768
Ok(())
57735769
}
57745770

@@ -6744,11 +6740,6 @@ mod tests {
67446740
matches!(root, DataFusionError::ResourcesExhausted(_)),
67456741
"Expected ResourcesExhausted, got: {root}",
67466742
);
6747-
let msg = root.to_string();
6748-
assert!(
6749-
msg.contains("Failed to reserve memory for sort during spill"),
6750-
"Expected sort reservation error, got: {msg}",
6751-
);
67526743
}
67536744
}
67546745

datafusion/physical-plan/src/aggregates/ordered_final_stream.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ struct OrderedFinalSpillContext {
9797
enum OrderedFinalAggregateState {
9898
ReadingInput {
9999
table: OrderedAggregateTable<FinalMarker>,
100+
/// None if either
101+
/// - Disk Manager doesn't enable temporary file creation
102+
/// - The group keys are fully ordered, it's expected to use bounded memory
100103
spill_context: Option<Box<OrderedFinalSpillContext>>,
101104
},
102105
Spilling {
@@ -292,7 +295,7 @@ impl OrderedFinalAggregateStream {
292295
clippy::too_many_arguments,
293296
reason = "keeps replay metric reuse explicit"
294297
)]
295-
fn new_with_input_and_metrics(
298+
pub(in crate::aggregates) fn new_with_input_and_metrics(
296299
agg: &AggregateExec,
297300
context: &Arc<TaskContext>,
298301
partition: usize,
@@ -440,6 +443,8 @@ impl OrderedFinalAggregateStream {
440443
Ok(()) => {}
441444
Err(e @ DataFusionError::ResourcesExhausted(_)) => {
442445
let Some(spill_context) = spill_context else {
446+
// `None` means spilling is not supported, see comments
447+
// at `OrderedFinalAggregateState` for details.
443448
return ControlFlow::Break((
444449
Poll::Ready(Some(Err(e))),
445450
OrderedFinalAggregateState::Done,

0 commit comments

Comments
 (0)