Skip to content

Commit 8ea923f

Browse files
committed
MDEV-24818: Optimize multi-statement INSERT into an empty table
If the user "opts in" (as in the parent commit 92b2a91), we can optimize multiple INSERT statements to use table-level locking and undo logging. There will be a change of behavior: CREATE TABLE t(a PRIMARY KEY) ENGINE=InnoDB; SET foreign_key_checks=0, unique_checks=0; BEGIN; INSERT INTO t SET a=1; INSERT INTO t SET a=1; COMMIT; will end up with an empty table, because in case of an error, the entire transaction will be rolled back, instead of rolling back the failing statement. Previously, the second INSERT statement would have been logged row by row, and only that second statement would have been rolled back, leaving the first INSERT intact. lock_table_x_unlock(), trx_mod_table_time_t::WAS_BULK: Remove. Because we cannot really support statement rollback in this optimized mode, we will not optimize the locking. The exclusive table lock will be held until the end of the transaction.
1 parent 92b2a91 commit 8ea923f

File tree

14 files changed

+209
-124
lines changed

14 files changed

+209
-124
lines changed

mysql-test/suite/innodb/r/insert_into_empty.result

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,54 @@ DROP TEMPORARY TABLE t,t2;
5151
ERROR 25006: Cannot execute statement in a READ ONLY transaction
5252
SET tx_read_only=0;
5353
DROP TEMPORARY TABLE t,t2;
54+
#
55+
# MDEV-24818 Optimize multiple INSERT into empty table
56+
#
57+
CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
58+
BEGIN;
59+
INSERT INTO t1 VALUES (5),(6),(7);
60+
INSERT INTO t1 VALUES (4),(5),(6);
61+
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
62+
COMMIT;
63+
SELECT * FROM t1;
64+
f1
65+
BEGIN;
66+
INSERT INTO t1 VALUES (5),(6),(7);
67+
SAVEPOINT a;
68+
INSERT INTO t1 VALUES (4),(5),(6);
69+
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
70+
ROLLBACK TO SAVEPOINT a;
71+
COMMIT;
72+
SELECT * FROM t1;
73+
f1
74+
5
75+
6
76+
7
77+
DROP TABLE t1;
78+
SET foreign_key_checks=1;
79+
CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
80+
BEGIN;
81+
INSERT INTO t1 VALUES (5),(6),(7);
82+
INSERT INTO t1 VALUES (4),(5),(6);
83+
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
84+
COMMIT;
85+
SELECT * FROM t1;
86+
f1
87+
5
88+
6
89+
7
90+
BEGIN;
91+
INSERT INTO t1 VALUES (5),(6),(7);
92+
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
93+
SAVEPOINT a;
94+
INSERT INTO t1 VALUES (4),(5),(6);
95+
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
96+
ROLLBACK TO SAVEPOINT a;
97+
COMMIT;
98+
SELECT * FROM t1;
99+
f1
100+
5
101+
6
102+
7
103+
DROP TABLE t1;
104+
SET foreign_key_checks=0;

mysql-test/suite/innodb/t/insert_into_empty.test

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,47 @@ INSERT INTO t VALUES(0);
5555
DROP TEMPORARY TABLE t,t2;
5656
SET tx_read_only=0;
5757
DROP TEMPORARY TABLE t,t2;
58+
59+
--echo #
60+
--echo # MDEV-24818 Optimize multiple INSERT into empty table
61+
--echo #
62+
63+
CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
64+
BEGIN;
65+
INSERT INTO t1 VALUES (5),(6),(7);
66+
--error ER_DUP_ENTRY
67+
INSERT INTO t1 VALUES (4),(5),(6);
68+
COMMIT;
69+
SELECT * FROM t1;
70+
BEGIN;
71+
INSERT INTO t1 VALUES (5),(6),(7);
72+
SAVEPOINT a;
73+
--error ER_DUP_ENTRY
74+
INSERT INTO t1 VALUES (4),(5),(6);
75+
ROLLBACK TO SAVEPOINT a;
76+
COMMIT;
77+
SELECT * FROM t1;
78+
DROP TABLE t1;
79+
80+
# Repeat the same with the MDEV-515 test disabled
81+
SET foreign_key_checks=1;
82+
83+
CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
84+
BEGIN;
85+
INSERT INTO t1 VALUES (5),(6),(7);
86+
--error ER_DUP_ENTRY
87+
INSERT INTO t1 VALUES (4),(5),(6);
88+
COMMIT;
89+
SELECT * FROM t1;
90+
BEGIN;
91+
--error ER_DUP_ENTRY
92+
INSERT INTO t1 VALUES (5),(6),(7);
93+
SAVEPOINT a;
94+
--error ER_DUP_ENTRY
95+
INSERT INTO t1 VALUES (4),(5),(6);
96+
ROLLBACK TO SAVEPOINT a;
97+
COMMIT;
98+
SELECT * FROM t1;
99+
DROP TABLE t1;
100+
101+
SET foreign_key_checks=0;

storage/innobase/btr/btr0cur.cc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3547,12 +3547,11 @@ btr_cur_optimistic_insert(
35473547
DATA_TRX_ID_LEN));
35483548
} else {
35493549
ut_ad(thr->graph->trx->id);
3550-
ut_ad(thr->graph->trx->id
3550+
ut_ad(thr->graph->trx->bulk_insert
3551+
|| thr->graph->trx->id
35513552
== trx_read_trx_id(
35523553
static_cast<const byte*>(
3553-
trx_id->data))
3554-
|| static_cast<ins_node_t*>(
3555-
thr->run_node)->bulk_insert);
3554+
trx_id->data)));
35563555
}
35573556
}
35583557
#endif

storage/innobase/handler/ha_innodb.cc

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3062,9 +3062,6 @@ ha_innobase::reset_template(void)
30623062
m_prebuilt->pk_filter = NULL;
30633063
m_prebuilt->template_type = ROW_MYSQL_NO_TEMPLATE;
30643064
}
3065-
if (ins_node_t* node = m_prebuilt->ins_node) {
3066-
node->bulk_insert = false;
3067-
}
30683065
}
30693066

30703067
/*****************************************************************//**
@@ -3119,6 +3116,7 @@ ha_innobase::init_table_handle_for_HANDLER(void)
31193116
m_prebuilt->used_in_HANDLER = TRUE;
31203117

31213118
reset_template();
3119+
m_prebuilt->trx->bulk_insert = false;
31223120
}
31233121

31243122
#ifdef WITH_INNODB_DISALLOW_WRITES
@@ -15091,9 +15089,7 @@ ha_innobase::extra(
1509115089
shared lock instead of an exclusive lock. */
1509215090
stmt_boundary:
1509315091
trx->end_bulk_insert(*m_prebuilt->table);
15094-
if (ins_node_t* node = m_prebuilt->ins_node) {
15095-
node->bulk_insert = false;
15096-
}
15092+
trx->bulk_insert = false;
1509715093
break;
1509815094
case HA_EXTRA_NO_KEYREAD:
1509915095
m_prebuilt->read_just_key = 0;
@@ -15109,12 +15105,22 @@ ha_innobase::extra(
1510915105
goto stmt_boundary;
1511015106
case HA_EXTRA_NO_IGNORE_DUP_KEY:
1511115107
trx->duplicates &= ~TRX_DUP_IGNORE;
15108+
if (trx->is_bulk_insert()) {
15109+
/* Allow a subsequent INSERT into an empty table
15110+
if !unique_checks && !foreign_key_checks. */
15111+
break;
15112+
}
1511215113
goto stmt_boundary;
1511315114
case HA_EXTRA_WRITE_CAN_REPLACE:
1511415115
trx->duplicates |= TRX_DUP_REPLACE;
1511515116
goto stmt_boundary;
1511615117
case HA_EXTRA_WRITE_CANNOT_REPLACE:
1511715118
trx->duplicates &= ~TRX_DUP_REPLACE;
15119+
if (trx->is_bulk_insert()) {
15120+
/* Allow a subsequent INSERT into an empty table
15121+
if !unique_checks && !foreign_key_checks. */
15122+
break;
15123+
}
1511815124
goto stmt_boundary;
1511915125
case HA_EXTRA_BEGIN_ALTER_COPY:
1512015126
m_prebuilt->table->skip_alter_undo = 1;
@@ -15192,30 +15198,44 @@ ha_innobase::start_stmt(
1519215198
/* Reset the AUTOINC statement level counter for multi-row INSERTs. */
1519315199
trx->n_autoinc_rows = 0;
1519415200

15195-
m_prebuilt->sql_stat_start = TRUE;
15201+
const auto sql_command = thd_sql_command(thd);
15202+
1519615203
m_prebuilt->hint_need_to_fetch_extra_cols = 0;
1519715204
reset_template();
15198-
trx->end_bulk_insert(*m_prebuilt->table);
15205+
15206+
switch (sql_command) {
15207+
case SQLCOM_INSERT:
15208+
case SQLCOM_INSERT_SELECT:
15209+
if (trx->is_bulk_insert()) {
15210+
/* Allow a subsequent INSERT into an empty table
15211+
if !unique_checks && !foreign_key_checks. */
15212+
break;
15213+
}
15214+
/* fall through */
15215+
default:
15216+
trx->end_bulk_insert(*m_prebuilt->table);
15217+
m_prebuilt->sql_stat_start = TRUE;
15218+
if (!trx->bulk_insert) {
15219+
break;
15220+
}
15221+
trx->bulk_insert = false;
15222+
trx->last_sql_stat_start.least_undo_no = trx->undo_no;
15223+
}
1519915224

1520015225
if (m_prebuilt->table->is_temporary()
1520115226
&& m_mysql_has_locked
1520215227
&& m_prebuilt->select_lock_type == LOCK_NONE) {
15203-
dberr_t error;
15204-
15205-
switch (thd_sql_command(thd)) {
15228+
switch (sql_command) {
1520615229
case SQLCOM_INSERT:
1520715230
case SQLCOM_UPDATE:
1520815231
case SQLCOM_DELETE:
1520915232
case SQLCOM_REPLACE:
1521015233
init_table_handle_for_HANDLER();
1521115234
m_prebuilt->select_lock_type = LOCK_X;
1521215235
m_prebuilt->stored_select_lock_type = LOCK_X;
15213-
error = row_lock_table(m_prebuilt);
15214-
15215-
if (error != DB_SUCCESS) {
15216-
int st = convert_error_code_to_mysql(
15217-
error, 0, thd);
15218-
DBUG_RETURN(st);
15236+
if (dberr_t error = row_lock_table(m_prebuilt)) {
15237+
DBUG_RETURN(convert_error_code_to_mysql(
15238+
error, 0, thd));
1521915239
}
1522015240
break;
1522115241
}
@@ -15229,9 +15249,9 @@ ha_innobase::start_stmt(
1522915249

1523015250
m_prebuilt->select_lock_type = LOCK_X;
1523115251

15232-
} else if (trx->isolation_level != TRX_ISO_SERIALIZABLE
15233-
&& thd_sql_command(thd) == SQLCOM_SELECT
15234-
&& lock_type == TL_READ) {
15252+
} else if (sql_command == SQLCOM_SELECT
15253+
&& lock_type == TL_READ
15254+
&& trx->isolation_level != TRX_ISO_SERIALIZABLE) {
1523515255

1523615256
/* For other than temporary tables, we obtain
1523715257
no lock for consistent read (plain SELECT). */
@@ -15339,9 +15359,11 @@ ha_innobase::external_lock(
1533915359
}
1534015360
}
1534115361

15362+
const auto sql_command = thd_sql_command(thd);
15363+
1534215364
/* Check for UPDATEs in read-only mode. */
1534315365
if (srv_read_only_mode) {
15344-
switch (thd_sql_command(thd)) {
15366+
switch (sql_command) {
1534515367
case SQLCOM_CREATE_TABLE:
1534615368
if (lock_type != F_WRLCK) {
1534715369
break;
@@ -15368,13 +15390,29 @@ ha_innobase::external_lock(
1536815390
m_prebuilt->hint_need_to_fetch_extra_cols = 0;
1536915391

1537015392
reset_template();
15371-
trx->end_bulk_insert(*m_prebuilt->table);
15393+
switch (sql_command) {
15394+
case SQLCOM_INSERT:
15395+
case SQLCOM_INSERT_SELECT:
15396+
if (trx->is_bulk_insert()) {
15397+
/* Allow a subsequent INSERT into an empty table
15398+
if !unique_checks && !foreign_key_checks. */
15399+
break;
15400+
}
15401+
/* fall through */
15402+
default:
15403+
trx->end_bulk_insert(*m_prebuilt->table);
15404+
if (!trx->bulk_insert) {
15405+
break;
15406+
}
15407+
trx->bulk_insert = false;
15408+
trx->last_sql_stat_start.least_undo_no = trx->undo_no;
15409+
}
1537215410

1537315411
switch (m_prebuilt->table->quiesce) {
1537415412
case QUIESCE_START:
1537515413
/* Check for FLUSH TABLE t WITH READ LOCK; */
1537615414
if (!srv_read_only_mode
15377-
&& thd_sql_command(thd) == SQLCOM_FLUSH
15415+
&& sql_command == SQLCOM_FLUSH
1537815416
&& lock_type == F_RDLCK) {
1537915417

1538015418
if (!m_prebuilt->table->space) {
@@ -15458,7 +15496,7 @@ ha_innobase::external_lock(
1545815496

1545915497
if (m_prebuilt->select_lock_type != LOCK_NONE) {
1546015498

15461-
if (thd_sql_command(thd) == SQLCOM_LOCK_TABLES
15499+
if (sql_command == SQLCOM_LOCK_TABLES
1546215500
&& THDVAR(thd, table_locks)
1546315501
&& thd_test_options(thd, OPTION_NOT_AUTOCOMMIT)
1546415502
&& thd_in_lock_tables(thd)) {

storage/innobase/include/lock0lock.h

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,12 +379,6 @@ lock_table(
379379
@param mode LOCK_X or LOCK_IX */
380380
void lock_table_resurrect(dict_table_t *table, trx_t *trx, lock_mode mode);
381381

382-
/** Release a table X lock after rolling back an insert into an empty table
383-
(which was covered by a TRX_UNDO_EMPTY record).
384-
@param table table to be X-unlocked
385-
@param trx transaction */
386-
void lock_table_x_unlock(dict_table_t *table, trx_t *trx);
387-
388382
/** Sets a lock on a table based on the given mode.
389383
@param[in] table table to lock
390384
@param[in,out] trx transaction

storage/innobase/include/row0ins.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,6 @@ struct ins_node_t
207207
and buffers for sys fields in row allocated */
208208
void vers_update_end(row_prebuilt_t *prebuilt, bool history_row);
209209
bool vers_history_row() const; /* true if 'row' is historical */
210-
211-
/** Bulk insert enabled for this table */
212-
bool bulk_insert= false;
213210
};
214211

215212
/** Create an insert object.

storage/innobase/include/trx0roll.h

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*****************************************************************************
22
33
Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
4-
Copyright (c) 2015, 2020, MariaDB Corporation.
4+
Copyright (c) 2015, 2021, MariaDB Corporation.
55
66
This program is free software; you can redistribute it and/or modify it under
77
the terms of the GNU General Public License as published by the Free Software
@@ -34,14 +34,6 @@ Created 3/26/1996 Heikki Tuuri
3434
extern bool trx_rollback_is_active;
3535
extern const trx_t* trx_roll_crash_recv_trx;
3636

37-
/*******************************************************************//**
38-
Returns a transaction savepoint taken at this point in time.
39-
@return savepoint */
40-
trx_savept_t
41-
trx_savept_take(
42-
/*============*/
43-
trx_t* trx); /*!< in: transaction */
44-
4537
/** Report progress when rolling back a row of a recovered transaction. */
4638
void trx_roll_report_progress();
4739
/*******************************************************************//**

storage/innobase/include/trx0trx.h

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -491,12 +491,8 @@ class trx_mod_table_time_t
491491
covered by a TRX_UNDO_EMPTY record (for the first statement to
492492
insert into an empty table) */
493493
static constexpr undo_no_t BULK= 1ULL << 63;
494-
/** Flag in 'first' to indicate that some operations were
495-
covered by a TRX_UNDO_EMPTY record (for the first statement to
496-
insert into an empty table) */
497-
static constexpr undo_no_t WAS_BULK= 1ULL << 62;
498494

499-
/** First modification of the table, possibly ORed with BULK or WAS_BULK */
495+
/** First modification of the table, possibly ORed with BULK */
500496
undo_no_t first;
501497
/** First modification of a system versioned column (or NONE) */
502498
undo_no_t first_versioned= NONE;
@@ -525,15 +521,13 @@ class trx_mod_table_time_t
525521
}
526522

527523
/** Notify the start of a bulk insert operation */
528-
void start_bulk_insert() { first|= BULK | WAS_BULK; }
524+
void start_bulk_insert() { first|= BULK; }
529525

530526
/** Notify the end of a bulk insert operation */
531527
void end_bulk_insert() { first&= ~BULK; }
532528

533529
/** @return whether an insert is covered by TRX_UNDO_EMPTY record */
534530
bool is_bulk_insert() const { return first & BULK; }
535-
/** @return whether an insert was covered by TRX_UNDO_EMPTY record */
536-
bool was_bulk_insert() const { return first & WAS_BULK; }
537531

538532
/** Invoked after partial rollback
539533
@param limit number of surviving modified rows (trx_t::undo_no)
@@ -788,6 +782,8 @@ struct trx_t : ilist_node<> {
788782
wants to suppress foreign key checks,
789783
(in table imports, for example) we
790784
set this FALSE */
785+
/** whether an insert into an empty table is active */
786+
bool bulk_insert;
791787
/*------------------------------*/
792788
/* MySQL has a transaction coordinator to coordinate two phase
793789
commit between multiple storage engines and the binary log. When
@@ -1090,6 +1086,17 @@ struct trx_t : ilist_node<> {
10901086
t.second.end_bulk_insert();
10911087
}
10921088

1089+
/** @return whether a bulk insert into empty table is in progress */
1090+
bool is_bulk_insert() const
1091+
{
1092+
if (!bulk_insert || check_unique_secondary || check_foreigns)
1093+
return false;
1094+
for (const auto& t : mod_tables)
1095+
if (t.second.is_bulk_insert())
1096+
return true;
1097+
return false;
1098+
}
1099+
10931100
private:
10941101
/** Assign a rollback segment for modifying temporary tables.
10951102
@return the assigned rollback segment */

0 commit comments

Comments
 (0)