Skip to main content

datafusion_expr_common/
groups_accumulator.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Vectorized [`GroupsAccumulator`]
19
20use arrow::array::{ArrayRef, BooleanArray};
21use datafusion_common::{Result, utils::split_vec_min_alloc};
22
23/// Describes how many rows should be emitted during grouping.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum EmitTo {
26    /// Emit all groups
27    All,
28    /// Emit only the first `n` groups and shift all existing group
29    /// indexes down by `n`.
30    ///
31    /// For example, if `n=10`, group_index `0, 1, ... 9` are emitted
32    /// and group indexes `10, 11, 12, ...` become `0, 1, 2, ...`.
33    First(usize),
34}
35
36impl EmitTo {
37    /// Removes the number of rows from `v` required to emit the right
38    /// number of rows, returning a `Vec` with elements taken, and the
39    /// remaining values in `v`.
40    ///
41    /// This avoids copying if Self::All
42    pub fn take_needed<T>(&self, v: &mut Vec<T>) -> Vec<T> {
43        match self {
44            Self::All => {
45                // Take the entire vector, leave new (empty) vector
46                std::mem::take(v)
47            }
48            Self::First(n) => split_vec_min_alloc(v, *n),
49        }
50    }
51}
52
53/// `GroupsAccumulator` implements a single aggregate (e.g. AVG) and
54/// stores the state for *all* groups internally.
55///
56/// Logically, a [`GroupsAccumulator`] stores a mapping from each group index to
57/// the state of the aggregate for that group. For example an implementation for
58/// `min` might look like
59///
60/// ```text
61///    ┌─────┐
62///    │  0  │───────────▶   100
63///    ├─────┤
64///    │  1  │───────────▶   200
65///    └─────┘
66///      ...                 ...
67///    ┌─────┐
68///    │ N-2 │───────────▶    50
69///    ├─────┤
70///    │ N-1 │───────────▶   200
71///    └─────┘
72///
73///
74///  Logical group      Current Min
75///     number          value for that
76///                     group
77/// ```
78///
79/// # Notes on Implementing `GroupsAccumulator`
80///
81/// All aggregates must first implement the simpler [`Accumulator`] trait, which
82/// handles state for a single group. Implementing `GroupsAccumulator` is
83/// optional and is harder to implement than `Accumulator`, but can be much
84/// faster for queries with many group values.  See the [Aggregating Millions of
85/// Groups Fast blog] for more background.
86/// For more background, please also see the [Aggregating Millions of Groups Fast in Apache Arrow DataFusion 28.0.0 blog]
87///
88/// [Aggregating Millions of Groups Fast in Apache Arrow DataFusion 28.0.0 blog]: https://datafusion.apache.org/blog/2023/08/05/datafusion_fast_grouping
89///
90/// [`NullState`] can help keep the state for groups that have not seen any
91/// values and produce the correct output for those groups.
92///
93/// [`NullState`]: https://docs.rs/datafusion/latest/datafusion/physical_expr/struct.NullState.html
94///
95/// # Details
96/// Each group is assigned a `group_index` by the hash table and each
97/// accumulator manages the specific state, one per `group_index`.
98///
99/// `group_index`es are contiguous (there aren't gaps), and thus it is
100/// expected that each `GroupsAccumulator` will use something like `Vec<..>`
101/// to store the group states.
102///
103/// [`Accumulator`]: crate::accumulator::Accumulator
104/// [Aggregating Millions of Groups Fast blog]: https://arrow.apache.org/blog/2023/08/05/datafusion_fast_grouping/
105pub trait GroupsAccumulator: Send + std::any::Any {
106    /// Updates the accumulator's state from its arguments, encoded as
107    /// a vector of [`ArrayRef`]s.
108    ///
109    /// * `values`: the input arguments to the accumulator
110    ///
111    /// * `group_indices`: The group indices to which each row in `values` belongs.
112    ///
113    /// * `opt_filter`: if present, only update aggregate state using
114    ///   `values[i]` if `opt_filter[i]` is true
115    ///
116    /// * `total_num_groups`: the number of groups (the largest
117    ///   group_index is thus `total_num_groups - 1`).
118    ///
119    /// Note that subsequent calls to update_batch may have larger
120    /// total_num_groups as new groups are seen.
121    ///
122    /// See [`NullState`] to help keep the state for groups that have not seen any
123    /// values and produce the correct output for those groups.
124    ///
125    /// [`NullState`]: https://docs.rs/datafusion/latest/datafusion/physical_expr/struct.NullState.html
126    fn update_batch(
127        &mut self,
128        values: &[ArrayRef],
129        group_indices: &[usize],
130        opt_filter: Option<&BooleanArray>,
131        total_num_groups: usize,
132    ) -> Result<()>;
133
134    /// Returns the final aggregate value for each group as a single
135    /// `RecordBatch`, resetting the internal state.
136    ///
137    /// The rows returned *must* be in group_index order: The value
138    /// for group_index 0, followed by 1, etc.  Any group_index that
139    /// did not have values, should be null.
140    ///
141    /// For example, a `SUM` accumulator maintains a running sum for
142    /// each group, and `evaluate` will produce that running sum as
143    /// its output for all groups, in group_index order
144    ///
145    /// If `emit_to` is [`EmitTo::All`], the accumulator should
146    /// return all groups and release / reset its internal state
147    /// equivalent to when it was first created.
148    ///
149    /// If `emit_to` is [`EmitTo::First`], only the first `n` groups
150    /// should be emitted and the state for those first groups
151    /// removed. State for the remaining groups must be retained for
152    /// future use. The group_indices on subsequent calls to
153    /// `update_batch` or `merge_batch` will be shifted down by
154    /// `n`. See [`EmitTo::First`] for more details.
155    fn evaluate(&mut self, emit_to: EmitTo) -> Result<ArrayRef>;
156
157    /// Returns the intermediate aggregate state for this accumulator,
158    /// used for multi-phase grouping, resetting its internal state.
159    ///
160    /// See [`Accumulator::state`] for more information on multi-phase
161    /// aggregation.
162    ///
163    /// For example, `AVG` might return two arrays: `SUM` and `COUNT`
164    /// but the `MIN` aggregate would just return a single array.
165    ///
166    /// Note more sophisticated internal state can be passed as
167    /// single `StructArray` rather than multiple arrays.
168    ///
169    /// See [`Self::evaluate`] for details on the required output
170    /// order and `emit_to`.
171    ///
172    /// [`Accumulator::state`]: crate::accumulator::Accumulator::state
173    fn state(&mut self, emit_to: EmitTo) -> Result<Vec<ArrayRef>>;
174
175    /// Merges intermediate state (the output from [`Self::state`])
176    /// into this accumulator's current state.
177    ///
178    /// For some aggregates (such as `SUM`), `merge_batch` is the same
179    /// as `update_batch`, but for some aggregates (such as `COUNT`,
180    /// where the partial counts must be summed) the operations
181    /// differ. See [`Self::state`] for more details on how state is
182    /// used and merged.
183    ///
184    /// * `values`: arrays produced from previously calling `state` on other accumulators.
185    ///
186    /// Other arguments are the same as for [`Self::update_batch`], except that
187    /// there is no `opt_filter` — aggregate filters are applied during the
188    /// partial (update) phase, so by the time intermediate states are merged
189    /// no per-row filtering is needed.
190    fn merge_batch(
191        &mut self,
192        values: &[ArrayRef],
193        group_indices: &[usize],
194        total_num_groups: usize,
195    ) -> Result<()>;
196
197    /// Converts an input batch directly to the intermediate aggregate state.
198    ///
199    /// This is the equivalent of treating each input row as its own group. It
200    /// is invoked when the Partial phase of a multi-phase aggregation is not
201    /// reducing the cardinality enough to warrant spending more effort on
202    /// pre-aggregation (see `Background` section below), and switches to
203    /// passing intermediate state directly on to the next aggregation phase.
204    ///
205    /// Examples:
206    /// * `COUNT`: an array of 1s for each row in the input batch.
207    /// * `SUM/MIN/MAX`: the input values themselves.
208    ///
209    /// # Arguments
210    /// * `values`: the input arguments to the accumulator
211    /// * `opt_filter`: if present, any row where `opt_filter[i]` is false should be ignored
212    ///
213    /// # Background
214    ///
215    /// In a multi-phase aggregation (see [`Accumulator::state`]), the initial
216    /// Partial phase reduces the cardinality of the input data as soon as
217    /// possible in the plan.
218    ///
219    /// This strategy is very effective for queries with a small number of
220    /// groups, as most of the data is aggregated immediately and only a small
221    /// amount of data must be repartitioned (see [`Accumulator::state`] for
222    /// background)
223    ///
224    /// However, for queries with a large number of groups, the Partial phase
225    /// often does not reduce the cardinality enough to warrant the memory and
226    /// CPU cost of actually performing the aggregation. For such cases, the
227    /// HashAggregate operator will dynamically switch to passing intermediate
228    /// state directly to the next aggregation phase with minimal processing
229    /// using this method.
230    ///
231    /// [`Accumulator::state`]: crate::accumulator::Accumulator::state
232    fn convert_to_state(
233        &self,
234        values: &[ArrayRef],
235        opt_filter: Option<&BooleanArray>,
236    ) -> Result<Vec<ArrayRef>>;
237
238    /// Amount of memory used to store the state of this accumulator,
239    /// in bytes.
240    ///
241    /// This function is called once per batch, so it should be `O(n)` to
242    /// compute, not `O(num_groups)`
243    ///
244    /// May be expensive; check the implementation before calling on hot paths.
245    fn size(&self) -> usize;
246}
247
248#[cfg(test)]
249mod tests {
250    use super::EmitTo;
251
252    /// When `n` is small relative to `len`, the old `split_off(n) + swap` pattern had
253    /// two allocation problems:
254    ///
255    /// 1. The returned Vec kept the original large backing allocation even though it
256    ///    only contains `n` elements (wasted capacity on a short-lived value).
257    /// 2. `split_off` allocated a fresh Vec for the `len - n` remaining elements,
258    ///    even though that side is much larger than `n` — the expensive side to
259    ///    allocate.
260    ///
261    /// `split_vec_min_alloc` fixes both: when `n * 2 <= len` it uses
262    /// `drain(0..n).collect()`, allocating only `n` elements for the emitted prefix
263    /// and keeping the original large backing in the remaining accumulator.
264    #[test]
265    fn take_needed_first_small_n_allocates_minimally() {
266        let mut v: Vec<i32> = Vec::with_capacity(128);
267        v.extend(0..20i32);
268        let original_capacity = v.capacity(); // 128
269
270        // n=4, n*2=8 <= len=20 -> drain branch in split_vec_min_alloc
271        let emitted = EmitTo::First(4).take_needed(&mut v);
272
273        assert_eq!(emitted, vec![0, 1, 2, 3]);
274        assert_eq!(v, (4..20i32).collect::<Vec<_>>());
275
276        // The emitted prefix must NOT carry the original large allocation.
277        // Old split_off+swap returned a Vec with capacity=128 for only 4 elements.
278        assert!(
279            emitted.capacity() <= 4,
280            "emitted prefix capacity {} should be ~n=4, not the original {}",
281            emitted.capacity(),
282            original_capacity,
283        );
284
285        // The remaining accumulator must retain the original large allocation so
286        // that incoming groups don't immediately force a realloc.
287        // Old split_off+swap left the remaining vec with a small fresh allocation.
288        assert_eq!(
289            v.capacity(),
290            original_capacity,
291            "remaining vec capacity {} should equal original {}",
292            v.capacity(),
293            original_capacity,
294        );
295    }
296}