Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mysql-test/main/rowid_filter_innodb.result
Original file line number Diff line number Diff line change
Expand Up @@ -1958,7 +1958,7 @@ ANALYZE
"r_table_time_ms": "REPLACED",
"r_other_time_ms": "REPLACED",
"r_engine_stats": {
"pages_accessed": 84
"pages_accessed": 86
},
"filtered": "REPLACED",
"r_filtered": 2.43902439,
Expand Down Expand Up @@ -2112,7 +2112,7 @@ ANALYZE
"r_table_time_ms": "REPLACED",
"r_other_time_ms": "REPLACED",
"r_engine_stats": {
"pages_accessed": 84
"pages_accessed": 86
},
"filtered": "REPLACED",
"r_filtered": 2.43902439,
Expand Down
73 changes: 73 additions & 0 deletions mysql-test/suite/innodb/r/non_covering_sec_idx_scan.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
create table t1 (
pk int not null primary key,
domain_grp int,
val int
) engine=innodb;
insert into t1 select seq, mod(seq,10), seq from seq_1_to_10000;
create index domain_idx on t1(domain_grp);
analyze table t1 persistent for all;
Table Op Msg_type Msg_text
test.t1 analyze status Engine-independent statistics collected
test.t1 analyze status OK
set @js='$out_scan';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_FULL_SCAN;
PAGES_ACCESSED_FULL_SCAN
23
set @js='$out_idx';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_SEC_INDEX;
PAGES_ACCESSED_SEC_INDEX
1046
drop table t1;
create table t2 (
pk varchar(500) not null primary key,
domain_grp int,
val int
) engine=innodb;
insert into t2 select lpad(seq,500,'0'), mod(seq,4), seq from seq_1_to_20000;
create index domain_idx on t2(domain_grp);
analyze table t2 persistent for all;
Table Op Msg_type Msg_text
test.t2 analyze status Engine-independent statistics collected
test.t2 analyze status OK
select stat_value into @size from mysql.innodb_index_stats
where database_name='test' and table_name='t2' and index_name='PRIMARY'
and stat_name='size';
select stat_value into @leaf from mysql.innodb_index_stats
where database_name='test' and table_name='t2' and index_name='PRIMARY'
and stat_name='n_leaf_pages';
select @size as TOTAL_PAGES, @leaf as LEAF_PAGES, (@size - @leaf) as NON_LEAF_PAGES;
TOTAL_PAGES LEAF_PAGES NON_LEAF_PAGES
761 715 46
select (@size - @leaf) > 1 as THREE_LEVEL_TREE;
THREE_LEVEL_TREE
1
set @js='$out_scan2';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_FULL_SCAN_3LEVEL;
PAGES_ACCESSED_FULL_SCAN_3LEVEL
717
set @js='$out_idx2';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_SEC_INDEX_3LEVEL;
PAGES_ACCESSED_SEC_INDEX_3LEVEL
7309
drop table t2;
create table t3 (
pk int not null primary key,
k int,
val int
) engine=innodb;
insert into t3 select seq, mod(seq*997+13,10007), seq from seq_1_to_10000;
create index k_idx on t3(k);
analyze table t3 persistent for all;
Table Op Msg_type Msg_text
test.t3 analyze status Engine-independent statistics collected
test.t3 analyze status OK
set @js='$out_uncorr';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_UNCORRELATED;
PAGES_ACCESSED_UNCORRELATED
20012
drop table t3;
4 changes: 4 additions & 0 deletions mysql-test/suite/innodb/t/non_covering_sec_idx_scan.opt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
--innodb-page-size=16k
--innodb-buffer-pool-size=64M
--innodb-default-row-format=dynamic
--loose-innodb-adaptive-hash-index=0
94 changes: 94 additions & 0 deletions mysql-test/suite/innodb/t/non_covering_sec_idx_scan.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#
# MDEV-32286 ANALYZE displays a huge number of InnoDB secondary index pages_accessed
#
# The .result records raw pages_accessed values, so the .opt pins every
# server option they depend on: page size and row format (tree shape),
# buffer pool size (the whole working set must stay resident, so that no
# access path depends on eviction timing), and the adaptive hash index
# (its hash guesses skip descent pages).
--source include/have_innodb.inc
--source include/have_sequence.inc

create table t1 (
pk int not null primary key,
domain_grp int,
val int
) engine=innodb;

insert into t1 select seq, mod(seq,10), seq from seq_1_to_10000;

create index domain_idx on t1(domain_grp);

analyze table t1 persistent for all;

# "val" is outside domain_idx, so each matching row needs a clustered-index
# lookup either way.
let $out_scan=`analyze format=json select sql_no_cache val from t1 ignore index(domain_idx) where domain_grp=3`;
evalp set @js='$out_scan';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_FULL_SCAN;

# ~1000 rows share key 3
let $out_idx=`analyze format=json select sql_no_cache val from t1 force index(domain_idx) where domain_grp=3`;
evalp set @js='$out_idx';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_SEC_INDEX;
Comment thread
iMineLink marked this conversation as resolved.

drop table t1;

# Wide primary key: the clustered index needs a 3-level tree.
create table t2 (
pk varchar(500) not null primary key,
domain_grp int,
val int
) engine=innodb;

insert into t2 select lpad(seq,500,'0'), mod(seq,4), seq from seq_1_to_20000;

create index domain_idx on t2(domain_grp);

analyze table t2 persistent for all;

# more than one non-leaf page implies an internal level below the root
select stat_value into @size from mysql.innodb_index_stats
where database_name='test' and table_name='t2' and index_name='PRIMARY'
and stat_name='size';
select stat_value into @leaf from mysql.innodb_index_stats
where database_name='test' and table_name='t2' and index_name='PRIMARY'
and stat_name='n_leaf_pages';
select @size as TOTAL_PAGES, @leaf as LEAF_PAGES, (@size - @leaf) as NON_LEAF_PAGES;
select (@size - @leaf) > 1 as THREE_LEVEL_TREE;

let $out_scan2=`analyze format=json select sql_no_cache val from t2 ignore index(domain_idx) where domain_grp=3`;
evalp set @js='$out_scan2';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_FULL_SCAN_3LEVEL;

# ~5000 rows share key 3
let $out_idx2=`analyze format=json select sql_no_cache val from t2 force index(domain_idx) where domain_grp=3`;
evalp set @js='$out_idx2';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_SEC_INDEX_3LEVEL;

drop table t2;

# Secondary index decorrelated from clustered key order: the clustered-leaf
# hint must give up instead of paying a wasted probe on every row.
create table t3 (
pk int not null primary key,
k int,
val int
) engine=innodb;

insert into t3 select seq, mod(seq*997+13,10007), seq from seq_1_to_10000;

create index k_idx on t3(k);

analyze table t3 persistent for all;

let $out_uncorr=`analyze format=json select sql_no_cache val from t3 force index(k_idx) where k between 0 and 10006`;
evalp set @js='$out_uncorr';
set @out=(select json_extract(@js,'$**.r_engine_stats.pages_accessed'));
select cast(json_extract(@out,'$[0]') as DOUBLE) as PAGES_ACCESSED_UNCORRELATED;

drop table t3;
61 changes: 61 additions & 0 deletions storage/innobase/btr/btr0cur.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,67 @@ dberr_t btr_cur_t::search_leaf(const dtuple_t *tuple, page_cur_mode_t mode,
goto search_loop;
}

bool btr_cur_t::try_leaf_hint(const dtuple_t *tuple, page_id_t hint_page_id,
mtr_t *mtr)
{
/* hint_page_id was not read from a latched parent page, so it may now
precede the caller's already-latched secondary-index leaf in the
B-tree latching order: never block on its latch (page latches have no
deadlock detection) and never read it from disk. */
buf_block_t *const block= buf_page_try_get(hint_page_id, mtr);
if (!block)
return false;

const page_t *const page= block->page.frame;
if (block->page.is_freed() || !fil_page_index_page_check(page) ||
!page_is_leaf(page) ||
!!page_is_comp(page) != index()->table->not_redundant() ||
btr_page_get_index_id(page) != index()->id)
{
/* Stale hint or, for search_leaf()'s own checks, corruption; we cannot
tell here, so fall back either way, and the full descent still reports a
corrupt live leaf. is_freed() is the guard that descent omits: a freed
but unreused page keeps old contents that pass the other checks. */
mtr->release_last_page();
return false;
}

page_cur.block= block;
up_match= 0;
up_bytes= 0;
low_match= 0;
low_bytes= 0;
if (page_cur_search_with_match(tuple, PAGE_CUR_LE, &up_match, &low_match,
&page_cur, nullptr) ||
page_rec_is_infimum(page_cur.rec))
{
/* Corruption, or tuple precedes every record on this page: its
predecessor, if any, is on an earlier leaf. */
mtr->release_last_page();
return false;
}

if (page_has_next(page) && low_match < dtuple_get_n_fields_cmp(tuple))
{
/* The record found is strictly less than tuple: PAGE_CUR_LE lands on
the greatest record <= tuple, and a full match would have made
low_match == n_fields_cmp. If it is the last user record of a leaf
with a right sibling, the true match may be on a later leaf; we cannot
resolve that from here, so reject the hint. The rightmost leaf needs
no such check: its last record is that match for any larger tuple. */
const rec_t *const next_rec= page_rec_get_next_const(page_cur.rec);
if (UNIV_UNLIKELY(!next_rec) || page_rec_is_supremum(next_rec))
{
mtr->release_last_page();
return false;
}
}

/* Unlike search_leaf(), this feeds no btr_search_info_update(): a hit
already provides the direct leaf access the adaptive hash index would. */
return true;
}

ATTRIBUTE_COLD void mtr_t::index_lock_upgrade()
{
auto &slot= m_memo[get_savepoint() - 1];
Expand Down
4 changes: 4 additions & 0 deletions storage/innobase/handler/ha_innodb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16037,6 +16037,10 @@ ha_innobase::reset()
/* This is a statement level counter. */
m_prebuilt->autoinc_last_value = 0;

/* The clustered leaf hint is scoped to one statement. */
m_prebuilt->clust_leaf_hint_page_no = 0;
m_prebuilt->clust_leaf_hint_miss_streak = 0;

m_prebuilt->skip_locked = false;
return(0);
}
Expand Down
13 changes: 13 additions & 0 deletions storage/innobase/include/btr0cur.h
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,19 @@ struct btr_cur_t {
@return error code */
inline dberr_t open_random_leaf(rec_offs *&offsets, mem_heap_t *& heap,
mtr_t &mtr);

/** Try a PAGE_CUR_LE, BTR_SEARCH_LEAF lookup directly on a previously
remembered leaf page instead of descending from the root (MDEV-32286).
The hint is a guess, never derived from a latched parent page, so the
page is acquired via a non-blocking, no-I/O buf_page_try_get() and a
miss (stale hint) is expected, not corruption; the caller falls back
to a normal search.
@param tuple key to search for; must use n_fields_cmp fields
@param hint_page_id remembered leaf page id
@param mtr mini-transaction
@return whether the cursor was positioned on the hinted leaf page */
bool try_leaf_hint(const dtuple_t *tuple, page_id_t hint_page_id,
mtr_t *mtr);
};

/** Modify the delete-mark flag of a record.
Expand Down
14 changes: 14 additions & 0 deletions storage/innobase/include/row0mysql.h
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,20 @@ struct row_prebuilt_t {
sel/upd/del */
lock_mode select_lock_type;/*!< LOCK_NONE, LOCK_S, or LOCK_X */
bool skip_locked; /*!< TL_{READ,WRITE}_SKIP_LOCKED */
uint8_t clust_leaf_hint_miss_streak; /*!< Consecutive
btr_cur_t::try_leaf_hint() misses; at
CLUST_LEAF_HINT_MAX_MISSES (row0sel.cc) the hint
is abandoned for the rest of the statement by
zeroing clust_leaf_hint_page_no below.
Reset per statement in ha_innobase::reset(),
matching autoinc_last_value. */
uint32_t clust_leaf_hint_page_no; /*!< Clustered leaf page
number remembered from the previous
Row_sel_get_clust_rec_for_mysql() lookup in this
statement, for btr_cur_t::try_leaf_hint(); the
tablespace is always the clustered index's own.
0 (the zero-initialized state; page 0 is the FSP
header, never a leaf) means no hint. */
lock_mode stored_select_lock_type;/*!< this field is used to
remember the original select_lock_type
that was decided in ha_innodb.cc,
Expand Down
44 changes: 43 additions & 1 deletion storage/innobase/row/row0sel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ to que_run_threads: this is to allow canceling runaway queries */

#define SEL_COST_LIMIT 100

/** Consecutive btr_cur_t::try_leaf_hint() misses at which
Row_sel_get_clust_rec_for_mysql gives up on the hint for the rest of the
statement. Must be > 1: a well-correlated scan misses once at every
leaf-page boundary. */
#define CLUST_LEAF_HINT_MAX_MISSES 2

/* Flags for search shortcut */
#define SEL_FOUND 0
#define SEL_EXHAUSTED 1
Expand Down Expand Up @@ -3397,9 +3403,45 @@ Row_sel_get_clust_rec_for_mysql::operator()(
clust_index = dict_table_get_first_index(sec_index->table);
prebuilt->clust_pcur->btr_cur.page_cur.index = clust_index;

dberr_t err = btr_pcur_open_with_no_init(prebuilt->clust_ref,
/* Consecutive rows of a non-covering secondary-index scan often
share a clustered leaf page, so try the remembered leaf before a
full descent; give up once misses show the scan has no correlation
to clustered key order. */
dberr_t err;
if (prebuilt->clust_leaf_hint_page_no
&& prebuilt->clust_pcur->btr_cur.try_leaf_hint(
prebuilt->clust_ref,
page_id_t(clust_index->table->space_id,
prebuilt->clust_leaf_hint_page_no),
mtr)) {
err = DB_SUCCESS;
/* Mirror what btr_pcur_open_with_no_init() below sets for
the same (PAGE_CUR_LE, BTR_SEARCH_LEAF) arguments. */
prebuilt->clust_pcur->latch_mode
= BTR_LATCH_MODE_WITHOUT_INTENTION(BTR_SEARCH_LEAF);
prebuilt->clust_pcur->search_mode = PAGE_CUR_LE;
prebuilt->clust_pcur->pos_state = BTR_PCUR_IS_POSITIONED;
prebuilt->clust_leaf_hint_miss_streak = 0;
} else {
err = btr_pcur_open_with_no_init(prebuilt->clust_ref,
PAGE_CUR_LE, BTR_SEARCH_LEAF,
prebuilt->clust_pcur, mtr);
if (prebuilt->clust_leaf_hint_page_no
&& ++prebuilt->clust_leaf_hint_miss_streak
>= CLUST_LEAF_HINT_MAX_MISSES) {
/* Give up on the hint for the rest of the
statement. */
prebuilt->clust_leaf_hint_page_no = 0;
} else if (err == DB_SUCCESS
&& prebuilt->clust_leaf_hint_miss_streak
< CLUST_LEAF_HINT_MAX_MISSES) {
/* Remember the leaf this descent landed on, for
the next lookup in this statement. */
prebuilt->clust_leaf_hint_page_no
= prebuilt->clust_pcur->btr_cur.page_cur
.block->page.id().page_no();
}
}
if (UNIV_UNLIKELY(err != DB_SUCCESS)) {
return err;
}
Expand Down