Compare all semantic fields when spooling equivalent stages - #19219
Open
yashmayya wants to merge 1 commit into
Open
Compare all semantic fields when spooling equivalent stages#19219yashmayya wants to merge 1 commit into
yashmayya wants to merge 1 commit into
Conversation
The spool optimizer decides two stages are interchangeable by comparing node fields in EquivalentStagesFinder. Several semantically significant fields were missing from those comparisons, so stages that compute different values looked equivalent and got merged, silently serving one stage's rows to both consumers: - WindowNode.getExclude(): the frame exclusion changes which rows feed the window function. Two windows differing only on EXCLUDE were merged. - AggregateNode.getGroupingSets(): the group keys only hold the union of the grouping columns, so ROLLUP(a,b) and CUBE(a,b) agree on every other field, including the data schema. - JoinNode.getMatchCondition(): ASOF joins carry their whole comparison here rather than in the non-equi conditions, so two ASOF joins differing only on > vs >= were merged. Also add _ignoreNulls to RexExpression.FunctionCall equals/hashCode, where it has been missing since IGNORE NULLS was introduced. Window agg calls are the only ones that set it today, and Calcite currently collapses such windows earlier (Window.Group hashes a digest that omits IGNORE NULLS), so this is not reachable from SQL on its own - but the field is semantically significant and belongs in the equality contract.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #19219 +/- ##
============================================
+ Coverage 65.69% 66.92% +1.23%
Complexity 1423 1423
============================================
Files 3439 3452 +13
Lines 218064 218519 +455
Branches 34679 34743 +64
============================================
+ Hits 143255 146249 +2994
+ Misses 63257 60570 -2687
- Partials 11552 11700 +148
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:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
EquivalentStagesFinderdecides whether two stages are interchangeable for the spool optimizer (useSpools=true) by comparing plan-node fields. Several semantically significant fields were missing from those comparisons, so stages that compute different values compared as equivalent.EquivalentStagesReplacerthen drops one stage and serves the survivor's rows to both consumers — silent wrong results, no error.Three of these are reachable and produce wrong query results:
WindowNode.getExclude()SUM(x) OVER (... EXCLUDE CURRENT ROW)and the same window withEXCLUDE NO OTHERSagree on every other compared field.AggregateNode.getGroupingSets()getGroupKeys()only holds the union of the grouping columns, soROLLUP(a,b)andCUBE(a,b)agree on group keys, agg calls and data schema.JoinNode.getMatchCondition()nonEquiConditionsto be empty and carry the whole comparison inmatchCondition, so two ASOF joins differing only on>vs>=compared as equivalent.All three are compared by the corresponding
PlanNode.equals(); only the equivalence check was missing them.This also adds
_ignoreNullstoRexExpression.FunctionCall#equals/#hashCode, where it has been missing since IGNORE NULLS was introduced in #14264. See the note below — unlike the three above, this one is not currently reachable from SQL on its own.Repro
Spool.jsongains an end-to-end case (H2-compared). Before the fix:The
EXCLUDE NO OTHERSwindow returned theEXCLUDE CURRENT ROWvalue because the two stages were spooled together. Note the two windows must partition on the same key — adding a join condition that widens one side's exchange keys makes the stages differ ongetKeys()and masks the bug.Tests
EquivalentStagesFinderTest: negative cases for all four fields, each verified to fail without its fix, plus matching positive cases (bothIGNORE NULLS, bothEXCLUDE CURRENT ROW, same grouping sets, same match condition) so the checks can't regress into blanket rejection.Spool.json: end-to-end value comparison, verified to fail without thegetExclude()fix. This suite previously had no window coverage.Note on IGNORE NULLS
The
FunctionCall#equalschange is a correctness-contract fix, not a live bug fix, and I want to be explicit about why.Window agg calls are the only ones that set
ignoreNullstoday (RexExpressionUtils.fromAggregateCallhardcodesfalse). Two window stages differing only in IGNORE NULLS never reach the equivalence check, because Calcite collapses them earlier:Window.Group#equals/hashCodecompare onlydigest, andcomputeString()renders agg calls viaRexCall.toString(), which does not print IGNORE NULLS. So the twoLogicalWindownodes have identical digests and the planner dedupes them.That upstream behaviour is itself a wrong-results bug, and it is independent of
useSpools. On master:plans both windows with
ignoreNulls=truewithuseSpools=false, and whichever variant appears first wins (swapping the two subqueries flips both toignoreNulls=false).Window.Group#excludeis in the digest, which is exactly why the EXCLUDE case survives Calcite and then gets mis-spooled instead.I have not tried to work around that here — it needs a fix in Calcite's
Window.Group(or a Pinot-side digest override) and is a separate concern from stage equivalence. Filing it separately. This PR makes the Pinot-side equality contract correct so the spool path does not become a second way to hit the same class of bug.Notes
expressions.protoandplan.proto. The change is strictly fewer merges, so a mixed-version cluster executes either plan shape correctly.FunctionCall#equalsis shared beyond the spool path (AggregateNode/ProjectNode/FilterNode/WindowNode.equals, andPlanNodeMerger), so multi-stageEXPLAINoutput also stops merging nodes that differ only on IGNORE NULLS, regardless ofuseSpools.excludesince Add support for window function EXCLUDE clause #18482,groupingSetssince Support GROUP BY GROUPING SETS / ROLLUP / CUBE in the multi-stage query engine #18817,matchConditionsince ASOF JOIN #15630, all gated onuseSpools(off by default).PlanNodeMerger.visitWindow/visitJoin/visitAggregatehave the same omissions. Those only affectEXPLAINrendering, not query results, so I left them out to keep this focused — happy to fold them in if reviewers prefer.