Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 162 additions & 23 deletions datafusion/physical-plan/src/aggregates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ use datafusion_common::{
assert_eq_or_internal_err, internal_err, not_impl_err,
};
use datafusion_execution::TaskContext;
use datafusion_execution::memory_pool::MemoryLimit;
use datafusion_expr::{Accumulator, Aggregate};
use datafusion_physical_expr::aggregate::AggregateFunctionExpr;
use datafusion_physical_expr::equivalence::ProjectionMapping;
Expand Down Expand Up @@ -1281,12 +1280,7 @@ impl AggregateExec {
&& self.group_by.is_single()
}

fn should_use_partial_reduce_hash_stream(&self, context: &TaskContext) -> bool {
// TODO: implement memory-limited path and remove this limitation
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
return false;
}

fn should_use_partial_reduce_hash_stream(&self, _context: &TaskContext) -> bool {
self.mode == AggregateMode::PartialReduce
&& self.limit_options.is_none()
&& self.input_order_mode == InputOrderMode::Linear
Expand Down Expand Up @@ -3213,7 +3207,7 @@ mod tests {

use arrow::array::{
BooleanArray, DictionaryArray, Float32Array, Float64Array, Int32Array,
Int64Array, StructArray, UInt32Array, UInt64Array,
Int64Array, NullArray, StructArray, UInt32Array, UInt64Array,
};
use arrow::compute::{SortOptions, concat_batches};
use arrow::datatypes::Int32Type;
Expand Down Expand Up @@ -4398,6 +4392,15 @@ mod tests {
}

fn partial_reduce_test_aggregate() -> Result<AggregateExec> {
partial_reduce_test_aggregate_with_batches(1)
}

/// Partial-reduce aggregate over `num_input_batches` identical input batches
/// of partial states, each reducing to groups `1, 2, 3` with sums
/// `50, 20, 30`.
fn partial_reduce_test_aggregate_with_batches(
num_input_batches: usize,
) -> Result<AggregateExec> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
Expand Down Expand Up @@ -4430,7 +4433,7 @@ mod tests {
],
)?;
let partial_reduce_input = TestMemoryExec::try_new_exec(
&[vec![partial_state_batch]],
&[vec![partial_state_batch; num_input_batches]],
Arc::clone(&partial_schema),
None,
)?;
Expand Down Expand Up @@ -4466,26 +4469,162 @@ mod tests {
Ok(())
}

/// Spilling behavior is not implemented for partial-reduce stream yet, so fall
/// back to the existing `GroupedHashAggregateStream`
/// Partial-reduce hash aggregation emits its accumulated partial states early
/// under memory pressure instead of failing, and the early-emitted states
/// still merge into the correct result.
#[tokio::test]
async fn partial_reduce_aggregate_with_memory_limit_planning() -> Result<()> {
let partial_reduce = partial_reduce_test_aggregate()?;
async fn partial_reduce_aggregate_with_memory_limit_emits_early() -> Result<()> {
let num_input_batches = 3;
let partial_reduce =
partial_reduce_test_aggregate_with_batches(num_input_batches)?;
let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(1, 1.0)
.build_arc()?;
let task_ctx =
Arc::new(
TaskContext::default()
.with_session_config(SessionConfig::new().set_bool(
"datafusion.execution.enable_migration_aggregate",
true,
))
.with_runtime(runtime),
);
// A batch size smaller than the number of flushed groups also covers
// splitting one flush across several output batches.
let batch_size = 2;
let task_ctx = Arc::new(
TaskContext::default()
.with_session_config(migrated_hash_session_config(batch_size))
.with_runtime(runtime),
);

let stream = partial_reduce.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::GroupedHash(_)));
assert!(matches!(stream, StreamType::PartialReduceHash(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;

// The table is flushed after every input batch, so each of the three
// groups is emitted once per input batch instead of being merged into a
// single row. Each flush is sliced into batches of 2 and 1 rows.
assert_eq!(output.len(), 2 * num_input_batches);
assert_snapshot!(batches_to_string(&output), @r"
+---+-------------+
| a | SUM(b)[sum] |
+---+-------------+
| 1 | 50.0 |
| 2 | 20.0 |
| 3 | 30.0 |
| 1 | 50.0 |
| 2 | 20.0 |
| 3 | 30.0 |
| 1 | 50.0 |
| 2 | 20.0 |
| 3 | 30.0 |
+---+-------------+
");

Ok(())
}

/// Same shape as [`partial_reduce_test_aggregate_with_batches`], but with multiple
/// group keys.
fn partial_reduce_test_aggregate_rows_multi_group_keys(
num_input_batches: usize,
) -> Result<AggregateExec> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("n", DataType::Null, true),
Field::new("b", DataType::Float64, false),
]));
let group_by = PhysicalGroupBy::new_single(vec![
(col("a", &schema)?, "a".to_string()),
(col("n", &schema)?, "n".to_string()),
]);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("SUM(b)")
.build()?,
)];

let empty_input =
TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema), None)?;
let partial = AggregateExec::try_new(
AggregateMode::Partial,
group_by.clone(),
aggregates.clone(),
vec![None],
empty_input,
Arc::clone(&schema),
)?;
let partial_schema = partial.schema();
let partial_state_batch = RecordBatch::try_new(
Arc::clone(&partial_schema),
vec![
Arc::new(UInt32Array::from(vec![1, 2, 1, 3])),
Arc::new(NullArray::new(4)),
Arc::new(Float64Array::from(vec![10.0, 20.0, 40.0, 30.0])),
],
)?;
let partial_reduce_input = TestMemoryExec::try_new_exec(
&[vec![partial_state_batch; num_input_batches]],
Arc::clone(&partial_schema),
None,
)?;

AggregateExec::try_new(
AggregateMode::PartialReduce,
group_by,
aggregates,
vec![None],
partial_reduce_input,
partial_schema,
)
}

#[tokio::test]
async fn partial_reduce_aggregate_with_memory_limit_emits_early_multi_group_keys()
-> Result<()> {
let num_input_batches = 3;
let partial_reduce =
partial_reduce_test_aggregate_rows_multi_group_keys(num_input_batches)?;

// Pin the representation: this is exactly the condition
// `new_group_values` uses to pick `GroupValuesRows` over
// `GroupValuesColumn`. If a `Null` `GroupColumn` is ever added, this
// assertion fires and the test stops covering the row-encoded path.
let group_schema = partial_reduce
.group_by
.group_schema(&partial_reduce.schema())?;
assert!(
!group_values::multi_group_by::supported_schema(&group_schema),
"expected the Null group column to force the GroupValuesRows fallback"
);

let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(1, 1.0)
.build_arc()?;
let batch_size = 2;
let task_ctx = Arc::new(
TaskContext::default()
.with_session_config(migrated_hash_session_config(batch_size))
.with_runtime(runtime),
);

let stream = partial_reduce.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::PartialReduceHash(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;

// Same flush cadence as the column-backed test: one flush per input
// batch, each sliced into batches of 2 and 1 rows.
assert_eq!(output.len(), 2 * num_input_batches);
assert_snapshot!(batches_to_string(&output), @r"
+---+---+-------------+
| a | n | SUM(b)[sum] |
+---+---+-------------+
| 1 | | 50.0 |
| 2 | | 20.0 |
| 3 | | 30.0 |
| 1 | | 50.0 |
| 2 | | 20.0 |
| 3 | | 30.0 |
| 1 | | 50.0 |
| 2 | | 20.0 |
| 3 | | 30.0 |
+---+---+-------------+
");

Ok(())
}
Expand Down
Loading