compute: sort snapshot arrangements once instead of merging them - #38660
Draft
frankmcsherry wants to merge 6 commits into
Draft
compute: sort snapshot arrangements once instead of merging them#38660frankmcsherry wants to merge 6 commits into
frankmcsherry wants to merge 6 commits into
Conversation
While a dataflow hydrates, every update reaching an arrangement carries the snapshot time. The merge batcher still sorts each 64 KiB chunk and folds it into a geometric ladder of two-way merges, copying every row once per level (about thirteen times at 10M rows) and comparing full rows at every step, although nothing can cancel across times and the sealed chain is simply every update in `(data, time)` order, consolidated. `SnapshotBatcher` holds incoming chunks untouched while they all share one time. At `seal` it sorts a compact `(key prefix, chunk, position)` index, comparing full rows only on equal prefixes, and emits the sorted and consolidated chain in one copy pass. The first chunk with a second time hands everything held to a `MergeBatcher` and the batcher stays on that path, so steady-state behaviour is unchanged. `RowRef::sort_prefix` provides the prefix next to the `Ord` impl whose order it must agree with. Row-keyed arrangement sites use it through the `RowRowBatcher`, `RowValBatcher` and `RowBatcher` aliases and the two direct `Col2ValBatcher` sites in `context.rs` and `linear_join.rs`. Measured on one worker at 10M rows, five repetitions each, against the same tree without this change: a plain index on a table 3.53s to 2.81s (the arrangement operator 2.00s to 1.27s), an index on a distinct view 5.40s to 4.76s (2.25s to 1.68s), a fact-to-dimension join 8.3 to 11.5s down to 7.25s (input arrangement 2.0s to 1.6s, output 1.7s to 1.1s), and a count-by-key aggregate 7.36s to 6.96s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
frankmcsherry
force-pushed
the
snapshot-batcher
branch
from
September 4, 2026 17:08
661e124 to
cffe59b
Compare
The chunkers sort and consolidate every input container before the batcher sees it. With the snapshot batcher sorting everything it holds at `seal`, that work is wasted on its fast path. `UnsortedChunker` packs updates into chunks in arrival order, and the batcher sorts and consolidates each chunk itself only when it hands chunks to the merge batcher on the fallback path, which still receives what it requires. Wired at every Row-keyed arrangement site that already uses the snapshot batcher: the reduce inputs, the TopK stages, `FormArrangementKey` and the join's arrangements. Error arrangements keep their chunker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r run The snapshot batcher's seal sorts a (prefix, chunk, position) index. Sort it by prefix with an LSD radix sort that skips the byte positions on which all prefixes agree, and let the gather order runs of equal prefix by full comparison, so the random row reads a comparison costs happen only where the prefix leaves the order undecided. Touch the row sixteen entries ahead in the gather so its cache miss overlaps the current row's work. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chunker held updates in a Vec and copied them into chunks once enough had arrived, so every update moved twice. Copy each update into the open chunk as it arrives and roll the chunk when it fills. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
frankmcsherry
force-pushed
the
snapshot-batcher
branch
from
September 5, 2026 00:44
4e82e6e to
a1b51c2
Compare
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.
Motivation
Hydration time for an index is dominated by arrangement formation, in every shape measured: 57% of a plain index over a 10M-row table, 42% of an index on a
SELECT DISTINCTview, 47% of a fact-to-dimension join, and eight copies of it in a hierarchicalmaxor aTopK. Inside the merge batcher that time is the per-chunk sort, then a geometric ladder of two-way merges that copies every row once per level (about thirteen levels at 10M rows) and compares full rows at every step, then the builder copy.During hydration every update carries the snapshot time. Nothing can cancel across times, and the chain
sealemits is simply every held update in(data, time)order, consolidated. The ladder buys nothing for that case.Change
SnapshotBatcher(new, inmz_row_spine) holds incoming chunks untouched while they all share one time. Atsealit builds a compact(key prefix, chunk, position)index (16 bytes per update), sorts it comparing 8-byte prefixes and falling back to the full(data, time)comparison only on equal prefixes, and emits the sorted, consolidated chain in one copy pass. Copies per row drop from about fifteen to three (chunker, seal, builder) and most comparisons become integer compares over a cache-friendly array.The first chunk that carries a second time hands everything held to a
MergeBatcher, and the batcher stays on that path from then on, so steady-state behaviour is exactly the merge batcher's. A batch whose single time is not yet belowupperis kept and reported throughfrontier(), as the merge batcher does.RowRef::sort_prefixprovides the prefix (length saturated atu16::MAX, then the first six bytes) next to theOrdimpl whose length-first order it must agree with, so the contract has one owner. The batcher is generic over any data type led by aRowthrough a smallSortPrefixtrait, which tuples implement by delegating to their first component.Row-keyed arrangement sites pick it up through the
RowRowBatcher,RowValBatcherandRowBatcheraliases, the two directCol2ValBatchersites inrender/context.rsandrender/join/linear_join.rs, and a newmz_row_spine::KeyBatcherused by the five Row-ledconsolidate_namedsites intop_k.rsandreduce.rs(theTopK final consolidatealone is 2.5s of a 10M-row TopK hydration).Measurements
One worker, 10M rows, scrambled keys, five repetitions each, the same tree with and without this change:
SELECT DISTINCTcount(*), sum(v) GROUP BYwith 1.25M groupsProfile of the index after the change: the index sort is 0.17s of the 1.27s arrangement, the emission copy 0.25s, the builder about 0.3s, the chunker's per-container sort 0.15s.
Memory: pending chunks are the same 64 KiB chunker chunks the merge batcher would hold in its chains, plus the transient 16-byte-per-update index at
seal, plus the output chain while both are live. The merge batcher's final merge holds a comparable transient.Testing
Unit tests in
snapshot_batcher.rscover sorting and consolidation across chunks, a seal below the held time keeping data and reporting the frontier, the handover to the general path on a second time, later single-time batches staying on the fast path, andsort_prefixagreeing with row order across lengths and types. Every sqllogictest that arranges data exercises the path.A second commit adds
UnsortedChunker: the chunkers used to sort and consolidate every input container before the batcher saw it, which is wasted work ahead of a batcher that sorts everything atseal. The unsorted chunker packs updates in arrival order, and the batcher sorts and consolidates each chunk itself only on the fallback path, so the merge batcher still receives what it requires. Error arrangements keep their chunker.Measured on top of the other PRs of this series (identity key, prefix split, decode builder), one worker, 10M rows, three repetitions: index 2.40s to 2.17s (arrangement 1.40s to 1.15s), distinct 3.81s to 3.13s (its input arrangement 1.71s to 1.06s: the reduce-side chunker was sorting Row pairs with full comparisons), join 6.43s to 6.07s.
Open questions for review
sort_unstable_byon the index is 0.17s at 10M; a radix pass on the prefix would remove most of that but was not needed to show the effect.Checklist
$T ⇔ Proto$Tmapping (possibly in a backwards-incompatible way), then it is tagged with aT-protolabel.Release notes
This release will not include user-visible changes.
🤖 Generated with Claude Code
Sorting the index by radix, and settling equal prefixes per run
Two further changes to the seal, measured on the same 10M-row hydration:
(prefix, chunk, position)index is sorted with an LSD radix sort on the u64 prefix, one histogram pass then one scatter pass per byte position on which prefixes differ. Row prefixes agree on their length and tag bytes, so three or four of the eight passes run. Inputs below 65536 entries keep the comparison sort.Hydration at 10M rows, one worker, median of three, on top of the earlier commits of this PR and the other hydration PRs in flight:
ArrangeBy1162ms to 963msArranged DistinctBy1150ms to 887msArrangeBy1453ms to 1107ms and 1046ms to 815msArrangeAccumulable1790ms to 1073mscount_by gains most because its key repeats eight times per group, so equal prefixes were common and the comparator resolved each tie with two random row reads.
In the batcher's benchmark (
bench_snapshot_path, ignored by default), the seal of 10M rows goes from 691ms to 483ms: sort 229ms to 103ms, gather 420ms to 341ms. New tests cover the radix sort against a comparison sort and the ordering of rows that share a prefix.