Null handling for the sketch-backed distinct counts, plus a filtered DISTINCTCOUNTTHETASKETCH group-by MV fix - #19316
Conversation
yashmayya
left a comment
There was a problem hiding this comment.
Focused on correctness. The mechanical part is clean: I checked every wrapped range in the diff and found no loop bound left at length, no wrong row variable, and the group-by paths of all ten classes consistent.
Two paths were missed, both the non-grouped aggregate over a serialized-sketch column, and both inconsistent with their own group-by siblings. Third comment is a pre-existing bug this PR's rename brought into focus.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19316 +/- ##
=============================================
+ Coverage 39.17% 67.01% +27.84%
- Complexity 1423 1424 +1
=============================================
Files 3462 3462
Lines 220695 221647 +952
Branches 35258 34951 -307
=============================================
+ Hits 86447 148545 +62098
+ Misses 126373 61267 -65106
- Partials 7875 11835 +3960
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
d117b35 to
acecb18
Compare
…DISTINCTCOUNTTHETASKETCH group-by MV fix DISTINCTCOUNTBITMAP, DISTINCTCOUNTHLL, DISTINCTCOUNTHLLPLUS, DISTINCTCOUNTULL, DISTINCTCOUNTTHETASKETCH, DISTINCTCOUNTCPCSKETCH, FASTHLL, SEGMENTPARTITIONEDDISTINCTCOUNT and the raw and smart variants of each never received the query's null handling option, so a null row was aggregated as the column default no matter what the query asked for. They now extend NullableSingleInputAggregationFunction and skip null rows when the option is enabled, across aggregate, aggregateGroupBySV and aggregateGroupByMV, for single-value and multi-value columns, and for the dictionary, raw and serialized-sketch input paths. Nothing changes with the option disabled: null rows are still read as the column default, which is the answer that mode has always given. The empty-input answer is unchanged in every case. Each counting function already returned 0 for a null intermediate result, and the extractors that substitute an empty accumulator are untouched, so the raw CPC and theta sketch variants still render a serialized empty sketch for a locally empty segment rather than NULL. Two range operations that are not loops needed the same treatment, since a loop-shaped fix does not reach them: RoaringBitmap#addN over a whole dictionary id array, and a subList of the whole string array in the smart sketch base. This also fixes a pre-existing wrong-results bug in DistinctCountThetaSketchAggregationFunction#aggregateGroupByMV, which indexed the group keys and the multi-value arrays by the filter ordinal rather than by the row: groupKeysArray[filterIndex] where filterIndex counts filters. With one filter, every matching row was credited to row 0's group keys using row 0's values. Seventeen subscripts across the single-value and multi-value branches; get(filterIndex + 1), which really is a filter ordinal, is unchanged. One of the seventeen could not be deferred. Making deserializeSketches null-aware leaves null in the array for a null row, and that subscript reads the array, so CustomObjectAccumulator#apply's null check turned the wrong answer into an NPE. Fixing one of a pair and deferring the rest reads worse than fixing both, so the whole set is here. A filtered DISTINCTCOUNTTHETASKETCH grouped by a multi-value column therefore returns different, correct, results after this change, independently of the null handling option. DistinctCountSketchNullHandlingTest covers the paths the generic contract test cannot reach: it drives one synthetic single-value block through aggregate only, so the group-by paths, the multi-value column paths and the serialized sketch input are checked nowhere else. The dictionary-encoded multi-value cases were checked by mutation, and the four indexing tests fail against the pre-fix commit with the specific wrong answers rather than merely throwing. AggregationFunctionNullContractTest pins the nineteen types that now honour the option. The set was taken from a run rather than predicted.
acecb18 to
d36aaec
Compare
Documents advanced null-handling behavior added by apache/pinot#19316 for sketch-backed distinct-count functions across regular, group-by, multi-value, dictionary, and serialized-sketch paths. Upstream: apache/pinot#19316 Co-authored-by: Xiang Fu <xiangfu@Xiang-mac-mtv-2.local>
|
Documentation follow-up: pinot-contrib/pinot-docs#999 (merged) |
Summary
Part of #19218.
DISTINCTCOUNTBITMAP,DISTINCTCOUNTHLL,DISTINCTCOUNTHLLPLUS,DISTINCTCOUNTULL,DISTINCTCOUNTTHETASKETCH,DISTINCTCOUNTCPCSKETCH,FASTHLL,SEGMENTPARTITIONEDDISTINCTCOUNTand the raw and smart variants of each never received the query's null handling option. A null row
was aggregated as the column default whatever the query asked for, so
DISTINCTCOUNTHLLover acolumn of all nulls answered
1— the default counted once — instead of0.They now extend
NullableSingleInputAggregationFunctionand skip null rows when the option isenabled. The change reaches every aggregation path:
aggregate,aggregateGroupBySVandaggregateGroupByMV, single-value and multi-value columns, and the dictionary, raw andserialized-sketch input paths.
What does not change
With the option disabled, null rows are still read as the column default. That is the answer this
mode has always given and is a backward-compatibility constraint, not an oversight.
The empty-input answer is unchanged in every case. Each counting function already returned
0for anull intermediate result, and the extractors that substitute an empty accumulator are untouched — so
the raw CPC and theta sketch variants still render a serialized empty sketch for a locally empty
segment rather than
NULL. That distinction is per-class and was checked one class at a time ratherthan assumed.
Two things a loop-shaped fix does not reach
Not every read of the block is a loop. Two range operations needed the same treatment:
RoaringBitmap#addNover an entire dictionary-id array, and asubListof the entire string arrayin the smart sketch base. Both took every row regardless of the null bitmap.
A pre-existing wrong-results fix, pulled in
DistinctCountThetaSketchAggregationFunction#aggregateGroupByMVindexed the group keys and themulti-value arrays by the filter ordinal instead of by the row —
groupKeysArray[filterIndex],where
filterIndexcounts filters. With a single filter, every matching row was credited to row 0'sgroup keys using row 0's values. Seventeen subscripts across the single-value and multi-value
branches.
.get(filterIndex + 1), which really is a filter ordinal, is unchanged.The plan was to send this separately, but one of the seventeen could not be deferred. Making
deserializeSketchesnull-aware leavesnullin the array for a null row, and that subscript readsthe array, so
CustomObjectAccumulator#apply's null check turned the wrong answer into an NPE forDISTINCTCOUNTTHETASKETCH(sketchCol, params, predicate, '$1') … GROUP BY mvCol. Fixing one of apair and deferring its twin reads worse than fixing both, so the whole set is here.
A filtered
DISTINCTCOUNTTHETASKETCHgrouped by a multi-value column therefore returns different,correct, results after this change — independently of the null handling option.
Testing
DistinctCountSketchNullHandlingTestis new. It covers whatAggregationFunctionNullContractTestcannot: that harness drives one synthetic single-value blockthrough
aggregateonly, so the group-by paths, the multi-value column paths and the serializedsketch input are checked nowhere else.
DistinctCountULLAggregationFunctionTestandSegmentPartitionedDistinctCountAggregationFunctionTestgain enabled/disabled multi-value cases.The multi-value and dictionary-encoded cases were verified by mutation rather than by inspection:
reverting a wrapped range back to the full block makes them fail with the null rows counted, so they
guard the behaviour instead of passing vacuously.
The indexing fix adds filtered group-by-MV cases to
DistinctCountThetaSketchAggregationFunctionTestfor the single-value, multi-value and stringbranches, plus the null-hole case in
DistinctCountSketchNullHandlingTest. Run against the pre-fixcommit all four fail with the specific wrong answers — every row collapsed onto one group — rather
than merely throwing.
AggregationFunctionNullContractTestpins the nineteen types that now honour the option. That setcame from a run, not a prediction.