Support null handling in dictionary-based and multi-value group key generation - #19390
Conversation
a21e768 to
2b33578
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19390 +/- ##
============================================
+ Coverage 67.59% 67.63% +0.04%
Complexity 1430 1430
============================================
Files 3487 3487
Lines 224320 224326 +6
Branches 35417 35408 -9
============================================
+ Hits 151623 151721 +98
+ Misses 60679 60574 -105
- Partials 12018 12031 +13
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:
|
2b33578 to
c30ab5d
Compare
|
Thanks — both confirmed and fixed in 1. Dictionary-backed transforms collapse NULL into the default value. Correct, and the diagnosis is exact. The nullability check is now conservative in the right direction: a column read from a segment can prove it has no nulls via its null value vector, so it still reserves nothing; anything without a data source reserves an id. boolean tracksNulls = dataSource == null || dataSource.getNullValueVector() != null;Regression test: 2. Primitive null groups excluded from group accounting. Confirmed, and both methods now return One correction to the attribution, which matters for backporting: this is pre-existing on master, not new to the MV path. Regression test: |
c30ab5d to
2cd84a8
Compare
|
Confirmed and fixed in
if (_nullHandlingEnabled) {
RoaringBitmap nullBitmap = blockValSet.getNullBitmap();
if (nullBitmap != null && !nullBitmap.isEmpty()) {
PeekableIntIterator nullIterator = nullBitmap.getIntIterator();
while (nullIterator.hasNext()) {
ids[nullIterator.next()] = _nullComponent;
}
}
}Reusing the existing Regression test: Same note as before on provenance: this one is also pre-existing on master — its MV resolver has no null-bitmap reference either. My "every group key generator is null-aware" wording in the description was wrong as written, since the grouping-sets MV path was outside the change; it is accurate now that this is fixed. |
2cd84a8 to
f402575
Compare
|
All four applied in Dictionary path for dict-encoded columns beside raw ones. Done — the selection is now just Shared generator across project operators. Comment added at the decision point, recording that Unit coverage with Empty MV array. Added to the description under behavior changes, framed as you put it — one NULL group is not the Postgres answer, it is the closest one reachable given that Pinot ingests On the |
Documents the observable group-by corrections from apache/pinot#19390. - explains distinct SQL NULL groups across dictionary and raw encodings - covers empty multi-value arrays and group-limit attribution - calls out upgrade impact for dashboards and clients Validation: `scripts/validate-docs.py --changed-only=... --strict`; `git diff --check` Co-authored-by: Xiang Fu <xiangfu@Xiang-mac-mtv-2.local>
|
Documentation follow-up: pinot-contrib/pinot-docs#1026 (merged). |
Resolves conflicts with apache#19390 (null handling in dictionary-based group key generation): keep the null-handling additions and the new nullHandlingEnabled constructor parameter, and keep the optimized-bound logic from this PR (holder type selection from the full cardinality product), dropping the pre-fix optimization block reintroduced by the apache#19390 base revision. Also addresses review feedback: the Collectors.toMap in getGroupByExpressionSizesFromPredicates needs Integer::min as the merge function so duplicate group-by expressions (GROUP BY c0, c0) do not throw when the option is enabled; added a regression case to the capacity data provider.
Summary
With null handling enabled, a group-by query was routed to the no-dictionary group key generators regardless of how its columns are encoded, and the multi-value key path ignored nulls entirely. This PR makes every group key generator null-aware, so the routing can go back to being decided by column encoding alone.
Dictionary-based group key generation
With null handling enabled, a column whose segment tracks nulls reserves one further dictionary id, one past its last real id, to stand for a null value; its cardinality counts one more than the dictionary holds. A block's null rows are moved onto the reserved id before key composition (copying the block's id array, which is shared with other readers of the column), so the per-row kernels and the raw key arithmetic are untouched. The reserved id composes into the raw key like any other value and is read back out as SQL
NULL.A column that tracks no nulls reserves nothing, and neither does any column when null handling is disabled, which leaves the cardinalities, the holder selection and the block reads exactly as they are today. Dictionary-encoded group-by columns therefore keep the dictionary-based generator in both modes, instead of paying for per-row hash lookups on the no-dictionary path.
Multi-value key path of the no-dictionary generators
The
int[][]overloads of both no-dictionary generators read every column without consulting its null bitmap, so a null row was grouped under the column's default null value instead of under a group of its own — for every column on that path, single-value columns included. Both overloads now recognize nulls: a null row contributes oneNULLgroup, mirroring the single-value behavior and the row's physical storage as a one-element default value.With that closed, the null-enabled and null-disabled twin loops in both generators are merged: every iteration on these paths pays for a hash-map operation, which dwarfs the predicted per-row null check the merge adds, and both files end up smaller than before while doing more.
Bugfix: rows misattributed once the group limit is reached
The null-enabled at-limit path of
NoDictionaryMultiColumnGroupKeyGeneratorresolved each column's key value but never stored it, composing whatever the key buffer held from the previous row — which, after a new group's buffer swap, is{0, 0, ...}: exactly the first group's key. With null handling enabled andnumGroupsLimitreached, rows belonging to existing groups could be counted into the first group or dropped. The resolved value is now stored, same as the null-disabled twin always did.Behavior changes
All confined to queries that enable null handling:
NULLgroup. Pinot ingests an empty array as null, so[]and SQL NULL are indistinguishable by the time a query reads them, and an empty multi-value row has no representation in a segment. This is not the Postgres answer, whereunnest('{}')contributes no rows at all — it is the closest one reachable here, and it beats the previous behaviour of folding those rows into the column's default null value.NULLgroup; previously those rows were grouped under the column's default null value.numGroupsLimit, matching the null-handling-disabled behavior.With null handling disabled nothing changes: no id is reserved, the routing condition is the encoding check it used to be, and the per-row loops of the dictionary-based generator are byte-identical.
Tests
NullHandlingEnabledQueriesTestgains nine query-level tests covering every generator and key path with nulls: dictionary-encoded SV (including a row holding the column's default null value, which must not join theNULLgroup), two dictionary columns, dictionary MV, raw SV (INT and STRING, which exercise the primitive-map and object-map null keys respectively), raw MV, mixed raw/dictionary SV and MV, and a regression test for the group-limit misattribution that fails on the previous code by construction.DictionaryBasedGroupKeyGeneratorTestadditionally coversIntGroupIdMap.clearAndTrimpast the caching threshold.