feat: decimal support for percentile_cont and median - #24419
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24419 +/- ##
==========================================
+ Coverage 81.23% 81.33% +0.09%
==========================================
Files 1111 1117 +6
Lines 390208 396050 +5842
Branches 390208 396050 +5842
==========================================
+ Hits 316990 322113 +5123
- Misses 54591 55101 +510
- Partials 18627 18836 +209 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
CC @neilconway |
Jefffrey
left a comment
There was a problem hiding this comment.
Integer support for
percentile_cont(backported frommedianUDF)
this doesnt sound right, since this is continuous and not discrete so it must always be a float (or decimal)
| #[derive(PartialEq, Eq, Hash, Debug)] | ||
| pub struct Median { | ||
| signature: Signature, | ||
| percentile_cont: PercentileCont, |
There was a problem hiding this comment.
i do wonder if we should implement simplify for median to make it an unconditional rewrite to percentile_cont
There was a problem hiding this comment.
We've done forwarding calls to a child UDF before in other functions. I can try a rewrite approach and tell if it is more concise - I thought it could hurt programmatic usage in tests (like the mentioned test_oom)
There was a problem hiding this comment.
Well, I've tried, but the simplifier has a flaw with the window aggregation functions (median(x) OVER ) - it is unable to call a simplifier and just falls back to a normal UDAF accumulator, making the rewrite irrelevant.
This could be a reason why approx_percentile_cont doesn't do rewrites either. I can try fixing these simplifier bugs later.
4. query failed: DataFusion error: Internal error: median accumulator should have been simplified to standard percentile_cont.
This issue was likely caused by a bug in DataFusion's code. Please help us to resolve this by filing a bug report in our issue tracker: https://github.com/apache/datafusion/issues
[SQL] SELECT
timestamp,
tags,
value,
median(DISTINCT value) OVER (
PARTITION BY tags
ORDER BY timestamp
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS distinct_median
FROM median_window_test
ORDER BY tags, timestamp;
at /Users/irix/projects/third-party/datafusion/datafusion/sqllogictest/test_files/aggregate.slt:1080
With median distinct, it is also failing with some conflicting optimisation with group by - the plan omits grouping entirely
[SQL] explain select median(distinct c) from t;
[Diff] (-expected|+actual)
logical_plan
- 01)Projection: median(alias1) AS median(DISTINCT t.c)
- 02)--Aggregate: groupBy=[[]], aggr=[[median(alias1)]]
- 03)----Aggregate: groupBy=[[CAST(t.c AS Float64) AS alias1]], aggr=[[]]
- 04)------TableScan: t projection=[c]
+ 01)Aggregate: groupBy=[[]], aggr=[[percentile_cont(DISTINCT CAST(t.c AS Float64), Float64(0.5)) AS median(DISTINCT t.c)]]
+ 02)--TableScan: t projection=[c]
physical_plan
- 01)ProjectionExec: expr=[median(alias1)@0 as median(DISTINCT t.c)]
- 02)--AggregateExec: mode=Final, gby=[], aggr=[median(alias1)]
- 03)----CoalescePartitionsExec
- 04)------AggregateExec: mode=Partial, gby=[], aggr=[median(alias1)]
- 05)--------AggregateExec: mode=FinalPartitioned, gby=[alias1@0 as alias1], aggr=[]
- 06)----------RepartitionExec: partitioning=Hash([alias1@0], 4), input_partitions=1
- 07)------------AggregateExec: mode=Partial, gby=[CAST(c@0 AS Float64) as alias1], aggr=[]
- 08)--------------DataSourceExec: partitions=1, partition_sizes=[1]
+ 01)AggregateExec: mode=Single, gby=[], aggr=[percentile_cont(DISTINCT t.c, 0.5) as median(DISTINCT t.c)]
+ 02)--DataSourceExec: partitions=1, partition_sizes=[1]
at /Users/irix/projects/third-party/datafusion/datafusion/sqllogictest/test_files/aggregate.slt:1502
So, in the middle ground, I refactored the code a bit to avoid schema twiddling in median - now we just create accumulators explicitly, and it looks more concise.
There was a problem hiding this comment.
thanks for checking; the udaf simplifier does seem a bit more tricky to work with compared to udf 🤔
| struct DecimalInterpolator; | ||
|
|
||
| /// Precision multiplier for decimal linear interpolation calculations. | ||
| fn deduce_interpolation_precision<T: DecimalType>() -> usize { |
There was a problem hiding this comment.
is there specific reasoning chosen for these values?
There was a problem hiding this comment.
The first constant 10000 is chosen for Decimal32 with 4-byte native type specifically so that scale_by_num doesn't overflow when computing a decomposition. The second one is just the same as FLOAT_INTERPOLATION_PRECISION - I can reuse the same value for clarity
There was a problem hiding this comment.
i mainly ask since i had codex check some of the math and it mentioned some edge cases where the wrong value can be returned, for example:
> SELECT percentile_cont(0.00009) WITHIN GROUP (ORDER BY v)
FROM (VALUES (arrow_cast(0, 'Decimal32(9,0)')), (arrow_cast(999999999, 'Decimal32(9,0)'))) as t (v);
+---------------------------------------------------------------------+
| percentile_cont(Float64(0.00009)) WITHIN GROUP [t.v ASC NULLS LAST] |
+---------------------------------------------------------------------+
| 0 |
+---------------------------------------------------------------------+
1 row(s) fetched.
Elapsed 0.010 seconds.- this is only for decimal32, decimal64 & above seems fine
for reference, if done on float:
> SELECT percentile_cont(0.00009) WITHIN GROUP (ORDER BY v)
FROM (VALUES (0), (999999999)) as t (v);
+---------------------------------------------------------------------+
| percentile_cont(Float64(0.00009)) WITHIN GROUP [t.v ASC NULLS LAST] |
+---------------------------------------------------------------------+
| 89999.99991 |
+---------------------------------------------------------------------+
1 row(s) fetched.
Elapsed 0.010 seconds.i havent looked too closely at the interpolation maths, but i wonder if this is something we should try fix if possible? or can do in followup
There was a problem hiding this comment.
for reference too, from duckdb:
memory D select percentile_cont(0.00009) within group (order by v) from values (0::decimal(9,0)), (999999999::decimal(9,0)) t(v);
┌───────────────────────────────────┐
│ quantile_cont(0.00009 ORDER BY v) │
│ decimal(9,0) │
├───────────────────────────────────┤
│ 89999 │
└───────────────────────────────────┘There was a problem hiding this comment.
It's a good catch. Let me also generate some edge cases from other DB codebases as well (and especially for smaller decimals), and I'll return in a follow-up PR
Which issue does this PR close?
medianandpercentile_contimplementations (preserve Decimal inpercentile_cont, aliasmedian) #22042.Rationale for this change
UDF
percentile_contuses floating-point arithmetic to find a percentile, which doesn't work well for decimals. ThemedianUDF already supports decimals. Let's implement decimals inpercentile_contand buildmedianon top of it.A follow-up to #21988, refines #23954.
What changes are included in this PR?
percentile_cont, without float coercionmedianon top ofpercentile_contUDF by prepending a0.5fargumentpercentile_cont(NULL)return NULL (see changes below), under a new SLT test casetest_oomto call UDF directly with float parameters (it used integers before without adhering to coercion rules)Are these changes tested?
percentile_contAre there any user-facing changes?
percentile_cont(NULL)now returns a NULL type instead of a Float64 type, aligning with other aggregate UDFs