perf(read): feed sorted runs into the PK merge LoserTree - #668
Merged
JingsongLi merged 4 commits intoAug 4, 2026
Merged
Conversation
JunRuiLee
force-pushed
the
perf/mor-merge-fanin-by-sorted-run-master
branch
from
August 4, 2026 03:36
a0698a0 to
61b2662
Compare
Group non-overlapping data files into lazy concatenated runs so PK merge fan-in follows key-range overlap depth instead of file count. Preserve global merge semantics across splits and fall back safely on invalid key metadata.
JunRuiLee
force-pushed
the
perf/mor-merge-fanin-by-sorted-run-master
branch
from
August 4, 2026 03:44
61b2662 to
3ed83d8
Compare
JunRuiLee
marked this pull request as ready for review
August 4, 2026 05:58
Contributor
Author
Thanks again for the LGTM! I made a few follow-up fixes based on additional review feedback, mainly around unsigned Binary ordering, sorted-run semantics, unnecessary split cloning, and regression tests. All tests and clippy pass. Could you please take another quick look? Thanks! |
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.
Purpose
Primary-key merge-on-read currently feeds one input stream per data file into the LoserTree. On fragmented tables, peak merge fan-in therefore grows with file count even when many files have disjoint primary-key ranges.
This change concatenates files with strictly non-overlapping key ranges into sorted runs, so peak LoserTree fan-in follows key-range overlap depth while preserving merge semantics.
This is also what paimon-java already does:
MergeFileSplitReadunconditionally runsIntervalPartitionand wraps eachSortedRunin aConcatRecordReaderbefore handing it to the merge reader, so its fan-in has always tracked overlap depth rather than file count.Brief change log
Performance
Measured on a fragmented PK table with production shape: 6,647,251 rows, one
ARRAY<FLOAT>embedding column of dim 2048, 16 buckets,write-only=trueso it was never compacted — roughly 44 level-0 files per bucket whose key ranges overlap only 2 deep. All columns projected, no filter, 16 reader threads.Peak LoserTree fan-in per split, from planning alone:
Peak read memory and throughput at
read.batch-size=1024, with paimon-java on the same table for reference — read through thepaimon-coreAPI, no Spark or Flink, same projection and parallelism:-Xmx10g(smallest heap that completes)-Xmx16gSorted runs cut peak memory by 95.0% against the per-file fan-out, at 10.8% lower throughput. Against Java's smallest completing configuration they use 38.6% less memory at 83.0% higher throughput.
Java's RSS grows to fill whatever
-Xmxallows —-Xmx24greaches 23.72 GiB on this table — so its smallest completing heap is the fair comparison point;-Xmx6gOOMs. Java is measured as RSS and Rust as jemallocallocated, which tracked resident closely here, so neither side is credited with headroom it did not use.At
read.batch-size=8192, where the Rust reader reaches its best throughput on this table, the change wins on both axes. The per-file fan-out keeps 16 × 44 = 704 Parquet readers open at once, and the page-fault and allocator contention that costs shows up as lost CPU:The mechanism is that memory scales with the number of concurrently open Parquet streams rather than with per-stream cost: each open stream keeps its current row group's projected column chunks resident until that row group is fully consumed. Per-stream cost is comparable between the two implementations — Java's off-heap direct memory stayed at 0.01 GiB throughout, so the gap is not hidden allocation on either side.
These numbers come from a downstream build of the same grouping mechanism rather than from this exact diff, so treat the absolute throughput as indicative; the fan-in reduction and the memory-to-stream-count relationship are structural.
Tests
cargo fmt --all -- --checkcargo test -p paimon --libcargo test -p paimon table::merge_tree_split_generator::tests --libcargo test -p paimon table::kv_file_reader::tests --libcargo clippy -p paimon --lib --tests -- -D warningsgit diff --checkAPI and Format
Documentation