Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
68c6fd8
feat: add data‑free SQL harness for array_agg_distinct benchmark
kosiew Sep 9, 2026
713afe2
chore: adjust benchmark scope from 1M to 100K groups
kosiew Sep 9, 2026
e015a1a
bench: scale array_agg distinct groups
kosiew Sep 11, 2026
51e8497
feat: add benchmarks for GroupsAccumulatorAdapter
kosiew Sep 12, 2026
6b8155e
fix: adjust comment formatting in groups_accumulator_adapter benchmark
kosiew Sep 14, 2026
ea83270
empty commit
kosiew Sep 9, 2026
43ba7b4
feat: add optional AggregateMetric API with lazy internal timers and …
kosiew Sep 8, 2026
add4dbd
feat(metrics): safe refactors to avoid redundant allocations and impr…
kosiew Sep 8, 2026
9bf5850
fix: assert both internal_distinct timers are >0 in test
kosiew Sep 8, 2026
8500d50
docs: update metrics.md with identity/cardinality, accumulator timer,…
kosiew Sep 8, 2026
1075a78
fix(DistinctArrayAggAccumulator): correct `size()` calculation
kosiew Sep 8, 2026
61ed2b9
feat(aggregates): add 2‑partition execution test and fix array_agg di…
kosiew Sep 8, 2026
9669a50
feat(metrics): add lock‑free OnceLock fast path for AggregateSubMetrics
kosiew Sep 9, 2026
edbafbb
fix: restore Time::add min‑1ns behavior and remove exact‑duration API
kosiew Sep 9, 2026
78bec05
fix(time): corrected split to preserve exact duration adds and avoid …
kosiew Sep 9, 2026
1978756
feat(metrics): require RefUnwindSafe for AggregateMetric and add comp…
kosiew Sep 9, 2026
363efba
test: add legacy grouped `array_agg(DISTINCT)` metric test
kosiew Sep 9, 2026
466467f
fix(array_agg): skip internal DISTINCT timing for small batches and i…
kosiew Sep 9, 2026
031f7a1
feat(datafusion): add grouped update metric and update_batch_grouped …
kosiew Sep 9, 2026
d48a874
fix(groups_accumulator): improve Instant usage and fix unnecessary Op…
kosiew Sep 10, 2026
f58d726
refactor: simplify distinct_metric cloning in DistinctArrayAggAccumul…
kosiew Sep 10, 2026
93918f6
feat(metrics): add array_agg(DISTINCT) merge, convert_to_state, and m…
kosiew Sep 11, 2026
d7749ba
feat: cache aggregate metric handles for grouped accumulators
kosiew Sep 11, 2026
9d6e9db
feat(groups-accumulator-adapter): move slice/filter prep outside timi…
kosiew Sep 11, 2026
f3ed57b
feat: convert_to_state uses bounded 64‑row prep chunks with timer‑wra…
kosiew Sep 11, 2026
fe23635
refactor: simplify metric lookup and consolidate implementation block…
kosiew Sep 11, 2026
4faf1b5
empty commit2-before merge main
kosiew Sep 11, 2026
eafaa43
fix(metrics): create execution-owned aggregate_sub_metrics helper and…
kosiew Sep 11, 2026
9716814
docs: clarify metrics documentation about internal submetrics
kosiew Sep 11, 2026
f7969dd
perf(aggregate): move size accounting out of timer and improve error …
kosiew Sep 11, 2026
38d72dc
test: add regression test for groups_accumulator
kosiew Sep 11, 2026
f48ce70
fix: clear successful groups before propagating later-group error and…
kosiew Sep 11, 2026
af82e47
fix: update array_agg_distinct benchmark group size from 100K to 1000K
kosiew Sep 12, 2026
f6fba7d
feat: implement fast path for uninstrumented aggregates
kosiew Sep 12, 2026
d30fe36
merge with main
kosiew Sep 14, 2026
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
12 changes: 12 additions & 0 deletions benchmarks/bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ nlj: Benchmark for simple nested loop joins, testing various
hj: Benchmark for simple hash joins, testing various join scenarios
smj: Benchmark for simple sort merge joins, testing various join scenarios
dict: Benchmark for dictionary-encoded group-by scenarios
array_agg_distinct: 1000K-group, two-row-per-group array_agg(DISTINCT) benchmark
compile_profile: Compile and execute TPC-H across selected Cargo profiles, reporting timing and binary size


Expand Down Expand Up @@ -651,6 +652,9 @@ main() {
dict)
run_dict
;;
array_agg_distinct)
run_array_agg_distinct
;;
compile_profile)
run_compile_profile "${PROFILE_ARGS[@]}"
;;
Expand Down Expand Up @@ -1665,6 +1669,14 @@ run_dict() {
debug_run $CARGO_COMMAND --bin dfbench -- dict --iterations 5 -o "${RESULTS_FILE}" ${QUERY_ARG} ${LATENCY_ARG}
}

# Runs the data-free high-cardinality array_agg(DISTINCT) SQL benchmark.
run_array_agg_distinct() {
echo "Running array_agg_distinct benchmark..."
debug_run env BENCH_NAME=array_agg_distinct \
${QUERY:+BENCH_QUERY="${QUERY}"} \
bash -c "$SQL_CARGO_COMMAND"
}


compare_benchmarks() {
BASE_RESULTS_DIR="${SCRIPT_DIR}/results"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
description = "High-cardinality array_agg(DISTINCT) SQL benchmarks"

query_pattern = "q{QUERY_ID_PADDED}.benchmark"

[[examples]]
command = "cargo run --release --bin benchmark_runner -- array_agg_distinct"
description = "Run the high-cardinality array_agg(DISTINCT) benchmark."

[[examples]]
command = "cargo run --release --bin benchmark_runner -- array_agg_distinct --query 1 --iterations 5 --output /tmp/array_agg_distinct.json"
description = "Run five iterations and write comparable JSON results."
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name Q01
group array_agg_distinct

expect_plan AggregateExec

run
-- 1M groups, 2 rows/group, and 2 distinct values/group. `range` is end-exclusive.
-- This is data-free so comparisons isolate grouped array_agg(DISTINCT) execution.
SELECT value / 2 AS k, array_agg(DISTINCT value % 2) AS distinct_values
FROM range(2000000)
GROUP BY value / 2;
53 changes: 53 additions & 0 deletions datafusion/expr-common/src/accumulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@
use arrow::array::ArrayRef;
use datafusion_common::{Result, ScalarValue, internal_err};
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Duration;

/// A metric owned by one aggregate implementation.
///
/// Aggregate implementations use this interface for optional internal
/// subphases. The execution engine owns metric registration and aggregation.
pub trait AggregateMetric: Debug + Send + Sync + std::panic::RefUnwindSafe {
/// Adds elapsed time to this metric.
fn add_duration(&self, duration: Duration);
}

/// Factory for optional metrics owned by one aggregate expression.
///
/// `subphase` must be a stable static identifier. An implementation may request
/// no metrics. The execution engine assigns the aggregate expression identity.
pub trait AggregateMetrics: Debug + Send + Sync {
/// Returns the metric for an aggregate-owned internal subphase.
fn metric(&self, subphase: &'static str) -> Arc<dyn AggregateMetric>;
}

/// Tracks an aggregate function's state.
///
Expand Down Expand Up @@ -49,6 +69,12 @@ use std::fmt::Debug;
/// [`merge_batch`]: Self::merge_batch
/// [window function]: https://en.wikipedia.org/wiki/Window_function_(SQL)
pub trait Accumulator: Send + Sync + Debug + std::any::Any {
/// Supplies optional metrics owned by this aggregate expression.
///
/// The default preserves compatibility for accumulators without internal
/// submetrics.
fn set_metrics(&mut self, _metrics: Arc<dyn AggregateMetrics>) {}

/// Updates the accumulator's state from its input.
///
/// `values` contains the arguments to this aggregate function.
Expand All @@ -58,6 +84,33 @@ pub trait Accumulator: Send + Sync + Debug + std::any::Any {
/// running sum.
fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()>;

/// Returns an optional metric timed once per grouped adapter input batch.
///
/// A grouped accumulator adapter uses this for aggregate-owned work it
/// dispatches to one accumulator per group. The default preserves the
/// usual per-accumulator update path.
fn grouped_update_batch_metric(&self) -> Option<Arc<dyn AggregateMetric>> {
None
}

/// Updates state when called by a grouped accumulator adapter.
///
/// The default delegates to [`Self::update_batch`]. Implementations that
/// return a [`Self::grouped_update_batch_metric`] can avoid timing every
/// per-group call; the adapter records one interval for the full batch.
fn update_batch_grouped(&mut self, values: &[ArrayRef]) -> Result<()> {
self.update_batch(values)
}

/// Merges state when called by a grouped accumulator adapter.
///
/// The default delegates to [`Self::merge_batch`]. Implementations that
/// return a [`Self::grouped_update_batch_metric`] can avoid timing every
/// per-group merge; the adapter records one interval for the full batch.
fn merge_batch_grouped(&mut self, states: &[ArrayRef]) -> Result<()> {
self.merge_batch(states)
}

/// Returns the final aggregate value.
///
/// For example, the `SUM` accumulator maintains a running sum,
Expand Down
9 changes: 9 additions & 0 deletions datafusion/expr-common/src/groups_accumulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

use arrow::array::{ArrayRef, BooleanArray};
use datafusion_common::{Result, exec_err, not_impl_err, utils::split_vec_min_alloc};
use std::sync::Arc;

use crate::accumulator::AggregateMetrics;

/// Describes how many rows should be emitted during grouping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -188,6 +191,12 @@ impl<'a> GroupSelection<'a> {
/// [`Accumulator`]: crate::accumulator::Accumulator
/// [Aggregating Millions of Groups Fast blog]: https://arrow.apache.org/blog/2023/08/05/datafusion_fast_grouping/
pub trait GroupsAccumulator: Send + std::any::Any {
/// Supplies optional metrics owned by this aggregate expression.
///
/// The default preserves compatibility for accumulators without internal
/// submetrics.
fn set_metrics(&mut self, _metrics: Arc<dyn AggregateMetrics>) {}

/// Updates the accumulator's state from its arguments, encoded as
/// a vector of [`ArrayRef`]s.
///
Expand Down
4 changes: 3 additions & 1 deletion datafusion/expr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ pub use datafusion_doc::{
DocSection, Documentation, DocumentationBuilder, aggregate_doc_sections,
scalar_doc_sections, window_doc_sections,
};
pub use datafusion_expr_common::accumulator::Accumulator;
pub use datafusion_expr_common::accumulator::{
Accumulator, AggregateMetric, AggregateMetrics,
};
pub use datafusion_expr_common::columnar_value::ColumnarValue;
pub use datafusion_expr_common::groups_accumulator::{
EmitTo, GroupSelection, GroupsAccumulator,
Expand Down
4 changes: 4 additions & 0 deletions datafusion/functions-aggregate-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ rand = { workspace = true }
[[bench]]
harness = false
name = "accumulate"

[[bench]]
harness = false
name = "groups_accumulator_adapter"
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//! Benchmarks the uninstrumented `GroupsAccumulatorAdapter` update path.

use std::hint::black_box;
use std::sync::Arc;

use arrow::array::{ArrayRef, Int64Array};
use arrow::datatypes::DataType;
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use datafusion_expr_common::accumulator::Accumulator;
use datafusion_expr_common::groups_accumulator::GroupsAccumulator;
use datafusion_functions_aggregate_common::aggregate::groups_accumulator::GroupsAccumulatorAdapter;
use datafusion_functions_aggregate_common::min_max::MaxAccumulator;

const NUM_GROUPS: usize = 8_192;

fn groups_accumulator_adapter(c: &mut Criterion) {
let values: ArrayRef = Arc::new(Int64Array::from_iter_values(0..NUM_GROUPS as i64));
let group_indices: Vec<_> = (0..NUM_GROUPS).collect();

c.bench_function("groups_accumulator_adapter/update_batch/8192_groups", |b| {
b.iter_batched(
|| {
GroupsAccumulatorAdapter::new(|| {
Ok(Box::new(MaxAccumulator::try_new(&DataType::Int64)?)
as Box<dyn Accumulator>)
})
},
|mut accumulator| {
accumulator
.update_batch(
&[Arc::clone(&values)],
&group_indices,
None,
NUM_GROUPS,
)
.unwrap();
black_box(accumulator);
},
BatchSize::SmallInput,
);
});
}

criterion_group!(benches, groups_accumulator_adapter);
criterion_main!(benches);
Loading