Repository navigation
Description
Search before asking
- I searched the issues and found no similar issue. Related: [Enhancement] Top-N is not read in key order when leading sort key columns are fixed by an equality predicate (WHERE k1 = x ORDER BY k2 LIMIT n) #70117 (Top-N not read in key order when leading keys are fixed), [improvement](topn) Refresh TopN runtime filter page ranges #70020 (refresh Top-N runtime filter page ranges; keeps the shared predicate through scan-key normalization), [improvement](be) Prioritize Parquet V2 row groups for TopN scans #69646 (the same idea for Parquet row groups).
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) callsrs_reader->init(...)for every rowset unconditionally. The current threshold only feeds the runtime predicate, which is evaluated afterinithas already opened the rowset's segments.BetaRowsetReaderhas 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:
- Before
rs_reader->initin_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_allzone 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. - 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.
- 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.
- 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