Skip to content

Commit b0643e2

Browse files
committed
feat: stream aggregates over grouped input
1 parent 4b4cf23 commit b0643e2

1 file changed

Lines changed: 104 additions & 16 deletions

File tree

  • datafusion/physical-plan/src/aggregates

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

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,14 @@ impl AggregateExec {
10641064
input_order_mode = InputOrderMode::Linear;
10651065
}
10661066

1067-
let group_completion_mode = GroupCompletionMode::from(&input_order_mode);
1067+
let group_completion_mode = if !group_by.has_grouping_set()
1068+
&& !groupby_exprs.is_empty()
1069+
&& input_eq_properties.grouping_satisfy(groupby_exprs.iter().cloned())?
1070+
{
1071+
GroupCompletionMode::Full
1072+
} else {
1073+
GroupCompletionMode::from(&input_order_mode)
1074+
};
10681075

10691076
// construct a map from the input expression to the output expression of the Aggregation group by
10701077
let group_expr_mapping =
@@ -1083,6 +1090,16 @@ impl AggregateExec {
10831090
aggr_expr.as_ref(),
10841091
)?
10851092
};
1093+
// `compute_properties` derives emission from the public input-order
1094+
// mode. Override it only for the new case where unsorted input still
1095+
// has a group-completion guarantee.
1096+
let cache = if input_order_mode == InputOrderMode::Linear
1097+
&& group_completion_mode != GroupCompletionMode::None
1098+
{
1099+
cache.with_emission_type(input.pipeline_behavior())
1100+
} else {
1101+
cache
1102+
};
10861103

10871104
let mut exec = AggregateExec {
10881105
mode,
@@ -1436,6 +1453,10 @@ impl AggregateExec {
14361453
let mut eq_properties = input
14371454
.equivalence_properties()
14381455
.project(group_expr_mapping, schema);
1456+
// Grouping information is consumed by this aggregate. The aggregate
1457+
// output may have a different row layout, so do not pass explicit
1458+
// input grouping assertions to another aggregate.
1459+
eq_properties.clear_groupings();
14391460

14401461
// True no-group aggregates produce only one row in each output
14411462
// partition, so aggregate outputs are constants within the partition.
@@ -2374,7 +2395,7 @@ impl ExecutionPlan for AggregateExec {
23742395
required_input_ordering: _,
23752396
// Derived at construction from the input ordering and `group_by`.
23762397
input_order_mode: _,
2377-
// Derived at construction from `input_order_mode`.
2398+
// Derived at construction from the input properties and `group_by`.
23782399
group_completion_mode: _,
23792400
// Derived at construction by `Self::compute_properties`.
23802401
cache: _,
@@ -3266,7 +3287,7 @@ mod tests {
32663287
Int64Array, NullArray, StructArray, UInt32Array, UInt64Array,
32673288
};
32683289
use arrow::compute::{SortOptions, concat_batches};
3269-
use arrow::datatypes::Int32Type;
3290+
use arrow::datatypes::{Int32Type, TimeUnit};
32703291
use datafusion_common::test_util::{batches_to_sort_string, batches_to_string};
32713292
use datafusion_common::{DataFusionError, internal_err};
32723293
use datafusion_execution::config::SessionConfig;
@@ -3277,6 +3298,7 @@ mod tests {
32773298
Accumulator, AggregateUDF, AggregateUDFImpl, EmitTo, GroupsAccumulator,
32783299
Signature, Volatility,
32793300
};
3301+
use datafusion_functions::datetime::date_bin;
32803302
use datafusion_functions_aggregate::approx_percentile_cont::approx_percentile_cont_udaf;
32813303
use datafusion_functions_aggregate::array_agg::array_agg_udaf;
32823304
use datafusion_functions_aggregate::average::avg_udaf;
@@ -3285,10 +3307,9 @@ mod tests {
32853307
use datafusion_functions_aggregate::median::median_udaf;
32863308
use datafusion_functions_aggregate::min_max::min_udaf;
32873309
use datafusion_functions_aggregate::sum::sum_udaf;
3288-
use datafusion_physical_expr::Partitioning;
3289-
use datafusion_physical_expr::PhysicalSortExpr;
32903310
use datafusion_physical_expr::aggregate::AggregateExprBuilder;
32913311
use datafusion_physical_expr::expressions::{Literal, NotExpr};
3312+
use datafusion_physical_expr::{Partitioning, PhysicalSortExpr, ScalarFunctionExpr};
32923313

32933314
use crate::projection::ProjectionExec;
32943315
use crate::repartition::RepartitionExec;
@@ -4852,7 +4873,7 @@ mod tests {
48524873
}
48534874

48544875
#[tokio::test]
4855-
async fn unsorted_contiguous_groups_use_final_emission() -> Result<()> {
4876+
async fn unsorted_contiguous_groups_use_incremental_emission() -> Result<()> {
48564877
let schema = Arc::new(Schema::new(vec![
48574878
Field::new("key", DataType::Int32, false),
48584879
Field::new("time_bin", DataType::Int64, false),
@@ -4880,18 +4901,21 @@ mod tests {
48804901
],
48814902
)?,
48824903
];
4904+
let key = col("key", &schema)?;
4905+
let time_bin = col("time_bin", &schema)?;
48834906
let group_by = PhysicalGroupBy::new_single(vec![
4884-
(col("key", &schema)?, "key".to_string()),
4885-
(col("time_bin", &schema)?, "time_bin".to_string()),
4907+
(Arc::clone(&key), "key".to_string()),
4908+
(Arc::clone(&time_bin), "time_bin".to_string()),
48864909
]);
48874910
let aggr_expr = Arc::new(
48884911
AggregateExprBuilder::new(sum_udaf(), vec![col("value", &schema)?])
48894912
.schema(Arc::clone(&schema))
48904913
.alias("SUM(value)")
48914914
.build()?,
48924915
);
4893-
let input: Arc<dyn ExecutionPlan> =
4894-
TestMemoryExec::try_new_exec(&[input_batches], Arc::clone(&schema), None)?;
4916+
let input = TestMemoryExec::try_new(&[input_batches], Arc::clone(&schema), None)?
4917+
.try_with_grouping_information(vec![vec![key, time_bin]])?;
4918+
let input: Arc<dyn ExecutionPlan> = Arc::new(input);
48954919
assert_eq!(input.output_partitioning().partition_count(), 1);
48964920

48974921
let aggregate = AggregateExec::try_new(
@@ -4904,15 +4928,20 @@ mod tests {
49044928
)?;
49054929

49064930
assert_eq!(aggregate.input_order_mode(), &InputOrderMode::Linear);
4907-
assert_eq!(aggregate.group_completion_mode, GroupCompletionMode::None);
4908-
// This captures the behavior before #24438. When the source can declare
4909-
// `(key, time_bin)` group-contiguous, the corresponding case can use
4910-
// `EmissionType::Incremental`.
4911-
assert_eq!(aggregate.cache().emission_type, EmissionType::Final);
4931+
assert_eq!(aggregate.group_completion_mode, GroupCompletionMode::Full);
4932+
assert_eq!(aggregate.cache().emission_type, EmissionType::Incremental);
4933+
assert!(
4934+
aggregate
4935+
.cache()
4936+
.equivalence_properties()
4937+
.geq_class()
4938+
.is_empty()
4939+
);
4940+
assert!(aggregate.cache().output_ordering().is_none());
49124941

49134942
let task_ctx = new_migrated_hash_ctx(1024);
49144943
let stream = aggregate.execute_typed(0, &task_ctx)?;
4915-
assert!(matches!(stream, StreamType::SingleHash(_)));
4944+
assert!(matches!(stream, StreamType::OrderedSingleAggregate(_)));
49164945
let stream: SendableRecordBatchStream = stream.into();
49174946
let output = collect(stream).await?;
49184947
assert_snapshot!(batches_to_sort_string(&output), @r"
@@ -4929,6 +4958,65 @@ mod tests {
49294958
Ok(())
49304959
}
49314960

4961+
#[test]
4962+
fn grouped_date_bin_projects_to_aggregate() -> Result<()> {
4963+
let schema = Arc::new(Schema::new(vec![
4964+
Field::new("key", DataType::Int32, false),
4965+
Field::new("time", DataType::Timestamp(TimeUnit::Second, None), false),
4966+
]));
4967+
let time_bin_expr = || -> Result<Arc<dyn PhysicalExpr>> {
4968+
Ok(Arc::new(ScalarFunctionExpr::try_new(
4969+
date_bin(),
4970+
vec![
4971+
lit(ScalarValue::new_interval_dt(0, 10_000)),
4972+
col("time", &schema)?,
4973+
],
4974+
&schema,
4975+
Arc::new(ConfigOptions::default()),
4976+
)?))
4977+
};
4978+
let input = TestMemoryExec::try_new(&[vec![]], Arc::clone(&schema), None)?
4979+
.try_with_grouping_information(vec![vec![
4980+
col("key", &schema)?,
4981+
time_bin_expr()?,
4982+
]])?;
4983+
let projection = ProjectionExec::try_new(
4984+
[
4985+
ProjectionExpr::new(col("key", &schema)?, "key"),
4986+
// Build this expression independently from the source
4987+
// assertion so the test exercises semantic expression matching.
4988+
ProjectionExpr::new(time_bin_expr()?, "time_bin"),
4989+
],
4990+
Arc::new(input),
4991+
)?;
4992+
4993+
let projected_schema = projection.schema();
4994+
let key = col("key", &projected_schema)?;
4995+
let time_bin = col("time_bin", &projected_schema)?;
4996+
assert!(
4997+
projection
4998+
.properties()
4999+
.equivalence_properties()
5000+
.grouping_satisfy([Arc::clone(&key), Arc::clone(&time_bin)])?
5001+
);
5002+
let aggregate = AggregateExec::try_new(
5003+
AggregateMode::Single,
5004+
PhysicalGroupBy::new_single(vec![
5005+
(key, "key".to_string()),
5006+
(time_bin, "time_bin".to_string()),
5007+
]),
5008+
vec![],
5009+
vec![],
5010+
Arc::new(projection),
5011+
projected_schema,
5012+
)?;
5013+
5014+
assert_eq!(aggregate.input_order_mode, InputOrderMode::Linear);
5015+
assert_eq!(aggregate.group_completion_mode, GroupCompletionMode::Full);
5016+
assert_eq!(aggregate.cache().emission_type, EmissionType::Incremental);
5017+
Ok(())
5018+
}
5019+
49325020
/// Ensures for ordered input, `OrderedPartialAggregateStream` is used.
49335021
#[tokio::test]
49345022
async fn ordered_partial_aggregate_planning() -> Result<()> {

0 commit comments

Comments
 (0)