Skip to main content

datafusion_functions_aggregate/
approx_distinct.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//! Defines physical expressions that can evaluated at runtime during query execution
19
20use crate::hyperloglog::{HLL_HASH_STATE, HyperLogLog};
21use arrow::array::{Array, BinaryArray, StringViewArray};
22use arrow::array::{
23    GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
24};
25use arrow::datatypes::{
26    ArrowPrimitiveType, Date32Type, Date64Type, FieldRef, Int32Type, Int64Type,
27    Time32MillisecondType, Time32SecondType, Time64MicrosecondType, Time64NanosecondType,
28    TimeUnit, TimestampMicrosecondType, TimestampMillisecondType,
29    TimestampNanosecondType, TimestampSecondType, UInt32Type, UInt64Type,
30};
31use arrow::{array::ArrayRef, datatypes::DataType, datatypes::Field};
32use datafusion_common::ScalarValue;
33use datafusion_common::{
34    DataFusionError, Result, downcast_value, internal_datafusion_err, internal_err,
35    not_impl_err,
36};
37use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs};
38use datafusion_expr::utils::format_state_name;
39use datafusion_expr::{
40    Accumulator, AggregateUDFImpl, Documentation, Signature, Volatility,
41};
42use datafusion_functions_aggregate_common::aggregate::count_distinct::{
43    Bitmap65536DistinctCountAccumulator, Bitmap65536DistinctCountAccumulatorI16,
44    BoolArray256DistinctCountAccumulator, BoolArray256DistinctCountAccumulatorI8,
45};
46use datafusion_functions_aggregate_common::noop_accumulator::NoopAccumulator;
47use datafusion_macros::user_doc;
48use std::fmt::{Debug, Formatter};
49use std::hash::{BuildHasher, Hash};
50use std::marker::PhantomData;
51
52make_udaf_expr_and_func!(
53    ApproxDistinct,
54    approx_distinct,
55    expression,
56    "approximate number of distinct input values",
57    approx_distinct_udaf
58);
59
60impl<T: Hash + ?Sized> From<&HyperLogLog<T>> for ScalarValue {
61    fn from(v: &HyperLogLog<T>) -> ScalarValue {
62        let values = v.as_ref().to_vec();
63        ScalarValue::Binary(Some(values))
64    }
65}
66
67impl<T: Hash + ?Sized> TryFrom<&[u8]> for HyperLogLog<T> {
68    type Error = DataFusionError;
69    fn try_from(v: &[u8]) -> Result<HyperLogLog<T>> {
70        let arr: [u8; 16384] = v.try_into().map_err(|_| {
71            internal_datafusion_err!("Impossibly got invalid binary array from states")
72        })?;
73        Ok(HyperLogLog::<T>::new_with_registers(arr))
74    }
75}
76
77impl<T: Hash + ?Sized> TryFrom<&ScalarValue> for HyperLogLog<T> {
78    type Error = DataFusionError;
79    fn try_from(v: &ScalarValue) -> Result<HyperLogLog<T>> {
80        if let ScalarValue::Binary(Some(slice)) = v {
81            slice.as_slice().try_into()
82        } else {
83            internal_err!(
84                "Impossibly got invalid scalar value while converting to HyperLogLog"
85            )
86        }
87    }
88}
89
90#[derive(Debug)]
91struct ApproxDistinctBitmapWrapper<A: Accumulator> {
92    inner: A,
93}
94
95impl<A: Accumulator> Accumulator for ApproxDistinctBitmapWrapper<A> {
96    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
97        self.inner.update_batch(values)
98    }
99
100    fn evaluate(&mut self) -> Result<ScalarValue> {
101        match self.inner.evaluate()? {
102            ScalarValue::Int64(Some(v)) => Ok(ScalarValue::UInt64(Some(v as u64))),
103            other => internal_err!("unexpected: {other}"),
104        }
105    }
106
107    fn size(&self) -> usize {
108        self.inner.size()
109    }
110
111    fn state(&mut self) -> Result<Vec<ScalarValue>> {
112        self.inner.state()
113    }
114
115    fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
116        self.inner.merge_batch(states)
117    }
118}
119
120#[derive(Debug)]
121struct NumericHLLAccumulator<T>
122where
123    T: ArrowPrimitiveType,
124    T::Native: Hash,
125{
126    hll: HyperLogLog<T::Native>,
127}
128
129impl<T> NumericHLLAccumulator<T>
130where
131    T: ArrowPrimitiveType,
132    T::Native: Hash,
133{
134    pub fn new() -> Self {
135        Self {
136            hll: HyperLogLog::new(),
137        }
138    }
139}
140
141#[derive(Debug)]
142struct StringHLLAccumulator<T>
143where
144    T: OffsetSizeTrait,
145{
146    hll: HyperLogLog<str>,
147    phantom_data: PhantomData<T>,
148}
149
150impl<T> StringHLLAccumulator<T>
151where
152    T: OffsetSizeTrait,
153{
154    pub fn new() -> Self {
155        Self {
156            hll: HyperLogLog::new(),
157            phantom_data: PhantomData,
158        }
159    }
160}
161
162#[derive(Debug)]
163struct StringViewHLLAccumulator {
164    hll: HyperLogLog<str>,
165}
166
167impl StringViewHLLAccumulator {
168    pub fn new() -> Self {
169        Self {
170            hll: HyperLogLog::new(),
171        }
172    }
173}
174
175#[derive(Debug)]
176struct BinaryHLLAccumulator<T>
177where
178    T: OffsetSizeTrait,
179{
180    hll: HyperLogLog<[u8]>,
181    phantom_data: PhantomData<T>,
182}
183
184impl<T> BinaryHLLAccumulator<T>
185where
186    T: OffsetSizeTrait,
187{
188    pub fn new() -> Self {
189        Self {
190            hll: HyperLogLog::new(),
191            phantom_data: PhantomData,
192        }
193    }
194}
195
196macro_rules! default_accumulator_impl {
197    () => {
198        fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
199            assert_eq!(1, states.len(), "expect only 1 element in the states");
200            let binary_array = downcast_value!(states[0], BinaryArray);
201            for v in binary_array.iter() {
202                let v = v.ok_or_else(|| {
203                    internal_datafusion_err!(
204                        "Impossibly got empty binary array from states"
205                    )
206                })?;
207                let other = v.try_into()?;
208                self.hll.merge(&other);
209            }
210            Ok(())
211        }
212
213        fn state(&mut self) -> Result<Vec<ScalarValue>> {
214            let value = ScalarValue::from(&self.hll);
215            Ok(vec![value])
216        }
217
218        fn evaluate(&mut self) -> Result<ScalarValue> {
219            Ok(ScalarValue::UInt64(Some(self.hll.count() as u64)))
220        }
221
222        fn size(&self) -> usize {
223            // HLL has static size
224            std::mem::size_of_val(self)
225        }
226    };
227}
228
229impl<T> Accumulator for BinaryHLLAccumulator<T>
230where
231    T: OffsetSizeTrait,
232{
233    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
234        let array: &GenericBinaryArray<T> =
235            downcast_value!(values[0], GenericBinaryArray, T);
236        // flatten because we would skip nulls
237        self.hll.extend(array.into_iter().flatten());
238        Ok(())
239    }
240
241    default_accumulator_impl!();
242}
243
244impl Accumulator for StringViewHLLAccumulator {
245    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
246        let array: &StringViewArray = downcast_value!(values[0], StringViewArray);
247
248        if array.data_buffers().is_empty() {
249            // Fast path: with no data buffers every value is inline, so they all
250            // take the u128 path — no need to check the length per row.
251            for (i, &view) in array.views().iter().enumerate() {
252                if !array.is_null(i) {
253                    self.hll.add_hashed(HLL_HASH_STATE.hash_one(view));
254                }
255            }
256        } else {
257            // Mixed batch: decide per row by length. Short strings still use the
258            // u128 path so they match how they'd be hashed in an all-inline
259            // batch; only the genuinely out-of-line strings materialize a &str.
260            for (i, &view) in array.views().iter().enumerate() {
261                if array.is_null(i) {
262                    continue;
263                }
264                // The low 32 bits of the u128 view encode the string length.
265                if (view as u32) <= 12 {
266                    self.hll.add_hashed(HLL_HASH_STATE.hash_one(view));
267                } else {
268                    self.hll.add(array.value(i));
269                }
270            }
271        }
272
273        Ok(())
274    }
275
276    default_accumulator_impl!();
277}
278
279impl<T> Accumulator for StringHLLAccumulator<T>
280where
281    T: OffsetSizeTrait,
282{
283    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
284        let array: &GenericStringArray<T> =
285            downcast_value!(values[0], GenericStringArray, T);
286        // flatten because we would skip nulls
287        self.hll.extend(array.into_iter().flatten());
288        Ok(())
289    }
290
291    default_accumulator_impl!();
292}
293
294impl<T> Accumulator for NumericHLLAccumulator<T>
295where
296    T: ArrowPrimitiveType + Debug,
297    T::Native: Hash,
298{
299    fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
300        let array: &PrimitiveArray<T> = downcast_value!(values[0], PrimitiveArray, T);
301        // flatten because we would skip nulls
302        self.hll.extend(array.into_iter().flatten());
303        Ok(())
304    }
305
306    default_accumulator_impl!();
307}
308
309impl Debug for ApproxDistinct {
310    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
311        f.debug_struct("ApproxDistinct")
312            .field("name", &self.name())
313            .field("signature", &self.signature)
314            .finish()
315    }
316}
317
318impl Default for ApproxDistinct {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324#[user_doc(
325    doc_section(label = "Approximate Functions"),
326    description = "Returns the approximate number of distinct input values calculated using the HyperLogLog algorithm.",
327    syntax_example = "approx_distinct(expression)",
328    sql_example = r#"```sql
329> SELECT approx_distinct(column_name) FROM table_name;
330+-----------------------------------+
331| approx_distinct(column_name)      |
332+-----------------------------------+
333| 42                                |
334+-----------------------------------+
335```"#,
336    standard_argument(name = "expression",)
337)]
338#[derive(PartialEq, Eq, Hash)]
339pub struct ApproxDistinct {
340    signature: Signature,
341}
342
343impl ApproxDistinct {
344    pub fn new() -> Self {
345        Self {
346            signature: Signature::any(1, Volatility::Immutable),
347        }
348    }
349}
350
351#[cold]
352fn get_small_int_approx_accumulator(
353    data_type: &DataType,
354) -> Result<Box<dyn Accumulator>> {
355    match data_type {
356        DataType::UInt8 => Ok(Box::new(ApproxDistinctBitmapWrapper {
357            inner: BoolArray256DistinctCountAccumulator::new(),
358        })),
359        DataType::Int8 => Ok(Box::new(ApproxDistinctBitmapWrapper {
360            inner: BoolArray256DistinctCountAccumulatorI8::new(),
361        })),
362        DataType::UInt16 => Ok(Box::new(ApproxDistinctBitmapWrapper {
363            inner: Bitmap65536DistinctCountAccumulator::new(),
364        })),
365        DataType::Int16 => Ok(Box::new(ApproxDistinctBitmapWrapper {
366            inner: Bitmap65536DistinctCountAccumulatorI16::new(),
367        })),
368        _ => internal_err!("unsupported small int type: {}", data_type),
369    }
370}
371
372#[cold]
373fn get_small_int_state_field(name: &str, data_type: &DataType) -> Result<Vec<FieldRef>> {
374    Ok(vec![
375        Field::new_list(
376            format_state_name(name, "approx_distinct"),
377            Field::new_list_field(data_type.clone(), true),
378            false,
379        )
380        .into(),
381    ])
382}
383
384impl AggregateUDFImpl for ApproxDistinct {
385    fn name(&self) -> &str {
386        "approx_distinct"
387    }
388
389    fn signature(&self) -> &Signature {
390        &self.signature
391    }
392
393    fn return_type(&self, _: &[DataType]) -> Result<DataType> {
394        Ok(DataType::UInt64)
395    }
396
397    fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
398        let data_type = args.input_fields[0].data_type();
399        match data_type {
400            DataType::Null => Ok(vec![
401                Field::new(
402                    format_state_name(args.name, self.name()),
403                    DataType::Null,
404                    true,
405                )
406                .into(),
407            ]),
408            DataType::UInt8 | DataType::Int8 | DataType::UInt16 | DataType::Int16 => {
409                get_small_int_state_field(args.name, data_type)
410            }
411            _ => Ok(vec![
412                Field::new(
413                    format_state_name(args.name, "hll_registers"),
414                    DataType::Binary,
415                    false,
416                )
417                .into(),
418            ]),
419        }
420    }
421
422    fn accumulator(&self, acc_args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
423        let data_type = acc_args.expr_fields[0].data_type();
424
425        let accumulator: Box<dyn Accumulator> = match data_type {
426            DataType::UInt8 | DataType::Int8 | DataType::UInt16 | DataType::Int16 => {
427                return get_small_int_approx_accumulator(data_type);
428            }
429            DataType::UInt32 => Box::new(NumericHLLAccumulator::<UInt32Type>::new()),
430            DataType::UInt64 => Box::new(NumericHLLAccumulator::<UInt64Type>::new()),
431            DataType::Int32 => Box::new(NumericHLLAccumulator::<Int32Type>::new()),
432            DataType::Int64 => Box::new(NumericHLLAccumulator::<Int64Type>::new()),
433            DataType::Date32 => Box::new(NumericHLLAccumulator::<Date32Type>::new()),
434            DataType::Date64 => Box::new(NumericHLLAccumulator::<Date64Type>::new()),
435            DataType::Time32(TimeUnit::Second) => {
436                Box::new(NumericHLLAccumulator::<Time32SecondType>::new())
437            }
438            DataType::Time32(TimeUnit::Millisecond) => {
439                Box::new(NumericHLLAccumulator::<Time32MillisecondType>::new())
440            }
441            DataType::Time64(TimeUnit::Microsecond) => {
442                Box::new(NumericHLLAccumulator::<Time64MicrosecondType>::new())
443            }
444            DataType::Time64(TimeUnit::Nanosecond) => {
445                Box::new(NumericHLLAccumulator::<Time64NanosecondType>::new())
446            }
447            DataType::Timestamp(TimeUnit::Second, _) => {
448                Box::new(NumericHLLAccumulator::<TimestampSecondType>::new())
449            }
450            DataType::Timestamp(TimeUnit::Millisecond, _) => {
451                Box::new(NumericHLLAccumulator::<TimestampMillisecondType>::new())
452            }
453            DataType::Timestamp(TimeUnit::Microsecond, _) => {
454                Box::new(NumericHLLAccumulator::<TimestampMicrosecondType>::new())
455            }
456            DataType::Timestamp(TimeUnit::Nanosecond, _) => {
457                Box::new(NumericHLLAccumulator::<TimestampNanosecondType>::new())
458            }
459            DataType::Utf8 => Box::new(StringHLLAccumulator::<i32>::new()),
460            DataType::LargeUtf8 => Box::new(StringHLLAccumulator::<i64>::new()),
461            DataType::Utf8View => Box::new(StringViewHLLAccumulator::new()),
462            DataType::Binary => Box::new(BinaryHLLAccumulator::<i32>::new()),
463            DataType::LargeBinary => Box::new(BinaryHLLAccumulator::<i64>::new()),
464            DataType::Null => {
465                Box::new(NoopAccumulator::new(ScalarValue::UInt64(Some(0))))
466            }
467            other => {
468                return not_impl_err!(
469                    "Support for 'approx_distinct' for data type {other} is not implemented"
470                );
471            }
472        };
473        Ok(accumulator)
474    }
475
476    fn documentation(&self) -> Option<&Documentation> {
477        self.doc()
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use arrow::array::AsArray;
485    use std::sync::Arc;
486
487    // A string longer than the 12-byte inline limit
488    const LONG: &str = "this string is definitely longer than twelve bytes";
489
490    fn distinct_count(acc: &mut StringViewHLLAccumulator) -> u64 {
491        match acc.evaluate().unwrap() {
492            ScalarValue::UInt64(Some(v)) => v,
493            other => panic!("unexpected evaluate result: {other:?}"),
494        }
495    }
496
497    /// Regression: a short (≤ 12-byte) Utf8View string must hash identically
498    /// regardless of which batch it appears in — all-inline or mixed.
499    #[test]
500    fn utf8view_acc_split_batches_match_single_mixed_batch() {
501        // Multiset: {"aaa" x2, "bbb", LONG}, so 3 distinct values.
502        let mixed: ArrayRef =
503            Arc::new(StringViewArray::from(vec!["aaa", "bbb", LONG, "aaa"]));
504        let mut acc_single = StringViewHLLAccumulator::new();
505        acc_single.update_batch(&[mixed]).unwrap();
506
507        // Same multiset, but split so "aaa" lands in both an all-inline batch
508        // and a batch with a data buffer (forced by LONG).
509        let inline_only: ArrayRef = Arc::new(StringViewArray::from(vec!["aaa", "bbb"]));
510        let with_buffer: ArrayRef = Arc::new(StringViewArray::from(vec!["aaa", LONG]));
511        assert!(inline_only.as_string_view().data_buffers().is_empty());
512        assert!(!with_buffer.as_string_view().data_buffers().is_empty());
513
514        let mut acc_split = StringViewHLLAccumulator::new();
515        acc_split.update_batch(&[inline_only]).unwrap();
516        acc_split.update_batch(&[with_buffer]).unwrap();
517
518        assert_eq!(
519            distinct_count(&mut acc_single),
520            distinct_count(&mut acc_split)
521        );
522        assert_eq!(distinct_count(&mut acc_single), 3);
523    }
524}