Skip to content

perf(calcite): rank string sort keys by bytes, walk reversed chunks backward - #575

Merged
dfa1 merged 1 commit into
mainfrom
perf/sort-string-reject
Oct 20, 2026
Merged

dfa1 merged 1 commit into
mainfrom
perf/sort-string-reject

Conversation

@dfa1

@dfa1 dfa1 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

ORDER BY … LIMIT on a string column built a Java String per row just to compare it with the worst row kept, and almost every row loses that comparison.

  • Byte rejection. A row whose first key is a string is first compared, as bytes, with the worst kept key, using VarBinArray#compareUnsigned.
    • FSST decodes only up to the first differing byte (Decompressor#compareUnsigned).
    • Dictionary and offset arrays compare in place.
    • A row that loses is skipped without building a String.
    • This only applies when the bound is ASCII, where byte order provably equals String.compareTo. A 5M-case fuzz with malformed UTF-8 found no disagreement.
  • Backward walk. A chunk whose five evenly spaced sample rows rank in strictly reverse order is walked backward: the best rows come first and the rest are rejected. A sample row the filter rejects is never read, since reading it can fail (a u64 past BIGINT), so such a chunk is walked forward instead.
  • Forward walk. It uses RowSelection#nextSelected, so perf(calcite): walk only the rows a dictionary filter selects #580's skipping of rows a dictionary filter rejects is kept.

Benchmark. 100M-row file through the jdbc:vortex: driver, warm JVM, median of 15 runs, against main (9bc12092):

query main #575
order by eventIdentifier desc limit 10 5616 ms 1680 ms (3.3×)
order by ISIN limit 10 1273 ms 962 ms (one round)

Review fixes. A subagent review raised these, all addressed:

FSST symbol lengths are deliberately not validated at load: Rust doesn't check them either.

./mvnw verify passes (9239 tests).

🤖 Generated with Claude Code

@dfa1
dfa1 force-pushed the perf/sort-string-reject branch from 4f79813 to 7e819dc Compare October 20, 2026 19:03
…ackward

ORDER BY ... LIMIT built a Java String for every row's string key just to
compare it with the worst row kept, and almost every row loses. Now a row
whose first key is a string is compared with that worst key's bytes first
(VarBinArray#compareUnsigned): FSST decodes only up to the first differing
byte (Decompressor#compareUnsigned), dictionary and offset arrays compare in
place. A row that loses is skipped without a String; any other row takes the
existing path, so results are unchanged by construction.

Byte order equals String.compareTo order when the bound is ASCII: up to the
first differing byte both strings decode the same, and a non-ASCII byte of
the row, valid UTF-8 or not, decodes to a character above every ASCII one
(a 5M-case fuzz found no disagreement). A non-ASCII bound falls back to
building every row.

A column stored in the opposite order to the query (eventIdentifier DESC on
ascending ids) has every row beat the worst kept, so nothing was rejected.
A chunk whose five evenly spaced sample rows rank in strictly reverse order
is now walked backward: its best rows come first and the rest are rejected.
A sample row the filter rejects is never read, as reading it can fail (a u64
past BIGINT), so such a chunk is walked forward. Walking backward admits a
row equal to the worst, since equal keys rank by scan position. Walking
forward goes through RowSelection#nextSelected, keeping #580's skipping of
the rows a dictionary filter rejects.

100M-row cashflow file, jdbc:vortex: driver, warm JVM (median of the last 8
of 15 runs, 3 interleaved rounds), against main 9bc1209:

  order by eventIdentifier desc limit 10   5616 -> 1680 ms  (3.3x)
  order by ISIN limit 10                   1273 ->  962 ms  (one round)

Tests: calcite SortStringKeysTest covers ties across chunk edges walked both
ways, a chunk under five rows, and a filtered-out sample row holding a u64
past BIGINT (it fails without the fix). The vortex-jni cross-check sorts
Rust-written FSST, dictionary and plain strings over three Rust chunks, with
"é" rows and ties at the bound, against Calcite's own sort.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dfa1
dfa1 force-pushed the perf/sort-string-reject branch from 7e819dc to 435b181 Compare October 20, 2026 19:06
@dfa1
dfa1 merged commit be5965c into main Oct 20, 2026
6 checks passed
@dfa1
dfa1 deleted the perf/sort-string-reject branch October 20, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant