[SPARK-59697][CORE] Skip the redundant record comparator in the external-sort spill merge when the key prefix is a total order - #58949
Open
david-mollitor-db wants to merge 1 commit into
Conversation
david-mollitor-db
force-pushed
the
spill-merge-radix-skip
branch
from
September 21, 2026 19:10
4e4b8ab to
db6ead4
Compare
…nal-sort spill merge when the key prefix is a total order The external-sort spill merge (`UnsafeSorterSpillMerger` / `UnsafeSorterBoundedSpillMerger`, reached via `UnsafeExternalSorter.getSortedIterator()`) orders spill-run heads by the 8-byte key prefix and, on a prefix tie, falls back to the full `RecordComparator`, which decodes and byte-compares the records. When the sort qualifies for radix sort (`canUseRadixSort` -- a single, prefix-sortable key) AND that key is non-null, the prefix is a lossless, order-preserving total order over actual rows, so equal prefixes are equal keys and the tie-break always returns 0 -- dead work on every prefix collision. The non-null requirement matters: a null is encoded in the prefix as an in-range sentinel long (e.g. `Long.MinValue`) that can collide with a real key equal to that sentinel, and the spill-merge iterator carries only the prefix (no isNull), so the record comparator is the only thing that separates a null from an equal- prefix real value. The in-memory radix path is null-aware (RadixSortSupport `nullsFirst()`), but the merge is not -- so the tie-break may only be skipped for a non-null key. This threads `canUseRadixSort` (already computed, previously only forwarded to `UnsafeInMemorySorter`) plus the sort key's nullability into both merge paths, and skips the record comparator only when `canUseRadixSort && !nullable`. The merger now takes a `@Nullable RecordComparator`: null means "prefix is a total order, compare by prefix only", so no separate boolean is needed. It is the merge-side analogue of the null-aware in-memory radix optimization and is behavior-preserving; all other sorts (multi-key, strings, large decimals, or a nullable key) keep the record-comparator tie-break unchanged. Co-authored-by: Isaac <no-reply@databricks.com>
david-mollitor-db
force-pushed
the
spill-merge-radix-skip
branch
from
September 22, 2026 02:39
db6ead4 to
c37e1d2
Compare
david-mollitor-db
marked this pull request as ready for review
September 22, 2026 02:49
This branch has not been deployed
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 changes were proposed in this pull request?
The external-sort spill merge (
UnsafeSorterSpillMerger/UnsafeSorterBoundedSpillMerger,reached via
UnsafeExternalSorter.getSortedIterator()) orders spill-run heads by the 8-byte keyprefix and, on a prefix tie, falls back to the full
RecordComparator(which decodes andbyte-compares the records).
When the sort qualifies for radix sort (
canUseRadixSort-- a single, prefix-sortable key), theprefix is a lossless, order-preserving total order for that key, so equal prefixes are equal
keys and the
RecordComparatortie-break always returns 0. This PR threads the already-computedcanUseRadixSortflag (previously only forwarded toUnsafeInMemorySorterand then dropped) intoboth merge paths -- the single-round
UnsafeSorterSpillMergerand the multi-roundUnsafeSorterBoundedSpillMerger-- so the record comparator is skipped on prefix ties in thatcase.
Why are the changes needed?
UnsafeExternalSorterbacksSortExecand key-based aggregation/window/join (viaUnsafeKVExternalSorter). On a spilled sort with a single prefix-sortable key, every prefixcollision in the merge currently pays for a full record decode + comparison that provably returns
0. Skipping it removes that dead work; the effect is largest for low-cardinality or duplicate-heavy
keys where prefix ties are frequent, and neutral otherwise.
Does this PR introduce any user-facing change?
No. The result is identical for all inputs. Skipping the tie-break is safe because
canUseRadixSortguarantees the prefix fully determines the sort order (equal prefix = equal key),so the record comparator would have returned 0 anyway -- the same precondition the in-memory radix
sort already relies on (each spilled run was ordered by prefix alone). Sorts with
canUseRadixSort=false(multi-key sorts, strings/binary, large decimals) keep therecord-comparator tie-break unchanged.
How was this patch tested?
UnsafeExternalSorterSuiteandUnsafeExternalSorterRadixSortSuitepass (60 tests). AddedtestSortingWithDuplicatePrefixesAcrossSpills, which inserts many records with duplicated keyprefixes across several spill files and asserts the merged output stays in non-decreasing prefix
order with every record preserved. It runs in both the base suite (record-comparator path) and the
radix subclass (the new skip path).
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 4.8