[SPARK-58232][SQL] Simplify cached-batch column-index resolution with a map lookup#57395
[SPARK-58232][SQL] Simplify cached-batch column-index resolution with a map lookup#57395ganeshashree wants to merge 2 commits into
Conversation
…ead of O(n*m) linear scan DefaultCachedBatchSerializer maps each selected output attribute to its ordinal in the cached schema. Both convertCachedBatchToColumnarBatch and convertCachedBatchToInternalRow did this by rebuilding the full list of cached exprIds and running indexOf per selected attribute, which is O(n*m) time and allocation per partition setup. Build a single exprId -> ordinal map once (O(n)) and look up each attribute in O(1), for overall O(n+m). getOrElse(exprId, -1) preserves the prior indexOf semantics of returning -1 when an attribute is absent. Behavior is unchanged; existing CachedBatchSerializerSuite and InMemoryColumnarQuerySuite pass. A micro-benchmark over the isolated index computation showed 2.7x (50 cols) to 35.1x (1000 cols) speedups, growing with column count as expected.
uros-b
left a comment
There was a problem hiding this comment.
On another note, PTAL at ArrowCachedBatchSerializer.scala:148 - the sibling serializer's convertCachedBatchToColumnarBatch still carries the identical O(n·m) pattern (selectedAttributes.map(a => cacheAttributes.map(o => o.exprId).indexOf(a.exprId)).toArray) that this PR removes from DefaultCachedBatchSerializer.
Since the stated goal is removing the quadratic column-index computation, leaving it in the Arrow fast path (the serializer used when Arrow caching is enabled, SPARK-57268, and at least as relevant for wide tables) makes the cleanup incomplete. The fix is a direct copy of the same map lookup.
…er ArrowCachedBatchSerializer Per review feedback: - Reuse Catalyst's AttributeSeq.indexOf(exprId) instead of hand-rolling an exprId -> ordinal map in DefaultCachedBatchSerializer. AttributeSeq exposes a @transient lazy HashMap that returns -1 on miss and, unlike a plain zipWithIndex.toMap, resolves to the first matching ordinal (matching the prior indexOf semantics on duplicate exprIds). - Apply the same fix to ArrowCachedBatchSerializer, whose columnar and row paths carried the identical O(n*m) column-index computation, so the cleanup is complete across both cached-batch serializers.
ArrowCachedBatchSerializer.convertCachedBatchToColumnarBatch now uses AttributeSeq.indexOf as well, so the quadratic pattern is gone from the Arrow fast path too. While there, I noticed the sibling convertCachedBatchToInternalRow in the same serializer had the same defect in a slightly different form: |
What changes were proposed in this pull request?
When reading from an in-memory (cached) relation,
DefaultCachedBatchSerializer(
sql/core/.../execution/columnar/InMemoryRelation.scala) maps each selected outputattribute to its ordinal in the cached schema. Both conversion methods computed this
with an O(n*m) algorithm that, for each of the
mselected attributes, rebuilt the fulllist of
ncached-schemaExprIds and linear-scanned it withindexOf:This PR builds a single ExprId -> ordinal map once (O(n)) and looks up each selected
attribute in O(1), for overall O(n+m) and no per-attribute list allocation:
The same change is applied to both:
getOrElse(exprId, -1)preserves the prior indexOf semantics of returning -1 whenan attribute is absent.
Why are the changes needed?
The old implementation is O(n*m) in time and transient allocation: for each of the
mselected attributes it rebuilds the full
n-element list of cachedExprIds andlinear-scans it. The map-based version is O(n+m). A microbenchmark isolating the
columnIndicescomputation (all columns selected in reverse order soindexOfcannotshort-circuit; 20k calls per case; asserts the old and new implementations produce
identical index arrays before timing) shows the expected quadratic-vs-linear divergence:
To be clear about scope:
columnIndicesis computed once per scan execution on thedriver , so in absolute terms this saves microseconds per query for typical tables and up to ~3.7 ms per query only for a pathologically wide
1000-column cached relation. This is primarily a code-quality cleanup,
removing a quadratic algorithm and per-attribute list reallocation in favor of the
obvious map lookup, with the benchmark included to confirm the algorithmic improvement
rather than to claim a meaningful end-to-end speedup..
Does this PR introduce any user-facing change?
No. This is a behavior-preserving internal optimization; output schema, ordering, and
data types are unchanged.
How was this patch tested?
build/sbt sql/compilepasses.CachedBatchSerializerSuiteandInMemoryColumnarQuerySuite(36 tests,including column pruning and reordering cases that directly exercise this mapping)
pass.
real
AttributeReference/ExprIdobjects with all columns selected in reverse order(so
indexOfcannot short-circuit at position 0), 20k calls per case. It asserted theold and new implementations produce identical index arrays before timing, confirming
behavior is preserved, and measured 2.7x (50 columns) to 35.1x (1000 columns) speedups
(see the table above). The benchmark was a temporary verification aid and is not
included in this PR.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)