Skip to content

MDEV-32067 InnoDB linear read ahead had better be logical#4600

Open
Thirunarayanan wants to merge 1 commit into
mainfrom
main_MDEV-32067
Open

MDEV-32067 InnoDB linear read ahead had better be logical#4600
Thirunarayanan wants to merge 1 commit into
mainfrom
main_MDEV-32067

Conversation

@Thirunarayanan

@Thirunarayanan Thirunarayanan commented Jan 28, 2026

Copy link
Copy Markdown
Member

MDEV-32067 InnoDB linear read ahead had better be logical

The traditional linear read-ahead, enabled by innodb_read_ahead_threshold=56,
only works if pages are allocated on adjacent page numbers, which is not
always the case for B-tree leaf pages.

After this change, the exact nonzero values of
innodb_read_ahead_threshold matter only for the read-ahead of
undo log pages.

Introduced Multi-Range Read (MRR) aware read-ahead that collects
actual leaf page numbers during B-tree traversal

buf_read_ahead_undo(): Renamed from buf_read_ahead_linear().
This function will no longer be invoked on any BLOB pages
(for which FIL_PAGE_PREV and FIL_PAGE_NEXT were not initialized
consistently) nor on any index pages. For index leaf pages,
we will introduce buf_read_ahead_one() and buf_read_ahead_pages().

buf_read_ahead_one(): Read ahead one (sibling leaf) page.
This logic cannot be disabled.

buf_read_ahead_pages(): Read ahead B-tree index leaf pages.

buf_read_ahead_random(): Split the function into two parts: one
that determines which range of pages should be read, and another
that actually initiates a read of the pages.

btr_pcur_move_to_next_page(): Invoke buf_read_ahead_one()
instead of buf_read_ahead_linear().

btr_pcur_move_backward_from_page(): Implement a fast path of
trying to acquire a latch on the previous page without waiting,
and invoke buf_read_ahead_one() on the preceding page, with the
assumption that we may be accessing that page in the near future.

btr_copy_blob_prefix(): Simplify the logic. On other than
ROW_FORMAT=COMPRESSED BLOB pages, the FIL_PAGE_NEXT field is not
meaningfully initialized. The FIL_PAGE_PREV field is not pointing
to anything meaningful either. buf_read_ahead_linear() expects
these to be set meaningfully. Only the non-default setting
innodb_random_read_ahead=ON might be meaningful here.

btr_cur_t::search_leaf(): Add MRR read-ahead context to collect
leaf page numbers at PAGE_LEVEL=1 during B-tree traversal.
The collected page numbers represent actual leaf pages that
will be accessed, enabling more targeted
read-ahead than linear page number assumptions.

mrr_readahead_ctx_t: New structure for passing MRR context
through the call chain from ha_innobase -> row_search_mvcc()
-> btr_pcur_open() -> search_leaf() and it has
READ_AHEAD_PAGES=64 limit.

@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.

@Thirunarayanan
Thirunarayanan force-pushed the main_MDEV-32067 branch 5 times, most recently from 1fc14cb to 7fa9c70 Compare February 2, 2026 07:21
@Thirunarayanan
Thirunarayanan requested a review from dr-m February 3, 2026 09:03
@Thirunarayanan
Thirunarayanan marked this pull request as ready for review February 3, 2026 09:03

@dr-m dr-m left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I only reviewed a small part of this so far. Please debug this with undo tablespace truncation enabled.

Comment thread storage/innobase/buf/buf0rea.cc
Comment thread storage/innobase/buf/buf0rea.cc Outdated
Comment thread storage/innobase/buf/buf0rea.cc
Comment thread storage/innobase/buf/buf0rea.cc
Comment thread storage/innobase/buf/buf0rea.cc Outdated
Comment thread storage/innobase/buf/buf0rea.cc Outdated
Comment thread storage/innobase/buf/buf0rea.cc Outdated
Comment thread storage/innobase/buf/buf0rea.cc

@iMineLink iMineLink left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new logic is a step forward towards more precise and efficient read-ahead logic.
I think that it can be extended easily to non MRR queries, by always passing a context to row_search_mvcc() initialized with the default 64 pages when no explicit limit is present.

Comment thread sql/multi_range_read.cc Outdated
{
/* Calculate pages needed, with some buffer for safety */
uint pages_needed=
(uint)((limit + records_per_page - 1) / records_per_page);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe return 0 if records_per_page is 0 ?

Comment thread sql/multi_range_read.cc Outdated
/* Use table statistics to estimate records per page */
ha_rows total_rows= table->file->stats.records;
ha_rows index_pages= table->file->stats.data_file_length /
table->file->stats.block_size;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe return 0 if table->file->stats.block_size is 0?

Comment thread sql/multi_range_read.cc Outdated
/* Apply bounds based on key size */
uint key_length= key_info->key_length;
uint page_size= table->file->stats.block_size;
uint max_records= page_size / key_length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe return 0 if key_length is 0 ?

Comment thread sql/multi_range_read.cc Outdated

uint max_pages_for_limit= 0;
if (limit_hint == HA_POS_ERROR);
else if (limit_hint > 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe rewrite condition as limit_hint != HA_POS_ERROR && limit_hint > 2 ? Can you please comment on the behavior when limit_hint==1 ?

Comment thread sql/multi_range_read.cc Outdated
if (!height && first && first_access)
buf_read_ahead_linear(page_id_t(block->page.id().space(), page));
}
if (latch_mode != BTR_MODIFY_TREE);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems this is idiomatic but maybe:

if (latch_mode != BTR_MODIFY_TREE) { /* Do nothing */ }
else ...

is more expressive?

/* On other BLOB pages except the first the BLOB header
always is at the page data start: */

offset = FIL_PAGE_DATA;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe this is not useful now that offset = FIL_PAGE_DATA is in the for loop?

{
skip:
space->release();
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe do not stop at first cached page?

if (space->is_stopping())
break;
buf_pool_t::hash_chain &chain= buf_pool.page_hash.cell_get(new_low.fold());
space->reacquire();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since space->acquire() as added earlier, I suppose extra space->release() calls are needed to keep old behavior.
I'm looking at the early exit paths goto func_exit;, I suppose space->release() should be called before jumping.

Comment thread storage/innobase/buf/buf0rea.cc
The traditional linear read-ahead, enabled by innodb_read_ahead_threshold,
only helps when consecutively accessed pages have adjacent page numbers.
That is rarely true for B-tree leaf pages: after splits, merges and page
reuse, the logical order of leaves has nothing to do with their
physical page numbers. So for a scan of scattered leaves the
old read-ahead either did nothing or read the wrong pages.

This replaces linear read-ahead of index pages with a logical
read-ahead that prefetches the actual leaf pages a scan is
going to visit, discovered from the B-tree during traversal.
After this change, the exact nonzero value of innodb_read_ahead_threshold
matters only for the read-ahead of undo log pages.

records_in_range() reports the first and last leaf page of a range,
and the optimizer forwards that extent to the engine through
handler::advise_page_range() so read-ahead can be sized and
bounded to the scan.

buf_read_ahead_undo(): Renamed from buf_read_ahead_linear(). Now invoked
only for undo log pages, whose page numbers are sequential. No longer called
on BLOB pages or index pages.

buf_read_ahead_one(): Read ahead a single leaf page. Cannot be disabled.

buf_read_ahead_pages(): Read ahead a set of known B-tree leaf pages.

buf_read_ahead_random(): Split into a decision part (which extent to read,
in buf0buf.cc) and an action part (issue the reads).

The pending-read throttle now compares os_aio_pending_reads_approx() with
buf_pool.curr_size() (a page count) instead of curr_pool_size() (bytes),
which had made the throttle a no-op. buf_read_ahead_pages() releases the
tablespace reference on a failed/corrupted page.

btr_read_ahead_t: A context which records the level-1 page and the
child page number of the last node pointer collected,
so read-ahead can be resumed later.

btr_read_ahead_collect(): scan the node pointers of a PAGE_LEVEL=1
page from a given record in scan order, appending child page numbers.

btr_cur_t::search_leaf(), btr_cur_t::open_leaf(): At PAGE_LEVEL=1, after the
descent has located the child to follow, harvest leaf page numbers starting
at that child in the scan direction. These are exactly the
leaves the cursor will visit, so the prefetch is precise regardless of
physical page numbers.

btr_read_ahead_resume_rec(): Locate the record from which read-ahead
should resume, by re-finding the last harvested child by value. It walks only
genuine records, so a concurrent page reorganization cannot cause an invalid
read (a stored byte offset could).

btr_pcur_move_to_next_page(), btr_pcur_move_backward_from_page(): Invoke
buf_read_ahead_one() on the following/preceding sibling.

btr_copy_blob_prefix(): Simplified; no longer relies on FIL_PAGE_PREV/NEXT.

advise_page_range(): Records the scan's estimated leaf extent.
Used only to size and bound read-ahead; it never
gates the pages the scan actually reads.
mrr_readahead_from_scan_range() derives the read-ahead ceiling from
the advised extent, falling back to a LIMIT-based estimate.
The window ramps up from small number of pages, doubling on each
refill toward the ceiling, so single-row probes such as index_first() for
MIN()/MAX() prefetch only a few leaves while long scans reach full depth.

start_readahead(): For a just-positioned scan, prefetch the collected batch
and set up the rolling cursor.

readahead_refill(): As the scan advances (general_fetch()), resume the
level-1 harvest under an index S-latch, chain across level-1 siblings, and
prefetch the next batch bounded by the advised last leaf, or to the end of
the index when the extent is unknown.

Read-ahead is started from index_read() (range and full index scans),
rnd_init()/general_fetch() (full table scans), and, at one-page depth, from
row_merge_read_clustered_index() (OPTIMIZE/ALTER rebuild). general_fetch()
sets active_handler_stats before issuing read-ahead so prefetched pages are
attributed to the query.

A truncation race is closed with a new STOPPING_READS tablespace flag:
mtr_t::commit_shrink() sets it before shrinking and clears it after, and
buf_read_ahead_undo() re-checks space->is_stopping().

trx_undo_get_prev_rec(), trx_undo_get_prev_rec_from_prev_page():
take the trx_undo_t object instead of a long parameter list
and always latch shared.

handler::multi_range_read_info_const() and DsMrr_impl::dsmrr_info_const()
Passed a page_range parameter; the default MRR implementation aggregates
the per-range leaf extents.

opt_range.cc carries it through the chosen QUICK_RANGE_SELECT, and
QUICK_RANGE_SELECT::reset() calls advise_page_range() before
multi_range_read_init().

main.analyze_stmt_prefetch_count: corrected for double counting now that
pages_read_count includes waits on prefetched pages.
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.

5 participants