Skip to content

Fix group key generation when optimizeMaxInitialResultHolderCapacity shrinks the cardinality product - #19379

Open
xiangfu0 wants to merge 1 commit into
apache:masterfrom
xiangfu0:fix-optimized-groupby-holder-selection
Open

Fix group key generation when optimizeMaxInitialResultHolderCapacity shrinks the cardinality product#19379
xiangfu0 wants to merge 1 commit into
apache:masterfrom
xiangfu0:fix-optimized-groupby-holder-selection

Conversation

@xiangfu0

Copy link
Copy Markdown
Contributor

Problem

With the query option optimizeMaxInitialResultHolderCapacity=true, DictionaryBasedGroupKeyGenerator shrinks the group-by cardinality product to the IN/EQ predicate sizes and then uses the shrunk value both for holder-type selection and for _globalGroupIdUpperBound. This is unsound in three ways:

  1. ArrayIndexOutOfBoundsException: ArrayBasedHolder uses raw dictionary-id mixed-radix products as group ids directly, so its _flags array (sized to the shrunk bound) is indexed by ids up to the full cardinality product. Repro: dict INT column dInt with cardinality 1000, SET optimizeMaxInitialResultHolderCapacity=true; SELECT dInt, COUNT(*) FROM t WHERE dInt IN (1,2,3,4,5) GROUP BY dInt throws AIOOBE: Index 5 out of bounds for length 5 in ArrayBasedHolder.markGroups.
  2. Silent wrong results: resetting longOverflow = false and shrinking the product can downgrade the holder to IntMapBasedHolder/LongMapBasedHolder, whose raw-key arithmetic still uses the full cardinalities — the keys overflow int/long and distinct groups collide.
  3. Multi-value group-by columns: a row matching an IN/EQ predicate on an MV column contributes one group per value inside the row, not only the matching values, so the predicate size is not a valid bound on the number of distinct groups (this also affects the two no-dictionary group key generators).

Fix

  • Holder-type selection always uses the full cardinality product (raw keys span the full mixed-radix space regardless of the filter). The predicate-derived value only caps _globalGroupIdUpperBound, which is a valid dense group-count bound for the map-based holders.
  • When the optimized bound shrinks below the full product, ArrayBasedHolder falls back to IntMapBasedHolder, which maps the sparse raw keys onto dense group ids. Inside the ArrayBasedHolder branch, _globalGroupIdUpperBound == cardinalityProduct now holds by construction. This deliberately trades a hash lookup per row for the smaller result holders the opt-in option asks for; the alternative (keep ArrayBasedHolder with the full bound whenever the product fits under arrayBasedThreshold) would silently turn the option into a no-op for every small-cardinality group-by, changing its documented, tested behavior. With the option off (the default), holder selection is bit-identical to before.
  • DefaultGroupByExecutor#getGroupByExpressionSizesFromPredicates excludes multi-value group-by expressions from the predicate-size map, covering all three group key generators, and DictionaryBasedGroupKeyGenerator also self-enforces the exclusion (it computes the bound from its own _isSingleValueColumn/_cardinalities arrays, which additionally removes the per-query cardinalityMap allocation and keeps the bound comparable to the cardinality product when the same expression appears multiple times in the GROUP BY).

Known adjacent follow-up (not in this PR): NoDictionaryMultiColumnGroupKeyGenerator does not cap its optimized upper bound by numGroupsLimit (not a correctness issue — the limit is enforced separately at group creation — but it oversizes result holder max capacity), and NoDictionaryGroupKeyGeneratorTest has no coverage with a non-null predicate-size map.

Tests

DictionaryBasedGroupKeyGeneratorTest additions (each verified to fail before the fix):

  • testOptimizedUpperBoundSmallerThanCardinalityProduct — the AIOOBE repro; now served by IntMapBasedHolder.
  • testOptimizedUpperBoundKeepsLongMapBasedHolder / testOptimizedUpperBoundKeepsArrayMapBasedHolder — the optimization must not downgrade the holder type across the int/long overflow boundaries.
  • testOptimizedUpperBoundMatchingCardinalityProductKeepsArrayBasedHolder — no deoptimization when the predicates do not prove fewer groups.
  • testOptimizedUpperBoundIgnoresMultiValuePredicates — MV predicate sizes must not shrink the bound.
  • The pre-existing testGetGroupByResultHolderCapacity now also runs process() over the block, so all 12 capacity cases exercise actual key generation (8 of them threw AIOOBE before the fix).

@codecov-commenter

codecov-commenter commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.54%. Comparing base (bbbed25) to head (dbe1598).

Files with missing lines Patch % Lines
...tion/groupby/DictionaryBasedGroupKeyGenerator.java 86.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19379      +/-   ##
============================================
+ Coverage     57.71%   67.54%   +9.83%     
- Complexity        7     1430    +1423     
============================================
  Files          2686     3486     +800     
  Lines        163987   224163   +60176     
  Branches      26627    35381    +8754     
============================================
+ Hits          94640   151406   +56766     
+ Misses        61352    60714     -638     
- Partials       7995    12043    +4048     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 67.54% <88.88%> (+9.83%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 67.54% <88.88%> (+9.83%) ⬆️
unittests 67.54% <88.88%> (+9.82%) ⬆️
unittests1 57.64% <88.88%> (-0.07%) ⬇️
unittests2 39.31% <0.00%> (?)

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.

@xiangfu0
xiangfu0 requested review from Jackie-Jiang, gortiz and yashmayya and a lite review from Copilot August 28, 2026 17:36

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.

…shrinks the cardinality product

With optimizeMaxInitialResultHolderCapacity=true, DictionaryBasedGroupKeyGenerator used the
IN/EQ-predicate-shrunk cardinality product both for holder type selection and for the group id
upper bound. ArrayBasedHolder uses raw dictionary-id mixed-radix products as group ids, so any
matching dictionary id beyond the shrunk bound threw ArrayIndexOutOfBoundsException, and
resetting longOverflow could downgrade the holder to int/long raw keys that overflow for the
full cardinalities, silently colliding distinct groups.

Holder type selection now always uses the full cardinality product; the predicate-derived value
only caps the dense group id upper bound, and ArrayBasedHolder falls back to IntMapBasedHolder
when the bound shrinks below the product. Multi-value group-by expressions are excluded from
the predicate-derived bound (every value inside a matching row becomes a group), enforced both
in DefaultGroupByExecutor for all generators and inside DictionaryBasedGroupKeyGenerator.
@xiangfu0
xiangfu0 force-pushed the fix-optimized-groupby-holder-selection branch from 009f03b to dbe1598 Compare August 29, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants