Skip the data shuffle for UNION ALL in the multi-stage engine - #19325
Closed
yashmayya wants to merge 1 commit into
Closed
Skip the data shuffle for UNION ALL in the multi-stage engine#19325yashmayya wants to merge 1 commit into
yashmayya wants to merge 1 commit into
Conversation
yashmayya
force-pushed
the
union-all-shuffle-elimination
branch
from
August 20, 2026 21:26
e57801c to
d6343c4
Compare
Contributor
|
Consider not adding exchange from the beginning for |
yashmayya
force-pushed
the
union-all-shuffle-elimination
branch
from
August 20, 2026 22:30
d6343c4 to
0cae090
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #19325 +/- ##
=============================================
- Coverage 67.00% 38.96% -28.04%
+ Complexity 1424 1423 -1
=============================================
Files 3463 3463
Lines 221671 221734 +63
Branches 34954 34975 +21
=============================================
- Hits 148524 86402 -62122
- Misses 61310 127454 +66144
+ Partials 11837 7878 -3959
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:
|
yashmayya
force-pushed
the
union-all-shuffle-elimination
branch
from
August 21, 2026 19:14
0cae090 to
a02289a
Compare
Contributor
Author
|
Superseded by #19330 |
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.
UNION ALLis a pure concatenation, so any row-to-worker mapping produces the same result and no redistribution is ever required. The V2 physical optimizer has always known this —TraitAssignment#assignSetOpreturns early forUnion.all("UNION ALL means we can return duplicates, so no trait required"). The V1 (default) planner did not:PinotSetOpExchangeNodeInsertRuleunconditionally hash-shuffled every set-op input on the full output row,UNION ALLincluded.This brings V1 in line. A
UNION ALLinput now gets a local exchange —SINGLETON, which is how Pinot already spells "local exchange" — carrying the projected columns as keys. The union stage inherits its inputs' worker assignment and each sender hands its rows to the worker on its own server: no shuffle, no hashing, and no network hop when sender and receiver land together.Opt out per query with
/*+ setOpOptions(is_colocated_by_set_op_keys='false') */, which restores the full-row shuffle.Why the keys are attached
The keys are unused while the exchange stays local. They are there so the mailbox layer can promote the exchange to a real
HASH_DISTRIBUTEDshuffle when the branches cannot share a worker assignment — the same idiomPinotJoinExchangeNodeInsertRulealready uses fordistribution_type='local':That also keeps the change from having to distinguish itself from anything else.
SINGLETONis shared with two features whose contracts are much stricter — the colocated dynamic-broadcast semi-join build side (every receiver needs the whole build side; the non-colocated variant broadcasts for exactly that reason) anddistribution_type='local'joins (equal keys must meet on one worker). Both of those are keyless or key-carrying in their own right, and the existing"Local exchange with parallelism requires keys"guard continues to protect the keyless one untouched. AUNION ALLinput is simply never a keyless local exchange, so no new marker or discriminator is needed anywhere.Degradation, and the co-residency fix
When the branches do not resolve to the same workers, the union stage cannot inherit both. Branches that still line up stay
SINGLETONand wire 1-to-1; the rest are promoted to a hash shuffle on the projected columns. Correct either way for a concatenation, and the aligned branches still pay nothing.The parallel wiring addresses a whole contiguous receiver range at the range's first host. That only holds when the receiver map was derived from the sender by
WorkerManager#assignWorkersForLocalExchange. When the stage instead takes its workers from the candidate servers, a range spans several hosts — so blocks would be posted to a mailbox on the wrong server and the real receiver would block until the query deadline. This now verifies co-residency instead of assuming it, and falls back to a full hash shuffle when it does not hold. That is a fix for the pre-existing callers too, not only forUNION ALL.A related consequence: a local exchange whose worker counts do not divide evenly used to throw. It now falls back to a hash shuffle, which is correct for every local-exchange kind since
HashExchangeroutes each key consistently. A join hintedlocalon both sides with mismatched worker counts previously failed outright and now plans and runs correctly.Set-op output distribution
PinotRelDistributionTraitRulehad noSetOpcase, so every set operation fell through toRANDOM_DISTRIBUTED. It now derives the output from what the input exchanges actually do:INTERSECT/EXCEPT/ distinctUNION, orUNION ALLwith the hint set to'false') → hash distributed on all output columns, so a downstream exchange keyed on those columns can skip its own shuffle. This holds foris_colocated_by_set_op_keys='true'too: the hint asserts that rows equal across all projected columns already share a worker, which is exactly the claimed distribution, and we take it at its word just as the exchange does.UNION ALLcase) → nothing claimed, because a local exchange redistributes nothing and therefore guarantees nothing. This is what keeps a deduplicating consumer — for example the aggregateUnionToDistinctRuleputs over a distinctUNION— from skipping a shuffle it needs.Latent bug fixes
All three were reachable before this change, two of them via an explicit
is_colocated_by_set_op_keys='true'hint over fully-pruned inputs; the new default makes them reachable without a hint:MailboxAssignmentVisitordivided by zero when a leaf stage had all of its segments pruned.WorkerManagerlet a zero-worker child anchor the local-exchange assignment. AUNION ALLwith one branch fully pruned would leave the union stage with no workers and silently return empty results.Notes
usePhysicalOptimizer=true) already plansUNION ALLwithout a shuffle and is untouched.SINGLETONandHASH_DISTRIBUTEDare pre-existingDistributionTypevalues already handled byBlockExchange, and the decision is broker-side at plan time. No rolling-upgrade concern and nobackward-incompatlabel.is_colocated_by_set_op_keys='true'is now a no-op on aUNION ALL(its inputs get a local exchange either way); only'false'changes anything there. The hint is unreleased, so no compatibility shim is owed.UNION ALLchanges. Because the union stage inherits its inputs' worker layout instead of redistributing, input skew is carried into the union stage rather than rebalanced; in practice the next exchange above the union re-partitions.Testing
QueryCompilationTest— local exchange by default (with keys), the hint opt-out, misaligned branches where only the misaligned one is promoted to a shuffle, distinct set ops unaffected, and both directions of the distribution derivation.MailboxAssignmentVisitorTest— 1-to-1 wiring, promotion to a hash shuffle on unequal counts, per-host addressing when a receiver range spans servers, zero-sender wiring, and a negative control proving a keyless local exchange (the semi-join build side) still fails loudly.ExplainPhysicalPlans.json/SetOpPlans.json/AggregatePlans.json— updated plan shapes; the'false'opt-out plan is retained.QueryHints.json(compared against H2, replayed on both optimizers) —UNION ALLcorrectness, dedup andGROUP BYabove a local union, the colocatedINTERSECTclaim, and the mismatched-partition-count case.Known gaps
Three of the four disagreement checks in
canInheritWorkerAssignment(partition parallelism, partition classes, partition function) and the preserved "Found multiple local exchanges" rejection have no direct test. Neither is a known bug; both are places where a future change could regress quietly. Happy to add them here if preferred.