mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
MDEV-41351 Allow a covering index scan for a SELECT ... FOR UPDATE under a full-scan table lock
MDEV-24813 introduced a switch innodb_table_lock_on_full_scan that requests a table lock on an innodb table for some unconditional SELECT/UPDATE/DELETE statements. With a full table lock, and when doing a covering scan, there is no need to request clustered index reads. This patch does so for X-locks in SELECT statements. Prior to this it is already the case for S-locks. There is one exception: we disable this optimization when inside a transaction with innodb_snapshot_isolation and a read view already open, in which case it needs trx_id obtained from the clustered index to check if there is a committed change the transaction never saw. Assisted-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
--- innodb_full_scan.result
|
||||
+++ innodb_full_scan,table_lock.result
|
||||
+++ innodb_full_scan,table_lock.reject
|
||||
@@ -10,14 +10,14 @@
|
||||
42
|
||||
COMMIT;
|
||||
@@ -119,3 +119,19 @@
|
||||
drop table t10, t11;
|
||||
# Range access
|
||||
create table t10 (a int, b varchar(100), index(a)) engine=innodb;
|
||||
@@ -185,7 +185,7 @@
|
||||
# 10000 rows; clustered path costs ~2/row
|
||||
SELECT ($e - $s) < 10000 AS covering;
|
||||
covering
|
||||
-0
|
||||
+1
|
||||
connect con1,localhost,root,,;
|
||||
SET innodb_lock_wait_timeout=1;
|
||||
# Case 1: covering point lookup (never a full scan, no table lock)
|
||||
@@ -231,5 +231,5 @@
|
||||
COMMIT;
|
||||
SELECT ($e - $s) < 10000 AS covering;
|
||||
covering
|
||||
-0
|
||||
+1
|
||||
DROP TABLE t2;
|
||||
|
||||
@@ -174,3 +174,81 @@ insert into t11 select a from t10 where a > 30;
|
||||
# lock_rec_created
|
||||
11
|
||||
drop table t10, t11;
|
||||
#
|
||||
# MDEV-41351 Redundant clustered lookups in covering scans when holding a table X-lock
|
||||
#
|
||||
# Covering full scan
|
||||
CREATE TABLE t1 (id INT PRIMARY KEY, sec INT, pad CHAR(100), KEY i1(sec))
|
||||
ENGINE=InnoDB;
|
||||
INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_10000;
|
||||
SELECT sec,id FROM t1 FOR UPDATE;
|
||||
# 10000 rows; clustered path costs ~2/row
|
||||
SELECT ($e - $s) < 10000 AS covering;
|
||||
covering
|
||||
0
|
||||
connect con1,localhost,root,,;
|
||||
SET innodb_lock_wait_timeout=1;
|
||||
# Case 1: covering point lookup (never a full scan, no table lock)
|
||||
connection default;
|
||||
EXPLAIN SELECT sec,id FROM t1 WHERE sec=5 FOR UPDATE;
|
||||
id select_type table type possible_keys key key_len ref rows Extra
|
||||
1 SIMPLE t1 ref i1 i1 5 const # Using index
|
||||
BEGIN;
|
||||
SELECT sec,id FROM t1 WHERE sec=5 FOR UPDATE;
|
||||
sec id
|
||||
5 5
|
||||
connection con1;
|
||||
# PK path, non-indexed column: must wait for A's clustered X lock
|
||||
UPDATE t1 SET pad='y' WHERE id=5;
|
||||
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
|
||||
connection default;
|
||||
COMMIT;
|
||||
# Case 2: covering full scan
|
||||
connection default;
|
||||
BEGIN;
|
||||
SELECT sec,id FROM t1 FOR UPDATE;
|
||||
connection con1;
|
||||
UPDATE t1 SET pad='y' WHERE id=5;
|
||||
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
|
||||
connection default;
|
||||
COMMIT;
|
||||
# locks released: the UPDATE now succeeds
|
||||
connection con1;
|
||||
UPDATE t1 SET pad='y' WHERE id=5;
|
||||
SELECT pad FROM t1 WHERE id=5;
|
||||
pad
|
||||
y
|
||||
disconnect con1;
|
||||
connection default;
|
||||
DROP TABLE t1;
|
||||
# Prefix case, row_search_with_covering_prefix coverage
|
||||
CREATE TABLE t2 (id INT PRIMARY KEY, a INT NOT NULL, v VARCHAR(50),
|
||||
KEY k(a, v(5))) ENGINE=InnoDB;
|
||||
INSERT INTO t2 SELECT seq, seq, 'x' FROM seq_1_to_10000;
|
||||
# LOCK_X, no read view
|
||||
BEGIN;
|
||||
SELECT id, a, v FROM t2 FORCE INDEX(k) ORDER BY a FOR UPDATE;
|
||||
COMMIT;
|
||||
SELECT ($e - $s) < 10000 AS covering;
|
||||
covering
|
||||
0
|
||||
DROP TABLE t2;
|
||||
#
|
||||
# A covering locking full scan must still visit the clustered index
|
||||
# when a snapshot isolation read view is open, so that a row changed
|
||||
# after the snapshot is reported as ER_CHECKREAD.
|
||||
#
|
||||
SET innodb_snapshot_isolation= ON;
|
||||
CREATE TABLE t1 (a INT PRIMARY KEY, b INT, KEY(b)) ENGINE=InnoDB STATS_PERSISTENT=0;
|
||||
START TRANSACTION WITH CONSISTENT SNAPSHOT;
|
||||
connect con1,localhost,root,,;
|
||||
INSERT INTO t1 VALUES (1, 1);
|
||||
disconnect con1;
|
||||
connection default;
|
||||
EXPLAIN SELECT a, b FROM t1 FORCE INDEX (b) FOR UPDATE;
|
||||
id select_type table type possible_keys key key_len ref rows Extra
|
||||
1 SIMPLE t1 index NULL b 5 NULL # Using index
|
||||
SELECT a, b FROM t1 FORCE INDEX (b) FOR UPDATE;
|
||||
ERROR HY000: Record has changed since last read in table 't1'; try restarting transaction
|
||||
COMMIT;
|
||||
DROP TABLE t1;
|
||||
|
||||
@@ -289,3 +289,100 @@ let $result= `SELECT $end_val - $start_val`;
|
||||
echo # lock_rec_created;
|
||||
echo $result;
|
||||
drop table t10, t11;
|
||||
|
||||
--echo #
|
||||
--echo # MDEV-41351 Redundant clustered lookups in covering scans when holding a table X-lock
|
||||
--echo #
|
||||
|
||||
--echo # Covering full scan
|
||||
CREATE TABLE t1 (id INT PRIMARY KEY, sec INT, pad CHAR(100), KEY i1(sec))
|
||||
ENGINE=InnoDB;
|
||||
INSERT INTO t1 SELECT seq, seq, seq FROM seq_1_to_10000;
|
||||
|
||||
let $bp= SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
|
||||
WHERE VARIABLE_NAME='INNODB_BUFFER_POOL_READ_REQUESTS';
|
||||
let $s= `$bp`;
|
||||
--disable_result_log
|
||||
SELECT sec,id FROM t1 FOR UPDATE;
|
||||
--enable_result_log
|
||||
let $e= `$bp`;
|
||||
--echo # 10000 rows; clustered path costs ~2/row
|
||||
evalp SELECT ($e - $s) < 10000 AS covering;
|
||||
|
||||
--connect (con1,localhost,root,,)
|
||||
SET innodb_lock_wait_timeout=1;
|
||||
|
||||
--echo # Case 1: covering point lookup (never a full scan, no table lock)
|
||||
--connection default
|
||||
--replace_column 9 #
|
||||
EXPLAIN SELECT sec,id FROM t1 WHERE sec=5 FOR UPDATE;
|
||||
BEGIN;
|
||||
SELECT sec,id FROM t1 WHERE sec=5 FOR UPDATE;
|
||||
--connection con1
|
||||
--echo # PK path, non-indexed column: must wait for A's clustered X lock
|
||||
--error ER_LOCK_WAIT_TIMEOUT
|
||||
UPDATE t1 SET pad='y' WHERE id=5;
|
||||
--connection default
|
||||
COMMIT;
|
||||
|
||||
--echo # Case 2: covering full scan
|
||||
--connection default
|
||||
BEGIN;
|
||||
--disable_result_log
|
||||
SELECT sec,id FROM t1 FOR UPDATE;
|
||||
--enable_result_log
|
||||
--connection con1
|
||||
--error ER_LOCK_WAIT_TIMEOUT
|
||||
UPDATE t1 SET pad='y' WHERE id=5;
|
||||
--connection default
|
||||
COMMIT;
|
||||
|
||||
--echo # locks released: the UPDATE now succeeds
|
||||
--connection con1
|
||||
UPDATE t1 SET pad='y' WHERE id=5;
|
||||
SELECT pad FROM t1 WHERE id=5;
|
||||
--disconnect con1
|
||||
--connection default
|
||||
DROP TABLE t1;
|
||||
|
||||
--echo # Prefix case, row_search_with_covering_prefix coverage
|
||||
CREATE TABLE t2 (id INT PRIMARY KEY, a INT NOT NULL, v VARCHAR(50),
|
||||
KEY k(a, v(5))) ENGINE=InnoDB;
|
||||
INSERT INTO t2 SELECT seq, seq, 'x' FROM seq_1_to_10000;
|
||||
|
||||
--echo # LOCK_X, no read view
|
||||
let $s= `$bp`;
|
||||
BEGIN;
|
||||
--disable_result_log
|
||||
SELECT id, a, v FROM t2 FORCE INDEX(k) ORDER BY a FOR UPDATE;
|
||||
--enable_result_log
|
||||
COMMIT;
|
||||
let $e= `$bp`;
|
||||
evalp SELECT ($e - $s) < 10000 AS covering;
|
||||
DROP TABLE t2;
|
||||
|
||||
--echo #
|
||||
--echo # A covering locking full scan must still visit the clustered index
|
||||
--echo # when a snapshot isolation read view is open, so that a row changed
|
||||
--echo # after the snapshot is reported as ER_CHECKREAD.
|
||||
--echo #
|
||||
SET innodb_snapshot_isolation= ON;
|
||||
|
||||
CREATE TABLE t1 (a INT PRIMARY KEY, b INT, KEY(b)) ENGINE=InnoDB STATS_PERSISTENT=0;
|
||||
|
||||
START TRANSACTION WITH CONSISTENT SNAPSHOT;
|
||||
|
||||
--connect (con1,localhost,root,,)
|
||||
INSERT INTO t1 VALUES (1, 1);
|
||||
--disconnect con1
|
||||
|
||||
--connection default
|
||||
--replace_column 9 #
|
||||
EXPLAIN SELECT a, b FROM t1 FORCE INDEX (b) FOR UPDATE;
|
||||
--disable_ps2_protocol
|
||||
--error ER_CHECKREAD
|
||||
SELECT a, b FROM t1 FORCE INDEX (b) FOR UPDATE;
|
||||
--enable_ps2_protocol
|
||||
COMMIT;
|
||||
|
||||
DROP TABLE t1;
|
||||
|
||||
@@ -6404,7 +6404,7 @@ ha_innobase::clone(
|
||||
DBUG_ASSERT(new_handler->m_prebuilt != NULL);
|
||||
|
||||
new_handler->m_prebuilt->select_lock_type
|
||||
= m_prebuilt->select_lock_type;
|
||||
= m_prebuilt->real_lock_mode();
|
||||
}
|
||||
|
||||
DBUG_RETURN(new_handler);
|
||||
@@ -7394,7 +7394,8 @@ ha_innobase::build_template(
|
||||
/* We always retrieve the whole clustered index record if we
|
||||
use exclusive row level locks, for example, if the read is
|
||||
done in an UPDATE statement or if we are using a no rollback
|
||||
table */
|
||||
table. LOCK_X_BY_TABLE (see extra_opt()) is
|
||||
deliberately not matched here */
|
||||
|
||||
whole_row = true;
|
||||
} else if (!whole_row) {
|
||||
@@ -16454,7 +16455,30 @@ int ha_innobase::extra_opt(enum ha_extra_function operation, ulong arg)
|
||||
case HA_EXTRA_FULL_SCAN:
|
||||
if (THDVAR(ha_thd(), table_lock_on_full_scan) &&
|
||||
!m_prebuilt->skip_locked && arg == ULONG_MAX)
|
||||
{
|
||||
m_prebuilt->full_table_scan = true;
|
||||
/* Allow a covering index scan to be considered for a LOCK_X
|
||||
read in a SELECT statement. Otherwise a LOCK_X read always
|
||||
reads the whole clustered index record (see
|
||||
ha_innobase::build_template()): UPDATE and DELETE need it to
|
||||
write the row, and a locking read through a secondary index
|
||||
must lock the clustered index record to lock the row against
|
||||
access through any other index.
|
||||
|
||||
With innodb_snapshot_isolation and a read view already open
|
||||
(e.g. inside a START TRANSACTION WITH CONSISTENT SNAPSHOT, or
|
||||
an earlier consistent read in a REPEATABLE READ transaction),
|
||||
it needs to check if there is a committed change the
|
||||
transaction never saw. To that end it needs the record's
|
||||
trx_id which is only available from clustered records, see
|
||||
lock_clust_rec_read_check_and_lock. In such a case we need to
|
||||
disable the optimization. */
|
||||
if (m_prebuilt->select_lock_type == LOCK_X &&
|
||||
thd_sql_command(ha_thd()) == SQLCOM_SELECT &&
|
||||
!(m_prebuilt->trx->snapshot_isolation &&
|
||||
m_prebuilt->trx->read_view.is_open()))
|
||||
m_prebuilt->select_lock_type= LOCK_X_BY_TABLE;
|
||||
}
|
||||
return 0;
|
||||
default:/* Do nothing */
|
||||
;
|
||||
@@ -16483,6 +16507,7 @@ ha_innobase::reset()
|
||||
|
||||
m_prebuilt->skip_locked = false;
|
||||
m_prebuilt->full_table_scan = false;
|
||||
m_prebuilt->select_lock_type = m_prebuilt->real_lock_mode();
|
||||
return(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,12 @@ enum lock_mode {
|
||||
in an exclusive mode */
|
||||
LOCK_NONE, /* this is used elsewhere to note consistent read */
|
||||
LOCK_NUM = LOCK_NONE, /* number of lock modes */
|
||||
LOCK_NONE_UNSET = 7
|
||||
LOCK_NONE_UNSET = 7,
|
||||
/* Only in row_prebuilt_t::select_lock_type: a LOCK_X read whose
|
||||
record locks are made redundant by a table-level LOCK_X
|
||||
(innodb_table_lock_on_full_scan), so a covering index may be used.
|
||||
Masking with LOCK_MODE_MASK yields LOCK_X. */
|
||||
LOCK_X_BY_TABLE = LOCK_X | 8
|
||||
};
|
||||
|
||||
/** A table lock */
|
||||
|
||||
@@ -576,7 +576,28 @@ struct row_prebuilt_t {
|
||||
updated */
|
||||
dtuple_t* clust_ref; /*!< prebuilt dtuple used in
|
||||
sel/upd/del */
|
||||
lock_mode select_lock_type;/*!< LOCK_NONE, LOCK_S, or LOCK_X */
|
||||
lock_mode select_lock_type;/*!< LOCK_NONE, LOCK_S, LOCK_X, or
|
||||
LOCK_X_BY_TABLE */
|
||||
|
||||
/** @return the lock mode to request for records and tables:
|
||||
select_lock_type, with LOCK_X_BY_TABLE mapped to LOCK_X */
|
||||
lock_mode real_lock_mode() const
|
||||
{
|
||||
return static_cast<lock_mode>(select_lock_type & LOCK_MODE_MASK);
|
||||
}
|
||||
|
||||
/** @return the table lock to request for a locking read: LOCK_IS or
|
||||
LOCK_IX, or the stronger LOCK_S or LOCK_X when full_table_scan makes
|
||||
a single table lock replace the record locks.
|
||||
select_lock_type must be LOCK_S, LOCK_X or LOCK_X_BY_TABLE. */
|
||||
lock_mode table_lock_mode() const
|
||||
{
|
||||
ut_ad(select_lock_type == LOCK_S || select_lock_type == LOCK_X
|
||||
|| select_lock_type == LOCK_X_BY_TABLE);
|
||||
return static_cast<lock_mode>(
|
||||
select_lock_type & (1U | unsigned{full_table_scan} << 1));
|
||||
}
|
||||
|
||||
bool skip_locked; /*!< TL_{READ,WRITE}_SKIP_LOCKED */
|
||||
lock_mode stored_select_lock_type;/*!< this field is used to
|
||||
remember the original select_lock_type
|
||||
|
||||
@@ -1194,8 +1194,7 @@ row_lock_table(row_prebuilt_t* prebuilt)
|
||||
|
||||
trx_start_if_not_started_xa(trx, false);
|
||||
|
||||
err = lock_table(prebuilt->table, NULL, static_cast<lock_mode>(
|
||||
prebuilt->select_lock_type), thr);
|
||||
err = lock_table(prebuilt->table, NULL, prebuilt->real_lock_mode(), thr);
|
||||
trx->error_state = err;
|
||||
} while (err != DB_SUCCESS
|
||||
&& row_mysql_handle_errors(&err, trx, thr, nullptr));
|
||||
@@ -1833,8 +1832,7 @@ row_unlock_for_mysql(
|
||||
trx,
|
||||
*btr_pcur_get_block(pcur),
|
||||
rec,
|
||||
static_cast<enum lock_mode>(
|
||||
prebuilt->select_lock_type));
|
||||
prebuilt->real_lock_mode());
|
||||
}
|
||||
no_unlock:
|
||||
mtr_commit(&mtr);
|
||||
|
||||
@@ -1352,6 +1352,8 @@ sel_set_rec_lock(
|
||||
return DB_LOCK_TABLE_FULL;
|
||||
}
|
||||
|
||||
/* Change LOCK_X_BY_TABLE to LOCK_X */
|
||||
mode &= LOCK_MODE_MASK;
|
||||
if (dict_index_is_clust(index)) {
|
||||
err = lock_clust_rec_read_check_and_lock(
|
||||
0, block, rec, index, offsets,
|
||||
@@ -3387,7 +3389,7 @@ Row_sel_get_clust_rec_for_mysql::operator()(
|
||||
/* Extract values from prebuilt */
|
||||
dtuple_t* clust_ref= prebuilt->clust_ref;
|
||||
btr_pcur_t* clust_pcur= prebuilt->clust_pcur;
|
||||
lock_mode select_lock_type= prebuilt->select_lock_type;
|
||||
lock_mode select_lock_type= prebuilt->real_lock_mode();
|
||||
trx_t* trx= prebuilt->trx;
|
||||
|
||||
dict_index_t* clust_index;
|
||||
@@ -4265,7 +4267,8 @@ bool row_search_with_covering_prefix(
|
||||
|
||||
/* In ha_innobase::build_template() we choose to access the
|
||||
whole row when using exclusive row locks or In case of fts
|
||||
query, we need to read from clustered index */
|
||||
query, we need to read from clustered index. LOCK_X_BY_TABLE (see
|
||||
extra_opt()) is deliberately not matched here */
|
||||
if (prebuilt->select_lock_type == LOCK_X || prebuilt->in_fts_query
|
||||
|| !index->is_btree()) {
|
||||
return false;
|
||||
@@ -4821,10 +4824,7 @@ aborted:
|
||||
} else {
|
||||
wait_table_again:
|
||||
err = lock_table(prebuilt->table, nullptr,
|
||||
prebuilt->full_table_scan ?
|
||||
prebuilt->select_lock_type :
|
||||
(prebuilt->select_lock_type == LOCK_S
|
||||
? LOCK_IS : LOCK_IX), thr);
|
||||
prebuilt->table_lock_mode(), thr);
|
||||
|
||||
if (err != DB_SUCCESS) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user