PS-10110 (8.4) Fix poor MyRocks query plans caused by 1-row range estimates#6063
Draft
polchawa-percona wants to merge 1 commit into
Draft
PS-10110 (8.4) Fix poor MyRocks query plans caused by 1-row range estimates#6063polchawa-percona wants to merge 1 commit into
polchawa-percona wants to merge 1 commit into
Conversation
https://perconadev.atlassian.net/browse/PS-10110 MyRocks estimated the number of rows in a key range as index_rows * (GetApproximateSizes(range) / index_disk_size). GetApproximateSizes() works at SST data block offset granularity and returns 0 when the whole range falls inside a single (~16KB) data block; GetApproximateMemTableStats() has the same flaw for narrow memtable ranges. The zero estimate was floored to 1 row, so a selective non-unique range containing hundreds of rows was reported to the optimizer as 1 row, causing poor join orders and large performance regressions compared to InnoDB. Fix by refining small estimates with a bounded index dive, similar to InnoDB index dives: when the approximate estimate is below the new session variable rocksdb_records_in_range_dive_threshold (default 100, 0 disables), scan the range with a key-only iterator reading at most threshold keys. Fewer keys than the cap gives an exact count; hitting the cap gives the threshold as a lower bound. Reverse column families and successor-adjusted bounds are handled; partial indexes are skipped because a raw iterator would undercount unmaterialized groups; the rocksdb_records_in_range / rocksdb_force_index_records_in_range / rocksdb_debug_optimizer_n_rows overrides keep precedence. The dive only triggers when the estimate is already pathologically small, i.e. the range lies within 1-2 data blocks, so its cost is bounded and comparable to the GetApproximateSizes() call it refines. index_merge_rocksdb.result: estimates are now the exact key counts. rocksdb.result: new variable in SHOW VARIABLES output.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://perconadev.atlassian.net/browse/PS-10110
Problem
MyRocks estimates the number of rows in a key range as
index_rows * (GetApproximateSizes(range) / index_disk_size). RocksDB'sGetApproximateSizes()works at SST data-block offset granularity and returns 0 when the whole range falls inside a single (~16KB) data block;GetApproximateMemTableStats()has the same flaw for narrow memtable ranges (this is the variant reproduced by Dmitry Lenev in the ticket —records_in_range()estimates go wrong before any data reaches SSTs too). The zero estimate is floored to 1 row, so a selective non-unique range containing hundreds of rows is reported to the optimizer as 1 row, causing poor join orders and large performance regressions vs InnoDB (the customer's 0.00s query took 0.28s).Fix
Refine small estimates with a bounded index dive, similar to InnoDB index dives: when the approximate estimate is below the new session variable
rocksdb_records_in_range_dive_threshold(default 100,0disables,SET_VAR-hintable), scan the range with a key-onlyrocksdb::Iteratorreading at most threshold keys:max(approx, threshold)).The dive only triggers when the estimate is already pathologically small — i.e. the range lies within 1–2 data blocks — so its cost is bounded and comparable to the
GetApproximateSizes()call it refines. Reverse column families and successor-adjusted bounds are handled; partial indexes are skipped (a raw iterator would undercount unmaterialized groups); the existingrocksdb_records_in_range,rocksdb_force_index_records_in_rangeandrocksdb_debug_optimizer_n_rowsoverrides keep precedence. An alternative fix inside RocksDB'sBlockBasedTable::ApproximateSize()was rejected: it would diverge the vendored RocksDB from upstream for all callers, still could not distinguish 3 rows from 500 within one block, and would not fix the memtable path.Tests
rocksdb.records_in_range_dive: reproduces the bug shape (200-row group hidden among 20000 rows) on both the memtable-only and SST paths, forward and reverse CFs, and uses a dedicated single-data-block column family whereGetApproximateSizes()returns 0 by construction to deterministically demonstrate the legacy 1-row collapse, exact counts, and cap semantics. Verified stable across 5 repeat runs (noFORCE INDEX— that path skipsrecords_in_range()entirely, which is also why the ticket's repro attempts with forced indexes did not hit the customer's stack).rocksdb_sys_vars.rocksdb_records_in_range_dive_threshold_basic(satisfiesrocksdb_sys_vars.all_vars).rocksdb.index_merge_rocksdb(estimates are now the exact key counts: 1→3, 2→4) androcksdb.rocksdb(new variable in SHOW VARIABLES).rocksdb+rocksdb_sys_varssuites run locally: remaining failures (bloomfilter*,drop_table2/3timeouts,partial_index_stressmissingpython) reproduce identically with the dive disabled → environmental;add_index_inplace/trx_info_rplpass repeatedly standalone → load-induced flakes.An 8.0 port can follow as a separate PR once this is approved.