1use std::fmt::Debug;
19use std::mem::size_of_val;
20use std::sync::Arc;
21
22use arrow::array::{Array, Float16Array};
23use arrow::compute::{filter, is_not_null};
24use arrow::datatypes::FieldRef;
25use arrow::{
26 array::{ArrayRef, Float32Array, Float64Array},
27 datatypes::{DataType, Field},
28};
29use datafusion_common::types::{NativeType, logical_float64};
30use datafusion_common::{
31 DataFusionError, Result, ScalarValue, downcast_value, internal_err, not_impl_err,
32 plan_err,
33};
34use datafusion_expr::expr::{AggregateFunction, Sort};
35use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs};
36use datafusion_expr::utils::format_state_name;
37use datafusion_expr::{
38 Accumulator, AggregateUDFImpl, Coercion, Documentation, Expr, Signature,
39 TypeSignature, TypeSignatureClass, Volatility,
40};
41use datafusion_functions_aggregate_common::tdigest::{DEFAULT_MAX_SIZE, TDigest};
42use datafusion_macros::user_doc;
43use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
44
45use crate::utils::{get_scalar_value, validate_percentile_expr};
46
47create_func!(ApproxPercentileCont, approx_percentile_cont_udaf);
48
49pub fn approx_percentile_cont(
51 order_by: Sort,
52 percentile: Expr,
53 centroids: Option<Expr>,
54) -> Expr {
55 let expr = order_by.expr.clone();
56
57 let args = if let Some(centroids) = centroids {
58 vec![expr, percentile, centroids]
59 } else {
60 vec![expr, percentile]
61 };
62
63 Expr::AggregateFunction(AggregateFunction::new_udf(
64 approx_percentile_cont_udaf(),
65 args,
66 false,
67 None,
68 vec![order_by],
69 None,
70 ))
71}
72
73#[user_doc(
74 doc_section(label = "Approximate Functions"),
75 description = "Returns the approximate percentile of input values using the t-digest algorithm.",
76 syntax_example = "approx_percentile_cont(percentile [, centroids]) WITHIN GROUP (ORDER BY expression)",
77 sql_example = r#"```sql
78> SELECT approx_percentile_cont(0.75) WITHIN GROUP (ORDER BY column_name) FROM table_name;
79+------------------------------------------------------------------+
80| approx_percentile_cont(0.75) WITHIN GROUP (ORDER BY column_name) |
81+------------------------------------------------------------------+
82| 65.0 |
83+------------------------------------------------------------------+
84> SELECT approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name;
85+-----------------------------------------------------------------------+
86| approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) |
87+-----------------------------------------------------------------------+
88| 65.0 |
89+-----------------------------------------------------------------------+
90```
91An alternate syntax is also supported:
92```sql
93> SELECT approx_percentile_cont(column_name, 0.75) FROM table_name;
94+-----------------------------------------------+
95| approx_percentile_cont(column_name, 0.75) |
96+-----------------------------------------------+
97| 65.0 |
98+-----------------------------------------------+
99
100> SELECT approx_percentile_cont(column_name, 0.75, 100) FROM table_name;
101+----------------------------------------------------------+
102| approx_percentile_cont(column_name, 0.75, 100) |
103+----------------------------------------------------------+
104| 65.0 |
105+----------------------------------------------------------+
106```
107"#,
108 standard_argument(name = "expression",),
109 argument(
110 name = "percentile",
111 description = "Percentile to compute. Must be a float value between 0 and 1 (inclusive)."
112 ),
113 argument(
114 name = "centroids",
115 description = "Number of centroids to use in the t-digest algorithm. _Default is 100_. A higher number results in more accurate approximation but requires more memory."
116 )
117)]
118#[derive(Debug, PartialEq, Eq, Hash)]
119pub struct ApproxPercentileCont {
120 signature: Signature,
121}
122
123impl Default for ApproxPercentileCont {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl ApproxPercentileCont {
130 pub fn new() -> Self {
132 let signature = Signature::one_of(
134 vec![
135 TypeSignature::Coercible(vec![
137 Coercion::new_implicit(
138 TypeSignatureClass::Float,
139 vec![TypeSignatureClass::Numeric],
140 NativeType::Float64,
141 ),
142 Coercion::new_implicit(
143 TypeSignatureClass::Native(logical_float64()),
144 vec![TypeSignatureClass::Numeric],
145 NativeType::Float64,
146 ),
147 ]),
148 TypeSignature::Coercible(vec![
150 Coercion::new_implicit(
151 TypeSignatureClass::Float,
152 vec![TypeSignatureClass::Numeric],
153 NativeType::Float64,
154 ),
155 Coercion::new_implicit(
156 TypeSignatureClass::Native(logical_float64()),
157 vec![TypeSignatureClass::Numeric],
158 NativeType::Float64,
159 ),
160 Coercion::new_implicit(
161 TypeSignatureClass::Integer,
162 vec![TypeSignatureClass::Numeric],
163 NativeType::Int64,
164 ),
165 ]),
166 ],
167 Volatility::Immutable,
168 );
169 Self { signature }
170 }
171
172 pub(crate) fn create_accumulator(
173 &self,
174 args: &AccumulatorArgs,
175 ) -> Result<ApproxPercentileAccumulator> {
176 let percentile =
177 validate_percentile_expr(&args.exprs[1], "APPROX_PERCENTILE_CONT")?;
178
179 let is_descending = args
180 .order_bys
181 .first()
182 .map(|sort_expr| sort_expr.options.descending)
183 .unwrap_or(false);
184
185 let percentile = if is_descending {
186 1.0 - percentile
187 } else {
188 percentile
189 };
190
191 let tdigest_max_size = if args.exprs.len() == 3 {
192 Some(validate_input_max_size_expr(&args.exprs[2])?)
193 } else {
194 None
195 };
196
197 let data_type = args.expr_fields[0].data_type();
198 let accumulator: ApproxPercentileAccumulator = match data_type {
199 DataType::Float16 | DataType::Float32 | DataType::Float64 => {
200 if let Some(max_size) = tdigest_max_size {
201 ApproxPercentileAccumulator::new_with_max_size(
202 percentile,
203 data_type.clone(),
204 max_size,
205 )
206 } else {
207 ApproxPercentileAccumulator::new(percentile, data_type.clone())
208 }
209 }
210 other => {
211 return not_impl_err!(
212 "Support for 'APPROX_PERCENTILE_CONT' for data type {other} is not implemented"
213 );
214 }
215 };
216
217 Ok(accumulator)
218 }
219}
220
221fn validate_input_max_size_expr(expr: &Arc<dyn PhysicalExpr>) -> Result<usize> {
222 let scalar_value = get_scalar_value(expr).map_err(|_e| {
223 DataFusionError::Plan(
224 "Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be a literal"
225 .to_string(),
226 )
227 })?;
228
229 let max_size = match scalar_value {
230 ScalarValue::UInt8(Some(q)) => q as usize,
231 ScalarValue::UInt16(Some(q)) => q as usize,
232 ScalarValue::UInt32(Some(q)) => q as usize,
233 ScalarValue::UInt64(Some(q)) => q as usize,
234 ScalarValue::Int32(Some(q)) if q > 0 => q as usize,
235 ScalarValue::Int64(Some(q)) if q > 0 => q as usize,
236 ScalarValue::Int16(Some(q)) if q > 0 => q as usize,
237 ScalarValue::Int8(Some(q)) if q > 0 => q as usize,
238 sv => {
239 return plan_err!(
240 "Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be UInt > 0 literal (got data type {}).",
241 sv.data_type()
242 );
243 }
244 };
245
246 Ok(max_size)
247}
248
249impl AggregateUDFImpl for ApproxPercentileCont {
250 fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
253 Ok(vec![
254 Field::new(
255 format_state_name(args.name, "max_size"),
256 DataType::UInt64,
257 false,
258 ),
259 Field::new(
260 format_state_name(args.name, "sum"),
261 DataType::Float64,
262 false,
263 ),
264 Field::new(
265 format_state_name(args.name, "count"),
266 DataType::Float64,
267 false,
268 ),
269 Field::new(
270 format_state_name(args.name, "max"),
271 DataType::Float64,
272 false,
273 ),
274 Field::new(
275 format_state_name(args.name, "min"),
276 DataType::Float64,
277 false,
278 ),
279 Field::new_list(
280 format_state_name(args.name, "centroids"),
281 Field::new_list_field(DataType::Float64, true),
282 false,
283 ),
284 ]
285 .into_iter()
286 .map(Arc::new)
287 .collect())
288 }
289
290 fn name(&self) -> &str {
291 "approx_percentile_cont"
292 }
293
294 fn signature(&self) -> &Signature {
295 &self.signature
296 }
297
298 #[inline]
299 fn accumulator(&self, acc_args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
300 Ok(Box::new(self.create_accumulator(&acc_args)?))
301 }
302
303 fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
304 if !arg_types[0].is_numeric() {
305 return plan_err!("approx_percentile_cont requires numeric input types");
306 }
307 if arg_types.len() == 3 && !arg_types[2].is_integer() {
308 return plan_err!(
309 "approx_percentile_cont requires integer centroids input types"
310 );
311 }
312 Ok(arg_types[0].clone())
313 }
314
315 fn supports_within_group_clause(&self) -> bool {
316 true
317 }
318
319 fn documentation(&self) -> Option<&Documentation> {
320 self.doc()
321 }
322}
323
324#[derive(Debug)]
325pub struct ApproxPercentileAccumulator {
326 digest: TDigest,
327 percentile: f64,
328 return_type: DataType,
329}
330
331impl ApproxPercentileAccumulator {
332 pub fn new(percentile: f64, return_type: DataType) -> Self {
333 Self {
334 digest: TDigest::new(DEFAULT_MAX_SIZE),
335 percentile,
336 return_type,
337 }
338 }
339
340 pub fn new_with_max_size(
341 percentile: f64,
342 return_type: DataType,
343 max_size: usize,
344 ) -> Self {
345 Self {
346 digest: TDigest::new(max_size),
347 percentile,
348 return_type,
349 }
350 }
351
352 pub(crate) fn max_size(&self) -> usize {
354 self.digest.max_size()
355 }
356
357 pub(crate) fn merge_digests(&mut self, digests: &[TDigest]) {
359 let digests = digests.iter().chain(std::iter::once(&self.digest));
360 self.digest = TDigest::merge_digests(digests)
361 }
362
363 pub(crate) fn convert_to_float(values: &ArrayRef) -> Result<Vec<f64>> {
365 debug_assert!(
366 values.null_count() == 0,
367 "convert_to_float assumes nulls have already been filtered out"
368 );
369 match values.data_type() {
370 DataType::Float64 => {
371 let array = downcast_value!(values, Float64Array);
372 Ok(array.values().iter().copied().collect::<Vec<_>>())
373 }
374 DataType::Float32 => {
375 let array = downcast_value!(values, Float32Array);
376 Ok(array.values().iter().map(|v| *v as f64).collect::<Vec<_>>())
377 }
378 DataType::Float16 => {
379 let array = downcast_value!(values, Float16Array);
380 Ok(array
381 .values()
382 .iter()
383 .map(|v| v.to_f64())
384 .collect::<Vec<_>>())
385 }
386 e => internal_err!(
387 "APPROX_PERCENTILE_CONT is not expected to receive the type {e:?}"
388 ),
389 }
390 }
391}
392
393impl Accumulator for ApproxPercentileAccumulator {
394 fn state(&mut self) -> Result<Vec<ScalarValue>> {
395 Ok(self.digest.to_scalar_state().into_iter().collect())
396 }
397
398 fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
399 let mut values = Arc::clone(&values[0]);
401 if values.null_count() > 0 {
402 values = filter(&values, &is_not_null(&values)?)?;
403 }
404 let sorted_values = &arrow::compute::sort(&values, None)?;
405 let sorted_values = ApproxPercentileAccumulator::convert_to_float(sorted_values)?;
406 self.digest = self.digest.merge_sorted_f64(&sorted_values);
407 Ok(())
408 }
409
410 fn evaluate(&mut self) -> Result<ScalarValue> {
411 if self.digest.count() == 0.0 {
412 return ScalarValue::try_from(self.return_type.clone());
413 }
414 let q = self.digest.estimate_quantile(self.percentile);
415
416 Ok(match &self.return_type {
419 DataType::Float16 => ScalarValue::Float16(Some(half::f16::from_f64(q))),
420 DataType::Float32 => ScalarValue::Float32(Some(q as f32)),
421 DataType::Float64 => ScalarValue::Float64(Some(q)),
422 v => unreachable!("unexpected return type {}", v),
423 })
424 }
425
426 fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
427 if states.is_empty() {
428 return Ok(());
429 }
430
431 let states = (0..states[0].len())
432 .map(|index| {
433 states
434 .iter()
435 .map(|array| ScalarValue::try_from_array(array, index))
436 .collect::<Result<Vec<_>>>()
437 .map(|state| TDigest::from_scalar_state(&state))
438 })
439 .collect::<Result<Vec<_>>>()?;
440
441 self.merge_digests(&states);
442
443 Ok(())
444 }
445
446 fn size(&self) -> usize {
447 size_of_val(self) + self.digest.size() - size_of_val(&self.digest)
448 + self.return_type.size()
449 - size_of_val(&self.return_type)
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use arrow::datatypes::DataType;
456
457 use datafusion_functions_aggregate_common::tdigest::TDigest;
458
459 use crate::approx_percentile_cont::ApproxPercentileAccumulator;
460
461 #[test]
462 fn test_combine_approx_percentile_accumulator() {
463 let mut digests: Vec<TDigest> = Vec::new();
464
465 for _ in 1..=50 {
467 let t = TDigest::new(100);
468 let values: Vec<_> = (1..=1_000).map(f64::from).collect();
469 let t = t.merge_unsorted_f64(values);
470 digests.push(t)
471 }
472
473 let t1 = TDigest::merge_digests(&digests);
474 let t2 = TDigest::merge_digests(&digests);
475
476 let mut accumulator =
477 ApproxPercentileAccumulator::new_with_max_size(0.5, DataType::Float64, 100);
478
479 accumulator.merge_digests(&[t1]);
480 assert_eq!(accumulator.digest.count(), 50_000.0);
481 accumulator.merge_digests(&[t2]);
482 assert_eq!(accumulator.digest.count(), 100_000.0);
483 }
484}