Skip to content

Null handling for the sketch-backed distinct counts, plus a filtered DISTINCTCOUNTTHETASKETCH group-by MV fix - #19316

Merged
Jackie-Jiang merged 1 commit into
apache:masterfrom
Jackie-Jiang:null_contract_distinct_count_sketches
Aug 20, 2026
Merged

Null handling for the sketch-backed distinct counts, plus a filtered DISTINCTCOUNTTHETASKETCH group-by MV fix#19316
Jackie-Jiang merged 1 commit into
apache:masterfrom
Jackie-Jiang:null_contract_distinct_count_sketches

Conversation

@Jackie-Jiang

@Jackie-Jiang Jackie-Jiang commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #19218.

DISTINCTCOUNTBITMAP, DISTINCTCOUNTHLL, DISTINCTCOUNTHLLPLUS, DISTINCTCOUNTULL,
DISTINCTCOUNTTHETASKETCH, DISTINCTCOUNTCPCSKETCH, FASTHLL, SEGMENTPARTITIONEDDISTINCTCOUNT
and 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 DISTINCTCOUNTHLL over a
column of all nulls answered 1 — the default counted once — instead of 0.

They now extend NullableSingleInputAggregationFunction and skip null rows when the option is
enabled. The change reaches every aggregation path: aggregate, aggregateGroupBySV and
aggregateGroupByMV, single-value and multi-value columns, and the dictionary, raw and
serialized-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 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. That distinction is per-class and was checked one class at a time rather
than 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#addN over an entire dictionary-id array, and a subList of the entire string array
in the smart sketch base. Both took every row regardless of the null bitmap.

A pre-existing wrong-results fix, pulled in

DistinctCountThetaSketchAggregationFunction#aggregateGroupByMV indexed the group keys and the
multi-value arrays by the filter ordinal instead of by the row — groupKeysArray[filterIndex],
where filterIndex counts filters. With a single 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.

The plan was to send this separately, but 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 for
DISTINCTCOUNTTHETASKETCH(sketchCol, params, predicate, '$1') … GROUP BY mvCol. Fixing one of a
pair and deferring its twin 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.

Testing

DistinctCountSketchNullHandlingTest is new. It covers what
AggregationFunctionNullContractTest cannot: that harness 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. DistinctCountULLAggregationFunctionTest and
SegmentPartitionedDistinctCountAggregationFunctionTest gain 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
DistinctCountThetaSketchAggregationFunctionTest for the single-value, multi-value and string
branches, plus the null-hole case in DistinctCountSketchNullHandlingTest. Run against the pre-fix
commit all four fail with the specific wrong answers — every row collapsed onto one group — rather
than merely throwing.

AggregationFunctionNullContractTest pins the nineteen types that now honour the option. That set
came from a run, not a prediction.

@Jackie-Jiang Jackie-Jiang added query Related to query processing functions Related to scalar or aggregation functions null support Related to NULL value handling backward-incompat Introduces a backward-incompatible API or behavior change release-notes Referenced by PRs that need attention when compiling the next release notes labels Aug 19, 2026
@Jackie-Jiang
Jackie-Jiang requested review from xiangfu0 and yashmayya and a balanced review from Copilot August 19, 2026 18:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@yashmayya yashmayya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 31.26791% with 1919 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.01%. Comparing base (2ae7195) to head (d36aaec).

Files with missing lines Patch % Lines
...n/DistinctCountThetaSketchAggregationFunction.java 51.93% 265 Missing and 70 partials ⚠️
.../function/DistinctCountULLAggregationFunction.java 14.23% 224 Missing and 5 partials ⚠️
...nction/DistinctCountBitmapAggregationFunction.java 13.76% 206 Missing and 7 partials ⚠️
...ntPartitionedDistinctCountAggregationFunction.java 21.40% 209 Missing and 4 partials ⚠️
...ction/DistinctCountHLLPlusAggregationFunction.java 26.11% 194 Missing and 4 partials ⚠️
...seDistinctCountSmartSketchAggregationFunction.java 7.06% 171 Missing ⚠️
.../function/DistinctCountHLLAggregationFunction.java 38.32% 163 Missing and 6 partials ⚠️
...ion/DistinctCountCPCSketchAggregationFunction.java 10.19% 137 Missing and 4 partials ⚠️
...tion/DistinctCountSmartHLLAggregationFunction.java 3.12% 62 Missing ⚠️
...tion/DistinctCountSmartULLAggregationFunction.java 1.66% 59 Missing ⚠️
... and 6 more
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     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.01% <31.26%> (+27.84%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.01% <31.26%> (+27.84%) ⬆️
unittests 67.01% <31.26%> (+27.84%) ⬆️
unittests1 57.71% <31.26%> (?)
unittests2 38.96% <0.28%> (-0.21%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jackie-Jiang
Jackie-Jiang force-pushed the null_contract_distinct_count_sketches branch from d117b35 to acecb18 Compare August 20, 2026 00:56
…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.
@Jackie-Jiang
Jackie-Jiang force-pushed the null_contract_distinct_count_sketches branch from acecb18 to d36aaec Compare August 20, 2026 19:31
@Jackie-Jiang Jackie-Jiang changed the title Give the sketch-backed distinct count functions the query's null handling option Null handling for the sketch-backed distinct counts, plus a filtered DISTINCTCOUNTTHETASKETCH group-by MV fix Aug 20, 2026
@Jackie-Jiang Jackie-Jiang added the bug Something is not working as expected label Aug 20, 2026
@Jackie-Jiang
Jackie-Jiang requested a balanced review from Copilot August 20, 2026 21:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Jackie-Jiang
Jackie-Jiang merged commit 7adf93f into apache:master Aug 20, 2026
16 of 17 checks passed
@Jackie-Jiang
Jackie-Jiang deleted the null_contract_distinct_count_sketches branch August 20, 2026 21:52
xiangfu0 added a commit to pinot-contrib/pinot-docs that referenced this pull request Aug 20, 2026
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>
@xiangfu0

Copy link
Copy Markdown
Contributor

Documentation follow-up: pinot-contrib/pinot-docs#999 (merged)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backward-incompat Introduces a backward-incompatible API or behavior change bug Something is not working as expected functions Related to scalar or aggregation functions null support Related to NULL value handling query Related to query processing release-notes Referenced by PRs that need attention when compiling the next release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants