Prune unused passthrough columns from UNNEST output (opt-in)#18782
Prune unused passthrough columns from UNNEST output (opt-in)#18782gortiz wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #18782 +/- ##
============================================
+ Coverage 64.78% 64.82% +0.04%
Complexity 1309 1309
============================================
Files 3380 3380
Lines 209544 209773 +229
Branches 32797 32861 +64
============================================
+ Hits 135746 135979 +233
+ Misses 62870 62840 -30
- Partials 10928 10954 +26
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:
|
Add an opt-in (default-off) query option `unnestColumnPruning` for the multi-stage engine. When enabled, a Project sitting directly above a CROSS JOIN UNNEST has its unreferenced input/passthrough columns - notably the unnested source array - dropped from the UnnestNode output, so the operator no longer copies them into every exploded row. - UnnestNode carries a passthrough index map + prunedPassthrough flag (legacy constructors default to "copy whole row"). - RelToPlanNodeConverter fuses the pruning into convertLogicalProject: computes the referenced left columns, builds a pruned UnnestNode with recomputed element/ordinality indexes, and remaps the project refs. Falls back to current behavior in every other shape. - UnnestOperator honors the passthrough map (primitive int[] hot path). - Additive protobuf fields keep old-broker->new-server safe; the flag defaults off so a new broker never emits the smaller schema to an un-upgraded server (enable only after the whole fleet is upgraded). Covered by planner, serde round-trip, operator, and integration tests (single/multi array, WITH ORDINALITY, zero-passthrough, array-also-selected).
7af66f6 to
f21dc95
Compare
yashmayya
left a comment
There was a problem hiding this comment.
Reviewed the correctness of the pruning and the rolling-upgrade story in depth; both hold up well. Things I specifically validated:
- The pruned output schema and the remapped project
InputRefs are built from the sameleftCount + k → numRetained + kmapping, so they can't drift out of sync, andbuildPrunedProjectcorrectly falls back to the unpruned conversion for any non-standard layout (correlate-filter present, all passthrough referenced, non-trailing element region, etc.). collectReferencedColumns/remapInputRefscover everyInputRef-bearing node:RexExpressionis only InputRef/Literal/FunctionCall, and the project expressions always come through the scalarfromRexCallpath, so the 3-argFunctionCallrebuild is lossless (isDistinct/ignoreNullsare only ever set by the aggregate/window paths, which never feed a Project) — same idiom as the existingrewriteInputRefshelpers.- Operator hot path is correct and the legacy branch is byte-for-byte unchanged; the proto change is additive with the right proto3 defaults. old-broker→new-server stays on the legacy whole-row copy; new-broker(flag-on)→old-server is the only thing that breaks (old server arraycopies the full input row into the smaller pruned
out), which is exactly why default-off is the right call.
No blockers from me.
One optional, non-blocking thought on enablement: pruneUnnestColumns is query-option-only, whereas the sibling useSpools just above it also honors a broker-config default (_envConfig.defaultUseSpools()). Wiring a matching defaultUnnestColumnPruning would let an operator flip this on once cluster-wide after the fleet is upgraded, instead of needing SET unnestColumnPruning=true on every query — and it'd make the eventual default flip a one-line constant change. Might also be worth a javadoc note that the option is a no-op under usePhysicalOptimizer (the v2 path doesn't go through RelToPlanNodeConverter). Neither blocks.
Summary
Adds an opt-in, default-off query option
unnestColumnPruningfor the multi-stage engine that prunes input/passthrough columns — notably the unnested source array — from theUNNESToutput when nothing downstream references them.Today, for a query like:
the
UnnestNodeoutput schema is the full Calcite Correlate row type[col1, mcol1, s], andUnnestOperatorcopies the entire input row (including the source arraymcol1) into every one of the N exploded rows — only for a parentProjectto immediately dropmcol1. For large arrays this needlessly widens every intermediate row (and serializes the array N times when an exchange sits between the UNNEST and the projecting operator).The array expression is only needed in the operator's input (to evaluate the explode), never in its output, unless the user also selects it. With the flag on, the source array is no longer carried.
How it works
UnnestNodecarries a passthrough index map +prunedPassthroughflag. Legacy constructors default to not pruned (copy the whole input row), so existing behavior is unchanged.RelToPlanNodeConverterfuses the pruning intoconvertLogicalProject: when aProjectsits directly above a Correlate/Uncollect (no wrapping correlate-filter), it computes which left columns the project actually references, builds a prunedUnnestNode(smaller output schema + passthrough map + recomputed element/ordinality indexes), and remaps that one project'sInputRefs. It falls back to the current behavior in every other shape, and converts the correlate at most once.UnnestOperatorhonors the passthrough map, copying only retained columns (resolved to a primitiveint[]so the per-row hot path stays allocation/box-free). When not pruned, it keeps the legacy whole-rowSystem.arraycopy.Backward compatibility / rolling upgrades
UnnestNodeis serialized broker→server and the operator runs server-side, so this is a two-sided change. The proto fields (passthroughInputIndexes,prunedPassthrough) are additive:prunedPassthrough=false⇒ legacy "copy whole row". Safe.Tests
UnnestSqlPlannerTest— flag on/off, source-array-also-selected (no-op), WITH ORDINALITY, multiple arrays, zero-passthrough.PlanNodeSerDeTest— protobuf round-trip for pruned (non-sequential indexes + ordinality) and legacyUnnestNode.UnnestOperatorTest— pruned single-array, zero-passthrough, WITH ORDINALITY, multiple arrays; legacy path unchanged.UnnestIntegrationTest— end-to-end pruned-vs-default result equality across single/multi array, WITH ORDINALITY, zero-passthrough, and array-also-selected shapes.Notes for reviewers
RelToPlanNodeConverter+ the node/operator + serde; the V2 physical path is untouched.