Skip to content

[Enhancement] Top-N scans open every rowset and segment before checking the current Top-N threshold #70135

Open

Description

@matanper

Search before asking

Description

For SELECT ... WHERE <leading keys fixed> ORDER BY <next key> DESC LIMIT n read in key order, the Top-N threshold usually becomes tight after the first rowset or two, yet the scan still opens every remaining rowset and segment. Opening a segment loads its footer and column metadata and builds its iterator. On a log table with about 1K segments per scan instance this open cost dominates the query once #70117's key-order read is in place.

On master:

  • VCollectIterator::_topn_next (be/src/storage/iterator/vcollect_iterator.cpp, the loop over _rs_splits) calls rs_reader->init(...) for every rowset unconditionally. The current threshold only feeds the runtime predicate, which is evaluated after init has already opened the rowset's segments.
  • BetaRowsetReader has no step that compares the threshold with a segment's zone map (or key bounds) before the segment is opened, so a segment whose whole [min, max] lies beyond the threshold is still opened, only to have every row filtered out.

A minor related point: in SegmentIterator::_get_row_ranges_from_conditions the zone-map row ranges are intersected twice, and pre_size2 is taken after the first intersection, so rows_stats_rp_filtered (RowsZoneMapRuntimePredicateFiltered) is always 0. Filtering itself is correct; only the counter is wrong.

Solution

Check the current threshold before opening:

  1. Before rs_reader->init in _topn_next, and when splitting rowsets into segment ranges, compare the Top-N predicate at its current threshold against each segment's zone map for the target column (inclusive bound; a segment holding nulls is kept when nulls sort first; pass_all zone maps are never pruned; only when the predicate's value type matches the column's). Skip segments it rules out, and skip the rowset when every segment is ruled out.
  2. Re-check per rowset as the walk reaches it, because the threshold keeps tightening while earlier rowsets and the query's other scan instances read rows.
  3. The bounds must be available without opening the segment, since reading the footer is most of what an open costs. Two options:
    • keep each segment's zone maps (from its footer) in memory after the first open, which is what we did; or
    • use the rowset meta's segments_key_bounds, which is already in memory. Top-N is only pushed down on key columns, so this works whenever a segment's min and max keys share the fixed key prefix, though it covers fewer segments than zone maps.
  4. Report it: SegmentsPrunedByTopNBeforeOpen, RowsetsPrunedByTopNBeforeInit.

Every published threshold comes from n rows the query has actually read, so a segment ruled out this way holds no row the result needs, including while the threshold is still being tightened.

On a 1-billion-row, 31-day table (16 buckets, ORDER BY event_time DESC LIMIT 100 for one tenant), skipping segments this way left 3 of 110 segments in a scan instance to open. Segment open time per instance went from 2 ms to 34 µs and warm latency from 68 ms to 53 ms, with identical results on 12 Top-N query shapes (ASC/DESC, OFFSET, extra filters, IN lists). On our production cluster, where each scan instance opens about 1–1.3K segments for the same query, segment opens are the largest remaining cost of the 30-day first page after #70117. We are happy to contribute a PR for master if this direction is welcome.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions