fix: correct percentile_cont(DISTINCT) accumulation and sliding-window retract - #23913
Conversation
…w retract `DistinctPercentileContAccumulator` had two bugs: 1. Panic on every distinct query. `update_batch` forwarded all argument columns to `GenericDistinctBuffer::update_batch`, which asserts a single input array. `percentile_cont` always passes two columns (the value and the percentile literal), so any `percentile_cont(DISTINCT x, p)` panicked with "DistinctValuesBuffer::update_batch expects only a single input array". 2. Wrong results in sliding windows. The buffer was a plain `HashSet` with no per-value multiplicity, so `retract_batch` removed a value entirely when a single occurrence left the window frame, even if other rows in the frame still carried that value. Replace the shared set-based buffer with a per-accumulator `HashMap<Hashable, usize>` count map: `update_batch` reads only the value column and increments counts, `retract_batch` decrements and removes a key only at zero, and `state`/`merge_batch` serialize the distinct keys as a List (unchanged state shape). The other `GenericDistinctBuffer` users (count/sum/ median/variance distinct) are unaffected; only percentile_cont supports retract, so it is the sole accumulator that needed multiplicity tracking. Add regression coverage in aggregate.slt for the plain distinct aggregate and for the sliding-window duplicate-value retract case. Co-authored-by: Claude Code
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #23913 +/- ##
=======================================
Coverage 80.67% 80.67%
=======================================
Files 1094 1094
Lines 371770 371790 +20
Branches 371770 371790 +20
=======================================
+ Hits 299907 299930 +23
+ Misses 53964 53954 -10
- Partials 17899 17906 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks @Jefffrey for review |
| struct DistinctPercentileContAccumulator<T: ArrowNumericType> { | ||
| distinct_values: GenericDistinctBuffer<T>, | ||
| /// Distinct value -> number of in-window rows carrying it. | ||
| counts: HashMap<Hashable<T::Native>, usize>, |
There was a problem hiding this comment.
GenericDistinctBuffer previously used foldhash, but this will switch to using siphash, which is a lot slower.
There was a problem hiding this comment.
Good catch — switched the count map to the foldhash RandomState that GenericDistinctBuffer uses, instead of the std default SipHash. Fixed in #23946.
| SELECT id, percentile_cont(DISTINCT x, 0.5) | ||
| OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) | ||
| FROM distinct_pct; |
There was a problem hiding this comment.
This needs an ORDER BY to be deterministic, or else use rowsort SLT directive.
There was a problem hiding this comment.
Added ORDER BY id to make the row order deterministic. Fixed in #23946.
| size_of_val(self) + self.distinct_values.size() | ||
| size_of_val(self) | ||
| + self.counts.capacity() | ||
| * (size_of::<Hashable<T::Native>>() + size_of::<usize>()) |
There was a problem hiding this comment.
estimate_memory_size::<(Hashable<T::Native>, usize)>(self.counts.capacity(), size_of_val(self)) is better I think.
There was a problem hiding this comment.
Switched to estimate_memory_size::<(Hashable<T::Native>, usize)>(self.counts.capacity(), size_of_val(self)). Fixed in #23946.
| if let Some(count) = self.counts.get_mut(&Hashable(value)) { | ||
| *count -= 1; | ||
| if *count == 0 { | ||
| self.counts.remove(&Hashable(value)); | ||
| } | ||
| } |
There was a problem hiding this comment.
wdyt of raising an error if the caller attempts to retract a value that isn't in the map?
There was a problem hiding this comment.
I left retract_batch silently skipping a value that isn't in the map rather than erroring. The sliding-window executor only retracts values it previously passed to update_batch, so a missing key shouldn't happen in practice — but if it ever did (a bug elsewhere), erroring would turn a latent inconsistency into a hard query failure, whereas skipping keeps the window's remaining counts intact. I'm not strongly attached either way; if you'd prefer a debug_assert! (catches it in tests/debug builds without failing production queries), I'm happy to add that instead. What do you think?
There was a problem hiding this comment.
If we try to retract a missing value, seems that we are likely to eventually produce incorrect query results, so I'd argue it is better to raise an error here rather than to silently proceed and return the wrong answer. I'd personally argue for internal_err!; but if you disagree, then I'd say debug_assert! is better than nothing 😊
There was a problem hiding this comment.
Agreed — returning a wrong answer is worse than failing loudly here. Went with internal_err! in #23946: retracting a value not in the count map now surfaces as an error rather than silently proceeding. Thanks!
| let arr = values[0].as_primitive::<T>(); | ||
| for value in arr.iter().flatten() { | ||
| *self.counts.entry(Hashable(value)).or_default() += 1; | ||
| } |
There was a problem hiding this comment.
GenericDistinctBuffer has a fast-path for null-free arrays; might be worth adopting here as well.
There was a problem hiding this comment.
Adopted the same null-free fast path in update_batch and retract_batch. Fixed in #23946.
|
Btw I notice that CodeCov says the test coverage is only 41%, which seems low? |
Add a grouped percentile_cont(DISTINCT ...) regression test. A GROUP BY with target_partitions=4 forces two-phase (Partial + FinalPartitioned) aggregation, so the distinct accumulator's state() and merge_batch() paths run — the paths the previous whole-table regression queries did not reach (flagged by patch-coverage review on apache#23913). Duplicate values within each group also verify multiplicity is preserved across the per-partition merge. Co-authored-by: Claude Code
|
On the coverage: the uncovered lines were mostly I've added a grouped |
…lator (apache#23946) ## Rationale for this change Follow-up to post-merge review feedback from @neilconway on apache#23913. ## What changes are included in this PR? - Use the fast foldhash `RandomState` for the distinct-value count map instead of the standard library's default SipHash (the shared `GenericDistinctBuffer` already does this; the merged fix regressed to SipHash on this hot path). - Use `estimate_memory_size` in `size()` instead of a hand-rolled capacity calculation. - Adopt the null-free fast path in `update_batch`/`retract_batch` (skip per-element validity checks when the input has no nulls), mirroring `GenericDistinctBuffer`. - Add `ORDER BY` to the sliding-window regression test so its row order is deterministic. ## Are these changes tested? Yes — existing percentile unit tests and the full `aggregate.slt` pass. ## Are there any user-facing changes? No.
Which issue does this PR close?
Rationale for this change
DistinctPercentileContAccumulatorreused the shared set-basedGenericDistinctBuffer, which doesn't fit it: (1) the buffer asserts a single input column butpercentile_contpasses two (value + percentile), so everypercentile_cont(DISTINCT ...)panicked; (2) the buffer is a plainHashSetwith no multiplicity, so sliding-windowretract_batchdropped a value while duplicates were still in the frame.What changes are included in this PR?
HashMap<Hashable, usize>count map:update_batchreads only the value column and increments;retract_batchdecrements and removes a key only at zero;state/merge_batchkeep the same List state shape. OtherGenericDistinctBufferusers are untouched.aggregate.sltfor the plain distinct query and the sliding-window duplicate-retract case.Are these changes tested?
Yes — new regression tests; the full
aggregate.sltsuite passes.Are there any user-facing changes?
percentile_cont(DISTINCT ...)now works instead of panicking, and returns correct results in sliding windows.