Skip to content

LSM: bounded range scans over multi-series compacted indexes drop rows and mis-order descending results #5214

Closed

Description

@lvca

Summary

Bounded range scans over an LSM index that has compacted into 3+ series silently drop rows, and the descending bounded iterator returns results in the wrong order. Data on disk is intact (a full scan returns everything); only the bounded/range read paths over the multi-series compacted layer are affected.

Multiple compacted series in a single file are produced when a compaction is RAM-bounded (INDEX_COMPACTION_RAM_MB), i.e. on large indexes whose immutable pages exceed the compaction budget. Small indexes compact into a single series, where these paths happen to work, which is why this has stayed latent.

This is independent of #5211 (which fixed only the unbounded full descending scan).

Reproduction

Non-unique LSM index over a String property, 20,000 keys, INDEX_COMPACTION_RAM_MB=1 so one compaction emits several series:

-- correct
SELECT count(*) FROM Doc;
-- => 20000

-- BUG: range query drops ~2,500 rows
SELECT count(*) FROM Doc WHERE email >= 'K...00005000' AND email <= 'K...00015000';
-- => 7514   (expected 10001)

At the index API level:

call expected actual
range(true, lo, hi) (ascending window) 10001 7514
range(false, hi, lo) (descending window) 10001, descending 2489, descending
iterator(false, fromKeys, true) keys <= fromKeys, descending correct set, ascending order

Root causes

1. Interior series dropped from bounded scans - LSMTreeIndexCompacted.newIterators(ascending, fromKeys, toKeys)

For a bounded scan each series only yields a cursor when fromKeys or toKeys falls inside that series (searchInCurrentPage returns null when resultInRootPage.outside). A series lying entirely within [lo, hi] - containing neither endpoint - fails both lookups, contributes no cursor, and all of its keys are lost. A wide range spanning many series drops every interior series.

2. Descending bounded iterator returns ascending order - LSMTreeIndexMutable.iterator(false, fromKeys, inclusive)

It delegates to the auto-detecting range(null, true, fromKeys, inclusive). With the begin-key null, the direction auto-detect defaults to ascending = true, so a descending request iterates ascending. Masked today because existing tests assert only element counts, never order.

3. Latent NPE on a null bound in the descending path

The straightforward fix for (2) - range(false, fromKeys, inclusive, null, true) - surfaces a NullPointerException (Cannot read the array length because "convertedKeys" is null) when the lower bound is null on a descending scan. Needs handling as part of the fix.

Impact

  • Range predicates (>=/<=/BETWEEN) and descending ordered index reads over large (multi-series-compacted) indexes can return incomplete or mis-ordered results.
  • Correctness only; no data is lost on disk.

Proposed fix

  1. In newIterators, emit a full-series cursor when a series lies wholly inside [fromKeys, toKeys] (both endpoints outside but the series is within range) instead of dropping it.
  2. Route iterator(false, fromKeys, …) through an explicit-direction descending range, and fix the null-bound NPE it exposes.
  3. Add regression tests asserting completeness and order (not just counts) for multi-series bounded and range scans, both directions, inclusive/exclusive bounds.

Found while adding follow-up coverage for #5211.

Activity

  1. added 2 commits that reference this issue on Jul 11, 2026
  2. lvca commented on Jul 11, 2026

    @lvca
    MemberAuthor

    Fixed in main (commit 6416604).

    The problem only affected very large indexes: once an index grows past the compaction memory budget it gets stored as several sorted segments, and a range query (>= AND <=, BETWEEN) could silently skip the segments that fell entirely inside the requested range, returning fewer rows than expected. Ordering a range DESC from an index could also come back ascending. A full scan of the type was always complete, so no data was ever lost on disk - only these bounded/ordered index reads were affected.

    Both are now corrected and covered by regression tests. The fix will ship in the next release.

  3. self-assigned this
    on Jul 11, 2026
  4. added theissue type on Jul 11, 2026
  5. added this to the 26.8.1 milestone on Jul 11, 2026
  6. added a commit that references this issue on Jul 22, 2026
  7. added 3 commits that reference this issue on Aug 14, 2026
  8. added a commit that references this issue on Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions