Skip to content

MDEV-32286 ANALYZE displays a huge number of InnoDB secondary index pages_accessed#5362

Open
iMineLink wants to merge 2 commits into
MariaDB:10.11from
iMineLink:MDEV-32286
Open

MDEV-32286 ANALYZE displays a huge number of InnoDB secondary index pages_accessed#5362
iMineLink wants to merge 2 commits into
MariaDB:10.11from
iMineLink:MDEV-32286

Conversation

@iMineLink

Copy link
Copy Markdown
Contributor

Added two commits, one for reproducing the issue cited in the ticket, and the second one with an optimization that adds a shortcut to directly fetch the clustered index leaf page with an hint (without full traversal) in case there's correlation during a non-covering secondary index scan.
The optimization self-disengages after 2 consecutive failures.
That's the price these kind of queries have to pay in terms of pages_accessed even when they don't benefit from this (maybe due to fully decorrelated secondary-to-clustered index access pattern).

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses MDEV-32286 by reducing inflated pages_accessed reporting during non-covering secondary-index scans in InnoDB, introducing a fast-path that reuses the previously accessed clustered-index leaf page when access patterns show correlation.

Changes:

  • Add a clustered-leaf “hint” fast-path (btr_cur_t::try_leaf_hint()) to avoid repeated root-to-leaf descents on correlated clustered lookups.
  • Track and reset per-statement hint state in row_prebuilt_t (remembered leaf page + consecutive miss streak with auto-disable).
  • Add an MTR test suite case for non-covering secondary index scans and update expected outputs affected by the new access pattern.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
storage/innobase/row/row0sel.cc Attempts clustered-leaf hint before full clustered-index descent; remembers last clustered leaf page id.
storage/innobase/include/row0mysql.h Adds per-statement state to row_prebuilt_t for clustered leaf hinting and miss streak tracking.
storage/innobase/include/btr0cur.h Declares btr_cur_t::try_leaf_hint() API for direct hinted leaf probing.
storage/innobase/handler/ha_innodb.cc Resets hint state in ha_innobase::reset() to scope hinting to a single statement.
storage/innobase/btr/btr0cur.cc Implements try_leaf_hint() using non-blocking, no-I/O page acquisition and page-local search.
mysql-test/suite/innodb/t/non_covering_sec_idx_scan.test Adds regression/repro test for pages_accessed behavior across correlated and decorrelated patterns.
mysql-test/suite/innodb/r/non_covering_sec_idx_scan.result Captures expected output for the new test case.
mysql-test/main/rowid_filter_innodb.result Updates expected pages_accessed values affected by the optimization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread storage/innobase/btr/btr0cur.cc Outdated
Comment thread storage/innobase/btr/btr0cur.cc Outdated
Comment thread mysql-test/suite/innodb/t/non_covering_sec_idx_scan.test
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread storage/innobase/btr/btr0cur.cc
Comment thread storage/innobase/include/row0mysql.h Outdated
Comment thread storage/innobase/row/row0sel.cc Outdated
Comment thread storage/innobase/row/row0sel.cc Outdated
Comment thread storage/innobase/row/row0sel.cc Outdated
…lation

A full scan of t1 reports pages_accessed=23; a secondary-index scan
touching 1,000 rows sharing one key reports 2003, the figure quoted in
the ticket (selecting a column outside the secondary index forces a
clustered-index lookup per row). t2 (3-level clustered index) and t3
(secondary index decorrelated from clustered key order) record the
baselines 15165 and 20010 for the same shape.

The .result records raw pages_accessed values, so a .opt pins every
server option they depend on: page size (16k), row format, buffer pool
size, and the adaptive hash index.
Row_sel_get_clust_rec_for_mysql::operator() re-descends the clustered
B-tree from the root for every row of a non-covering secondary-index
scan, although consecutive rows often share the same clustered leaf
page; ANALYZE FORMAT=JSON exposes this as an inflated pages_accessed.

Add btr_cur_t::try_leaf_hint(), which tries the leaf remembered in the
new row_prebuilt_t::clust_leaf_hint_page_id before falling back to a
full descent. The hint is a guess, never re-derived from a latched
parent page, so the page is acquired with buf_page_try_get(): by the
time it is tried it may precede the caller's already-latched
secondary-index leaf in the B-tree latching order, where a blocking
latch wait can deadlock (page latches have no deadlock detection), and
BUF_GET would both trip the ignore_unfixed assertion on a
freed-but-cached page and do a synchronous read for an evicted one.
Freed pages are rejected explicitly: their stale contents would still
pass the leaf/index-id validation.

A tuple sorting after the last record of a non-rightmost leaf may have
its PAGE_CUR_LE match on a later leaf, which cannot be resolved from
the hinted page. The page-local search already proves this cheaply:
its low_match falling short of n_fields_cmp with the cursor on the
last user record means the tuple sorts strictly after every record on
the page, so no separate boundary-record comparison is needed.

Attempting the hint on every row regresses uncorrelated scans (each
miss adds a real, pages_accessed-charged probe on top of the descent it
falls back to), so the new row_prebuilt_t::clust_leaf_hint_miss_streak
abandons the hint for the rest of the statement after
CLUST_LEAF_HINT_MAX_MISSES (2) consecutive misses. The threshold must
exceed 1 because a well-correlated scan misses once at every leaf-page
boundary. The hint page id and streak are reset per statement in
ha_innobase::reset(), matching autoinc_last_value.

innodb.non_covering_sec_idx_scan: 2003 to 1046 (2-level clustered
index), 15165 to 7309 (3-level, confirming internal levels are skipped
too, not just the root), 20010 to 20012 (uncorrelated: only the two
misses paid before giving up). rowid_filter_innodb: 84 to 86, the same
bounded two-miss cost.
@iMineLink

Copy link
Copy Markdown
Contributor Author

I reordered and adjusted the datatypes of the new row_prebuilt_t members.
Now they fit in pre-existing holes in the 3rd cacheline as verified with pahole:

### Before

$ pahole -C row_prebuilt_t build/RelWithDebInfo/storage/innobase/CMakeFiles/innobase.dir/row/row0sel.cc.o

...

struct btr_pcur_t *        pcur;                 /*   120     8 */
/* --- cacheline 2 boundary (128 bytes) --- */
struct btr_pcur_t *        clust_pcur;           /*   128     8 */
struct que_fork_t *        sel_graph;            /*   136     8 */
struct dtuple_t *          search_tuple;         /*   144     8 */
byte                       row_id[6];            /*   152     6 */

/* XXX 2 bytes hole, try to pack */

doc_id_t                   fts_doc_id;           /*   160     8 */
struct dtuple_t *          clust_ref;            /*   168     8 */
enum lock_mode             select_lock_type;     /*   176     4 */
bool                       skip_locked;          /*   180     1 */

/* XXX 3 bytes hole, try to pack */

enum lock_mode             stored_select_lock_type; /*   184     4 */

/* XXX 4 bytes hole, try to pack */

/* --- cacheline 3 boundary (192 bytes) --- */
ulint                      row_read_type;        /*   192     8 */

...

/* size: 456, cachelines: 8, members: 62 */
/* sum members: 418, holes: 8, sum holes: 34 */
/* sum bitfield members: 30 bits, bit holes: 1, sum bit holes: 2 bits */
/* last cacheline: 8 bytes */
### After

...

struct btr_pcur_t *        pcur;                 /*   120     8 */
/* --- cacheline 2 boundary (128 bytes) --- */
struct btr_pcur_t *        clust_pcur;           /*   128     8 */
struct que_fork_t *        sel_graph;            /*   136     8 */
struct dtuple_t *          search_tuple;         /*   144     8 */
byte                       row_id[6];            /*   152     6 */

/* XXX 2 bytes hole, try to pack */

doc_id_t                   fts_doc_id;           /*   160     8 */
struct dtuple_t *          clust_ref;            /*   168     8 */
enum lock_mode             select_lock_type;     /*   176     4 */
bool                       skip_locked;          /*   180     1 */
uint8_t                    clust_leaf_hint_miss_streak; /*   181     1 */

/* XXX 2 bytes hole, try to pack */

uint32_t                   clust_leaf_hint_page_no; /*   184     4 */
enum lock_mode             stored_select_lock_type; /*   188     4 */
/* --- cacheline 3 boundary (192 bytes) --- */
ulint                      row_read_type;        /*   192     8 */

...

/* size: 456, cachelines: 8, members: 64 */
/* sum members: 423, holes: 7, sum holes: 29 */
/* sum bitfield members: 30 bits, bit holes: 1, sum bit holes: 2 bits */
/* last cacheline: 8 bytes */

On a side note, it could be possible to reorder the fields in the struct to optimize its layout.

@iMineLink iMineLink requested a review from Thirunarayanan July 15, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants