use arrow::record_batch::RecordBatch;
use crate::metrics;
pub(super) struct SkipAggregationProbe {
probe_rows_threshold: usize,
probe_ratio_threshold: f64,
input_rows: usize,
num_groups: usize,
should_skip: bool,
is_locked: bool,
skipped_aggregation_rows: metrics::Count,
}
impl SkipAggregationProbe {
pub(super) fn new(
probe_rows_threshold: usize,
probe_ratio_threshold: f64,
skipped_aggregation_rows: metrics::Count,
) -> Self {
Self {
input_rows: 0,
num_groups: 0,
probe_rows_threshold,
probe_ratio_threshold,
should_skip: false,
is_locked: false,
skipped_aggregation_rows,
}
}
pub(super) fn update_state(&mut self, input_rows: usize, num_groups: usize) {
if self.is_locked {
return;
}
self.input_rows += input_rows;
self.num_groups = num_groups;
if self.input_rows >= self.probe_rows_threshold {
self.should_skip = self.num_groups as f64 / self.input_rows as f64
> self.probe_ratio_threshold;
self.is_locked = self.should_skip;
}
}
pub(super) fn should_skip(&self) -> bool {
self.should_skip
}
pub(super) fn record_skipped(&mut self, batch: &RecordBatch) {
self.skipped_aggregation_rows.add(batch.num_rows());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aggregates::grouped_hash_stream::GroupedHashAggregateStream;
use crate::aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy};
use crate::execution_plan::ExecutionPlan;
use crate::test::TestMemoryExec;
use std::sync::Arc;
use arrow::array::Int32Array;
use arrow::datatypes::{DataType, Field, Schema};
use datafusion_common::Result;
use datafusion_execution::TaskContext;
use datafusion_execution::runtime_env::RuntimeEnvBuilder;
use datafusion_functions_aggregate::count::count_udaf;
use datafusion_physical_expr::aggregate::AggregateExprBuilder;
use datafusion_physical_expr::expressions::col;
use futures::StreamExt;
#[tokio::test]
async fn test_skip_aggregation_probe_not_locked_until_skip() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("group_col", DataType::Int32, false),
Field::new("value_col", DataType::Int32, false),
]));
let probe_rows_threshold = 100;
let probe_ratio_threshold = 0.8;
let batch1_rows = 100;
let batch1_groups = 10;
let mut group_ids_batch1 = Vec::new();
for i in 0..batch1_rows {
group_ids_batch1.push((i % batch1_groups) as i32);
}
let values_batch1: Vec<i32> = vec![1; batch1_rows];
let batch1 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(group_ids_batch1)),
Arc::new(Int32Array::from(values_batch1)),
],
)?;
let batch2_rows = 360;
let batch2_groups = 360;
let group_ids_batch2: Vec<i32> = (batch1_groups..(batch1_groups + batch2_groups))
.map(|x| x as i32)
.collect();
let values_batch2: Vec<i32> = vec![1; batch2_rows];
let batch2 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(group_ids_batch2)),
Arc::new(Int32Array::from(values_batch2)),
],
)?;
let batch3_rows = 100;
let batch3_groups = 100;
let batch3_start_group = batch1_groups + batch2_groups;
let group_ids_batch3: Vec<i32> = (batch3_start_group
..(batch3_start_group + batch3_groups))
.map(|x| x as i32)
.collect();
let values_batch3: Vec<i32> = vec![1; batch3_rows];
let batch3 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(group_ids_batch3)),
Arc::new(Int32Array::from(values_batch3)),
],
)?;
let input_partitions = vec![vec![batch1, batch2, batch3]];
let runtime = RuntimeEnvBuilder::default().build_arc()?;
let mut task_ctx = TaskContext::default().with_runtime(runtime);
let mut session_config = task_ctx.session_config().clone();
session_config = session_config.set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&datafusion_common::ScalarValue::UInt64(Some(probe_rows_threshold)),
);
session_config = session_config.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&datafusion_common::ScalarValue::Float64(Some(probe_ratio_threshold)),
);
task_ctx = task_ctx.with_session_config(session_config);
let task_ctx = Arc::new(task_ctx);
let group_expr = vec![(col("group_col", &schema)?, "group_col".to_string())];
let aggr_expr = vec![Arc::new(
AggregateExprBuilder::new(count_udaf(), vec![col("value_col", &schema)?])
.schema(Arc::clone(&schema))
.alias("count_value")
.build()?,
)];
let exec = TestMemoryExec::try_new(&input_partitions, Arc::clone(&schema), None)?;
let exec = Arc::new(TestMemoryExec::update_cache(&Arc::new(exec)));
let aggregate_exec = AggregateExec::try_new(
AggregateMode::Partial,
PhysicalGroupBy::new_single(group_expr),
aggr_expr,
vec![None],
exec,
Arc::clone(&schema),
)?;
let mut stream =
GroupedHashAggregateStream::new(&aggregate_exec, &Arc::clone(&task_ctx), 0)?;
let mut results = Vec::new();
while let Some(result) = stream.next().await {
let batch = result?;
results.push(batch);
}
let metrics = aggregate_exec.metrics().unwrap();
let skipped_rows = metrics
.sum_by_name("skipped_aggregation_rows")
.map(|m| m.as_usize())
.unwrap_or(0);
assert_eq!(
skipped_rows, batch3_rows,
"Expected batch 3's rows ({batch3_rows}) to be skipped",
);
Ok(())
}
#[test]
fn test_skip_aggregation_probe_equality_does_not_skip() {
let threshold_ratio = 0.5_f64;
let threshold_rows = 10_usize;
let mut probe = SkipAggregationProbe::new(
threshold_rows,
threshold_ratio,
metrics::Count::new(),
);
probe.update_state(10, 5);
assert!(
!probe.should_skip(),
"ratio == threshold should not trigger skip (boundary is exclusive)"
);
}
}