Skip to content

compute: thin peek results by partitioning, not sorting - #38035

Closed
aljoscha wants to merge 1 commit into
aljoscha/peek-01-cooperativefrom
aljoscha/peek-02-thinning
Closed

compute: thin peek results by partitioning, not sorting#38035
aljoscha wants to merge 1 commit into
aljoscha/peek-01-cooperativefrom
aljoscha/peek-02-thinning

Conversation

@aljoscha

@aljoscha aljoscha commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Motivation

Part 3 of 3 in a stack that makes index peeks stop monopolizing the compute
worker. Part 1 is #38039, part 2 is #38034.

When a peek's finishing bounds how many rows it can need, the scan keeps twice
that many and periodically drops the excess. It did so by sorting the whole
buffer and truncating. That costs a log factor per row for an order that is
thrown away: the result is ordered once at the end, when the RowCollection is
built. Each comparison decodes both rows, so for a peek with an ORDER BY and
a small LIMIT over a large arrangement this is the dominant cost.

Part of CPU-195.

Description

select_nth_unstable_by gives the same retained rows in O(n) comparisons
instead of O(n log k).

The partition is not stable, so when rows tie across the cut it is unspecified
which of them survives. That is unobservable, and the argument is worth stating
because it is the only thing making this a safe swap:

  • A tie under RowComparator::compare_rows(l, r, || l.cmp(r)) means the
    order_by columns compare equal and the tiebreaker compares equal, and the
    tiebreaker is a full comparison of the encoded rows. So tied rows are
    byte-identical.
  • The retained run therefore agrees with any other choice on its first
    limit + offset rows, whichever way the cut lands.
  • RowSetFinishing::finish reads exactly offset..offset + limit of the merged
    result, and merge_sorted's output prefix depends only on the runs' prefixes.
    So the client sees the same rows either way.

mz_index_peek_result_sort_seconds is renamed to
mz_index_peek_result_thinning_seconds, since it no longer times a sort. The
one remaining sort, when the RowCollection is built, is timed by
mz_index_peek_row_collection_seconds.

Not changed on purpose. The unconditional sort in RowCollection::new,
which fires even when order_by is empty, stays. It is load-bearing for
determinism, not for correctness: per-worker peek responses are absorbed in
arrival order by a randomized StreamMap, so sorting each run by encoded bytes
plus a byte-order k-way merge in envd is what makes client-visible row order
reproducible independent of worker count and arrival order. Roughly 1100
multi-row, no-ORDER BY, nosort sqllogictest goldens ride on it, and
test/sqllogictest/range.slt:359 pins the length-first-then-bytes encoding
order exactly.

Verification

Covered by the existing suite: any change to which rows survive thinning, or to
their order, shows up in the sqllogictest goldens described above, since they
compare output positionally.

@linear-code

linear-code Bot commented Aug 4, 2026

Copy link
Copy Markdown

CPU-195

@aljoscha
aljoscha force-pushed the aljoscha/peek-01-cooperative branch from a18f52c to 802ed32 Compare August 4, 2026 08:56
@aljoscha
aljoscha force-pushed the aljoscha/peek-02-thinning branch from c86c46b to a8294a0 Compare August 4, 2026 08:56
@aljoscha
aljoscha force-pushed the aljoscha/peek-01-cooperative branch from 802ed32 to a0c373e Compare August 4, 2026 09:00
@aljoscha
aljoscha force-pushed the aljoscha/peek-02-thinning branch from a8294a0 to 1da3c70 Compare August 4, 2026 09:00
@aljoscha
aljoscha force-pushed the aljoscha/peek-01-cooperative branch from a0c373e to 6bd2303 Compare August 4, 2026 09:22
@aljoscha
aljoscha force-pushed the aljoscha/peek-02-thinning branch from 1da3c70 to 81b988e Compare August 4, 2026 09:22
@aljoscha
aljoscha force-pushed the aljoscha/peek-01-cooperative branch from 6bd2303 to 1b46946 Compare August 4, 2026 09:56
@aljoscha
aljoscha force-pushed the aljoscha/peek-02-thinning branch from 81b988e to 7493416 Compare August 4, 2026 09:56
@aljoscha
aljoscha force-pushed the aljoscha/peek-01-cooperative branch from 1b46946 to ab66d73 Compare August 4, 2026 10:41
@aljoscha
aljoscha force-pushed the aljoscha/peek-02-thinning branch from 7493416 to db7baa0 Compare August 4, 2026 10:41
@aljoscha
aljoscha marked this pull request as ready for review August 4, 2026 10:56
@aljoscha
aljoscha requested review from a team as code owners August 4, 2026 10:56
When a peek's finishing bounds how many rows it can need, the scan keeps
twice that many and periodically drops the excess. It did so by sorting
the whole buffer and truncating, which costs a log factor per row for an
order that is thrown away: the result is ordered once at the end, when
the `RowCollection` is built.

Partitioning gives the same retained rows in O(n) comparisons. Each
comparison decodes both rows, so this is the dominant cost of a peek with
an `ORDER BY` and a small `LIMIT` over a large arrangement.

The partition is not stable, so when rows tie across the cut it is
unspecified which survives. That is unobservable: a tie under this
comparator means the rows are byte-identical, so whichever way the cut
lands the retained run agrees with any other choice on its first
`limit + offset` rows, and that prefix is all the finishing reads.

`mz_index_peek_result_sort_seconds` becomes
`mz_index_peek_result_thinning_seconds`, since it no longer times a sort.
The one remaining sort, when the `RowCollection` is built, is timed by
`mz_index_peek_row_collection_seconds`.
@aljoscha
aljoscha force-pushed the aljoscha/peek-01-cooperative branch from ab66d73 to 40be927 Compare August 4, 2026 11:41
@aljoscha
aljoscha requested a review from a team as a code owner August 4, 2026 11:41
@aljoscha
aljoscha force-pushed the aljoscha/peek-02-thinning branch from db7baa0 to 6fc9408 Compare August 4, 2026 11:41
@aljoscha aljoscha closed this Aug 4, 2026
@aljoscha aljoscha reopened this Aug 4, 2026
@aljoscha

aljoscha commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Recreating on the correct base so GitHub picks up the stack. Superseded, see the replacement linked below.

@aljoscha aljoscha closed this Aug 4, 2026
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