1use 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 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 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 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 for (i, &view) in array.views().iter().enumerate() {
261 if array.is_null(i) {
262 continue;
263 }
264 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 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 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 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 #[test]
500 fn utf8view_acc_split_batches_match_single_mixed_batch() {
501 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 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}