Skip to content
Draft
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
90 changes: 90 additions & 0 deletions mysql-test/suite/innodb/r/index_lock_upgrade.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#
# MDEV-38814: High rate of index_lock_upgrades due to
# btr_cur_need_opposite_intention() mostly returning true
#
# Test with simple table structure:
# Single INT primary key
# DATETIME column dt that starts NULL and gets updated to a
# distinct timestamp per row
#
# The NULL-to-DATETIME growth makes btr_cur_optimistic_update()
# fall through to the pessimistic path (BTR_MODIFY_TREE).
CREATE TABLE t1 (
id INT NOT NULL,
dt DATETIME NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO t1 (id, dt) SELECT seq, NULL FROM seq_1_to_1000;
# Rows inserted with NULL DATETIME
SELECT COUNT(*) AS rows_inserted FROM t1;
rows_inserted
1000
SELECT COUNT(DISTINCT dt) AS distinct_timestamps FROM t1;
distinct_timestamps
0
# Index lock upgrades during inserts
SELECT @lu01 AS index_lock_upgrades_insert;
index_lock_upgrades_insert
5
SELECT @pu01 AS pessimistic_update_calls_insert;
pessimistic_update_calls_insert
0
SELECT @ou01 AS optim_err_underflows_insert;
optim_err_underflows_insert
0
SELECT @oo01 AS optim_err_overflows_insert;
optim_err_overflows_insert
0
SELECT @ps01 AS index_page_splits_insert;
index_page_splits_insert
6
SELECT @ma01 AS index_page_merge_attempts_insert;
index_page_merge_attempts_insert
0
SELECT @ms01 AS index_page_merge_successful_insert;
index_page_merge_successful_insert
0
SELECT @rs01 AS index_page_reorg_successful_insert;
index_page_reorg_successful_insert
0
SELECT @pd01 AS index_page_discards_insert;
index_page_discards_insert
0
# Now update all rows (batch update)
# This changes DATETIME from NULL to a distinct timestamp per row,
# triggering the pessimistic path due to record size change.
UPDATE t1 JOIN seq_1_to_1000 s ON t1.id = s.seq
SET t1.dt= '2025-01-01' + INTERVAL s.seq SECOND;
# Rows updated
SELECT COUNT(DISTINCT dt) AS distinct_timestamps FROM t1;
distinct_timestamps
1000
# Index lock upgrades during updates
SELECT @lu12 AS index_lock_upgrades_update;
index_lock_upgrades_update
6
SELECT @pu12 AS pessimistic_update_calls_update;
pessimistic_update_calls_update
6
SELECT @ou12 AS optim_err_underflows_update;
optim_err_underflows_update
0
SELECT @oo12 AS optim_err_overflows_update;
optim_err_overflows_update
6
SELECT @ps12 AS index_page_splits_update;
index_page_splits_update
6
SELECT @ma12 AS index_page_merge_attempts_update;
index_page_merge_attempts_update
0
SELECT @ms12 AS index_page_merge_successful_update;
index_page_merge_successful_update
0
SELECT @rs12 AS index_page_reorg_successful_update;
index_page_reorg_successful_update
40
SELECT @pd12 AS index_page_discards_update;
index_page_discards_update
0
DROP TABLE t1;
6 changes: 5 additions & 1 deletion mysql-test/suite/innodb/r/innodb_status_variables.result
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ WHERE variable_name LIKE 'INNODB_%'
AND variable_name NOT IN
('INNODB_ADAPTIVE_HASH_HASH_SEARCHES','INNODB_ADAPTIVE_HASH_NON_HASH_SEARCHES',
'INNODB_MEM_ADAPTIVE_HASH',
'INNODB_BUFFERED_AIO_SUBMITTED','INNODB_BUFFER_POOL_PAGES_LATCHED');
'INNODB_BUFFERED_AIO_SUBMITTED','INNODB_BUFFER_POOL_PAGES_LATCHED',
'INNODB_BTR_CUR_N_INDEX_LOCK_UPGRADES',
'INNODB_BTR_CUR_PESSIMISTIC_UPDATE_CALLS',
'INNODB_BTR_CUR_PESSIMISTIC_UPDATE_OPTIM_ERR_UNDERFLOWS',
'INNODB_BTR_CUR_PESSIMISTIC_UPDATE_OPTIM_ERR_OVERFLOWS');
variable_name
INNODB_ASYNC_READS_PENDING
INNODB_ASYNC_READS_TASKS_RUNNING
Expand Down
155 changes: 155 additions & 0 deletions mysql-test/suite/innodb/t/index_lock_upgrade.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
--source include/have_innodb.inc
--source include/have_innodb_4k.inc
--source include/have_sequence.inc
--source include/have_debug.inc

--echo #
--echo # MDEV-38814: High rate of index_lock_upgrades due to
--echo # btr_cur_need_opposite_intention() mostly returning true
--echo #

--echo # Test with simple table structure:
--echo # Single INT primary key
--echo # DATETIME column dt that starts NULL and gets updated to a
--echo # distinct timestamp per row
--echo #
--echo # The NULL-to-DATETIME growth makes btr_cur_optimistic_update()
--echo # fall through to the pessimistic path (BTR_MODIFY_TREE).
CREATE TABLE t1 (
id INT NOT NULL,
dt DATETIME NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;

--disable_query_log
SET GLOBAL innodb_monitor_enable= 'module_index';
--enable_query_log

let $lu0= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_n_index_lock_upgrades'`;
let $pu0= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_calls'`;
let $ou0= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_optim_err_underflows'`;
let $oo0= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_optim_err_overflows'`;
let $ps0= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_splits'`;
let $ma0= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_merge_attempts'`;
let $ms0= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_merge_successful'`;
let $rs0= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_reorg_successful'`;
let $pd0= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_discards'`;

INSERT INTO t1 (id, dt) SELECT seq, NULL FROM seq_1_to_1000;

--echo # Rows inserted with NULL DATETIME
SELECT COUNT(*) AS rows_inserted FROM t1;
SELECT COUNT(DISTINCT dt) AS distinct_timestamps FROM t1;

let $lu1= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_n_index_lock_upgrades'`;
let $pu1= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_calls'`;
let $ou1= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_optim_err_underflows'`;
let $oo1= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_optim_err_overflows'`;
let $ps1= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_splits'`;
let $ma1= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_merge_attempts'`;
let $ms1= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_merge_successful'`;
let $rs1= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_reorg_successful'`;
let $pd1= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_discards'`;

--disable_query_log
eval SET @lu01= $lu1 - $lu0;
eval SET @pu01= $pu1 - $pu0;
eval SET @ou01= $ou1 - $ou0;
eval SET @oo01= $oo1 - $oo0;
eval SET @ps01= $ps1 - $ps0;
eval SET @ma01= $ma1 - $ma0;
eval SET @ms01= $ms1 - $ms0;
eval SET @rs01= $rs1 - $rs0;
eval SET @pd01= $pd1 - $pd0;
--enable_query_log

--echo # Index lock upgrades during inserts
SELECT @lu01 AS index_lock_upgrades_insert;
SELECT @pu01 AS pessimistic_update_calls_insert;
SELECT @ou01 AS optim_err_underflows_insert;
SELECT @oo01 AS optim_err_overflows_insert;
SELECT @ps01 AS index_page_splits_insert;
SELECT @ma01 AS index_page_merge_attempts_insert;
SELECT @ms01 AS index_page_merge_successful_insert;
SELECT @rs01 AS index_page_reorg_successful_insert;
SELECT @pd01 AS index_page_discards_insert;

--echo # Now update all rows (batch update)
--echo # This changes DATETIME from NULL to a distinct timestamp per row,
--echo # triggering the pessimistic path due to record size change.

UPDATE t1 JOIN seq_1_to_1000 s ON t1.id = s.seq
SET t1.dt= '2025-01-01' + INTERVAL s.seq SECOND;

--echo # Rows updated
SELECT COUNT(DISTINCT dt) AS distinct_timestamps FROM t1;

let $lu2= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_n_index_lock_upgrades'`;
let $pu2= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_calls'`;
let $ou2= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_optim_err_underflows'`;
let $oo2= `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_btr_cur_pessimistic_update_optim_err_overflows'`;
let $ps2= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_splits'`;
let $ma2= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_merge_attempts'`;
let $ms2= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_merge_successful'`;
let $rs2= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_reorg_successful'`;
let $pd2= `SELECT COUNT_RESET FROM INFORMATION_SCHEMA.INNODB_METRICS
WHERE NAME = 'index_page_discards'`;

--disable_query_log
eval SET @lu12= $lu2 - $lu1;
eval SET @pu12= $pu2 - $pu1;
eval SET @ou12= $ou2 - $ou1;
eval SET @oo12= $oo2 - $oo1;
eval SET @ps12= $ps2 - $ps1;
eval SET @ma12= $ma2 - $ma1;
eval SET @ms12= $ms2 - $ms1;
eval SET @rs12= $rs2 - $rs1;
eval SET @pd12= $pd2 - $pd1;
--enable_query_log

--echo # Index lock upgrades during updates
SELECT @lu12 AS index_lock_upgrades_update;
SELECT @pu12 AS pessimistic_update_calls_update;
SELECT @ou12 AS optim_err_underflows_update;
SELECT @oo12 AS optim_err_overflows_update;
SELECT @ps12 AS index_page_splits_update;
SELECT @ma12 AS index_page_merge_attempts_update;
SELECT @ms12 AS index_page_merge_successful_update;
SELECT @rs12 AS index_page_reorg_successful_update;
SELECT @pd12 AS index_page_discards_update;

--disable_query_log
SET GLOBAL innodb_monitor_disable= 'module_index';
--disable_warnings
SET GLOBAL innodb_monitor_disable= default;
SET GLOBAL innodb_monitor_enable= default;
--enable_warnings
--enable_query_log

DROP TABLE t1;
6 changes: 5 additions & 1 deletion mysql-test/suite/innodb/t/innodb_status_variables.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ WHERE variable_name LIKE 'INNODB_%'
AND variable_name NOT IN
('INNODB_ADAPTIVE_HASH_HASH_SEARCHES','INNODB_ADAPTIVE_HASH_NON_HASH_SEARCHES',
'INNODB_MEM_ADAPTIVE_HASH',
'INNODB_BUFFERED_AIO_SUBMITTED','INNODB_BUFFER_POOL_PAGES_LATCHED');
'INNODB_BUFFERED_AIO_SUBMITTED','INNODB_BUFFER_POOL_PAGES_LATCHED',
'INNODB_BTR_CUR_N_INDEX_LOCK_UPGRADES',
'INNODB_BTR_CUR_PESSIMISTIC_UPDATE_CALLS',
'INNODB_BTR_CUR_PESSIMISTIC_UPDATE_OPTIM_ERR_UNDERFLOWS',
'INNODB_BTR_CUR_PESSIMISTIC_UPDATE_OPTIM_ERR_OVERFLOWS');
31 changes: 27 additions & 4 deletions storage/innobase/btr/btr0cur.cc
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ ulint btr_cur_n_sea_old;
#ifdef UNIV_DEBUG
/* Flag to limit optimistic insert records */
uint btr_cur_limit_optimistic_insert_debug;
/** Number of times index lock was upgraded from SX to X */
Atomic_counter<size_t> btr_cur_n_index_lock_upgrades;
/** Number of times btr_cur_pessimistic_update() was called */
Atomic_counter<size_t> btr_cur_pessimistic_update_calls;
/** Number of times DB_UNDERFLOW was returned as optimistic update error in btr_cur_pessimistic_update() */
Atomic_counter<size_t> btr_cur_pessimistic_update_optim_err_underflows;
/** Number of times DB_OVERFLOW was returned as optimistic update error in btr_cur_pessimistic_update() */
Atomic_counter<size_t> btr_cur_pessimistic_update_optim_err_overflows;
#endif /* UNIV_DEBUG */

/** In the optimistic insert, if the insert does not fit, but this much space
Expand Down Expand Up @@ -1599,6 +1607,7 @@ ATTRIBUTE_COLD void mtr_t::index_lock_upgrade()
index_lock *lock= static_cast<index_lock*>(slot.object);
lock->u_x_upgrade(SRW_LOCK_CALL);
slot.type= MTR_MEMO_X_LOCK;
ut_d(++btr_cur_n_index_lock_upgrades);
}

/** Mark a non-leaf page "least recently used", but avoid invoking
Expand Down Expand Up @@ -3602,10 +3611,10 @@ btr_cur_optimistic_update(
goto func_exit;
}

if (UNIV_UNLIKELY(page_get_data_size(page)
if (UNIV_UNLIKELY((new_rec_size < old_rec_size) && page_get_data_size(page)
- old_rec_size + new_rec_size
< BTR_CUR_PAGE_COMPRESS_LIMIT(index))) {
/* The page would become too empty */
/* The page would become too empty due to record shrinkage */
err = DB_UNDERFLOW;
goto func_exit;
}
Expand Down Expand Up @@ -3797,6 +3806,8 @@ btr_cur_pessimistic_update(
block = btr_cur_get_block(cursor);
index = cursor->index();

ut_d(++btr_cur_pessimistic_update_calls);

ut_ad(mtr->memo_contains_flagged(&index->lock, MTR_MEMO_X_LOCK |
MTR_MEMO_SX_LOCK));
ut_ad(mtr->memo_contains_flagged(block, MTR_MEMO_PAGE_X_FIX));
Expand All @@ -3822,6 +3833,9 @@ btr_cur_pessimistic_update(
cursor, offsets, offsets_heap, update,
cmpl_info, thr, trx_id, mtr);

ut_d(btr_cur_pessimistic_update_optim_err_underflows += (err == DB_UNDERFLOW));
ut_d(btr_cur_pessimistic_update_optim_err_overflows += (err == DB_OVERFLOW));

switch (err) {
case DB_ZIP_OVERFLOW:
case DB_UNDERFLOW:
Expand Down Expand Up @@ -3992,8 +4006,17 @@ btr_cur_pessimistic_update(
goto return_after_reservations;
}

rec = btr_cur_insert_if_possible(cursor, new_entry,
offsets, offsets_heap, n_ext, mtr);
if (optim_err == DB_OVERFLOW
&& !buf_block_get_page_zip(block)
&& page_get_max_insert_size_after_reorganize(
block->page.frame, 1) < BTR_CUR_PAGE_REORGANIZE_LIMIT) {
/* The page is too full: force a split instead of
reinserting on the same page. */
rec = NULL;
} else {
rec = btr_cur_insert_if_possible(cursor, new_entry,
offsets, offsets_heap, n_ext, mtr);
}

if (rec) {
page_cursor->rec = rec;
Expand Down
11 changes: 11 additions & 0 deletions storage/innobase/handler/ha_innodb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,17 @@ static SHOW_VAR innodb_status_variables[]= {
/* InnoDB bulk operations */
{"bulk_operations", &export_vars.innodb_bulk_operations, SHOW_SIZE_T},

#ifdef UNIV_DEBUG
{"btr_cur_n_index_lock_upgrades",
&btr_cur_n_index_lock_upgrades, SHOW_SIZE_T},
{"btr_cur_pessimistic_update_calls",
&btr_cur_pessimistic_update_calls, SHOW_SIZE_T},
{"btr_cur_pessimistic_update_optim_err_underflows",
&btr_cur_pessimistic_update_optim_err_underflows, SHOW_SIZE_T},
{"btr_cur_pessimistic_update_optim_err_overflows",
&btr_cur_pessimistic_update_optim_err_overflows, SHOW_SIZE_T},
#endif /* UNIV_DEBUG */

{NullS, NullS, SHOW_LONG}
};

Expand Down
8 changes: 8 additions & 0 deletions storage/innobase/include/btr0cur.h
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,14 @@ extern ulint btr_cur_n_sea_old;
#ifdef UNIV_DEBUG
/* Flag to limit optimistic insert records */
extern uint btr_cur_limit_optimistic_insert_debug;
/** Number of times index lock was upgraded from SX to X */
extern Atomic_counter<size_t> btr_cur_n_index_lock_upgrades;
/** Number of times btr_cur_pessimistic_update() was called */
extern Atomic_counter<size_t> btr_cur_pessimistic_update_calls;
/** Number of times DB_UNDERFLOW was returned as optimistic update error in btr_cur_pessimistic_update() */
extern Atomic_counter<size_t> btr_cur_pessimistic_update_optim_err_underflows;
/** Number of times DB_OVERFLOW was returned as optimistic update error in btr_cur_pessimistic_update() */
extern Atomic_counter<size_t> btr_cur_pessimistic_update_optim_err_overflows;
#endif /* UNIV_DEBUG */

#include "btr0cur.inl"
Expand Down
Loading