Skip to content

Commit

Permalink
MDEV-24818: Optimize multi-statement INSERT into an empty table
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dr-m committed Mar 16, 2021
1 parent 92b2a91 commit 8ea923f
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 124 deletions.
51 changes: 51 additions & 0 deletions mysql-test/suite/innodb/r/insert_into_empty.result
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,54 @@ DROP TEMPORARY TABLE t,t2;
ERROR 25006: Cannot execute statement in a READ ONLY transaction
SET tx_read_only=0;
DROP TEMPORARY TABLE t,t2;
#
# MDEV-24818 Optimize multiple INSERT into empty table
#
CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
INSERT INTO t1 VALUES (4),(5),(6);
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
COMMIT;
SELECT * FROM t1;
f1
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
SAVEPOINT a;
INSERT INTO t1 VALUES (4),(5),(6);
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
ROLLBACK TO SAVEPOINT a;
COMMIT;
SELECT * FROM t1;
f1
5
6
7
DROP TABLE t1;
SET foreign_key_checks=1;
CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
INSERT INTO t1 VALUES (4),(5),(6);
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
COMMIT;
SELECT * FROM t1;
f1
5
6
7
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
SAVEPOINT a;
INSERT INTO t1 VALUES (4),(5),(6);
ERROR 23000: Duplicate entry '5' for key 'PRIMARY'
ROLLBACK TO SAVEPOINT a;
COMMIT;
SELECT * FROM t1;
f1
5
6
7
DROP TABLE t1;
SET foreign_key_checks=0;
44 changes: 44 additions & 0 deletions mysql-test/suite/innodb/t/insert_into_empty.test
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,47 @@ INSERT INTO t VALUES(0);
DROP TEMPORARY TABLE t,t2;
SET tx_read_only=0;
DROP TEMPORARY TABLE t,t2;

--echo #
--echo # MDEV-24818 Optimize multiple INSERT into empty table
--echo #

CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
--error ER_DUP_ENTRY
INSERT INTO t1 VALUES (4),(5),(6);
COMMIT;
SELECT * FROM t1;
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
SAVEPOINT a;
--error ER_DUP_ENTRY
INSERT INTO t1 VALUES (4),(5),(6);
ROLLBACK TO SAVEPOINT a;
COMMIT;
SELECT * FROM t1;
DROP TABLE t1;

# Repeat the same with the MDEV-515 test disabled
SET foreign_key_checks=1;

CREATE TABLE t1(f1 INT PRIMARY KEY) ENGINE=InnoDB;
BEGIN;
INSERT INTO t1 VALUES (5),(6),(7);
--error ER_DUP_ENTRY
INSERT INTO t1 VALUES (4),(5),(6);
COMMIT;
SELECT * FROM t1;
BEGIN;
--error ER_DUP_ENTRY
INSERT INTO t1 VALUES (5),(6),(7);
SAVEPOINT a;
--error ER_DUP_ENTRY
INSERT INTO t1 VALUES (4),(5),(6);
ROLLBACK TO SAVEPOINT a;
COMMIT;
SELECT * FROM t1;
DROP TABLE t1;

SET foreign_key_checks=0;
7 changes: 3 additions & 4 deletions storage/innobase/btr/btr0cur.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3547,12 +3547,11 @@ btr_cur_optimistic_insert(
DATA_TRX_ID_LEN));
} else {
ut_ad(thr->graph->trx->id);
ut_ad(thr->graph->trx->id
ut_ad(thr->graph->trx->bulk_insert
|| thr->graph->trx->id
== trx_read_trx_id(
static_cast<const byte*>(
trx_id->data))
|| static_cast<ins_node_t*>(
thr->run_node)->bulk_insert);
trx_id->data)));
}
}
#endif
Expand Down
86 changes: 62 additions & 24 deletions storage/innobase/handler/ha_innodb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3062,9 +3062,6 @@ ha_innobase::reset_template(void)
m_prebuilt->pk_filter = NULL;
m_prebuilt->template_type = ROW_MYSQL_NO_TEMPLATE;
}
if (ins_node_t* node = m_prebuilt->ins_node) {
node->bulk_insert = false;
}
}

/*****************************************************************//**
Expand Down Expand Up @@ -3119,6 +3116,7 @@ ha_innobase::init_table_handle_for_HANDLER(void)
m_prebuilt->used_in_HANDLER = TRUE;

reset_template();
m_prebuilt->trx->bulk_insert = false;
}

#ifdef WITH_INNODB_DISALLOW_WRITES
Expand Down Expand Up @@ -15091,9 +15089,7 @@ ha_innobase::extra(
shared lock instead of an exclusive lock. */
stmt_boundary:
trx->end_bulk_insert(*m_prebuilt->table);
if (ins_node_t* node = m_prebuilt->ins_node) {
node->bulk_insert = false;
}
trx->bulk_insert = false;
break;
case HA_EXTRA_NO_KEYREAD:
m_prebuilt->read_just_key = 0;
Expand All @@ -15109,12 +15105,22 @@ ha_innobase::extra(
goto stmt_boundary;
case HA_EXTRA_NO_IGNORE_DUP_KEY:
trx->duplicates &= ~TRX_DUP_IGNORE;
if (trx->is_bulk_insert()) {
/* Allow a subsequent INSERT into an empty table
if !unique_checks && !foreign_key_checks. */
break;
}
goto stmt_boundary;
case HA_EXTRA_WRITE_CAN_REPLACE:
trx->duplicates |= TRX_DUP_REPLACE;
goto stmt_boundary;
case HA_EXTRA_WRITE_CANNOT_REPLACE:
trx->duplicates &= ~TRX_DUP_REPLACE;
if (trx->is_bulk_insert()) {
/* Allow a subsequent INSERT into an empty table
if !unique_checks && !foreign_key_checks. */
break;
}
goto stmt_boundary;
case HA_EXTRA_BEGIN_ALTER_COPY:
m_prebuilt->table->skip_alter_undo = 1;
Expand Down Expand Up @@ -15192,30 +15198,44 @@ ha_innobase::start_stmt(
/* Reset the AUTOINC statement level counter for multi-row INSERTs. */
trx->n_autoinc_rows = 0;

m_prebuilt->sql_stat_start = TRUE;
const auto sql_command = thd_sql_command(thd);

m_prebuilt->hint_need_to_fetch_extra_cols = 0;
reset_template();
trx->end_bulk_insert(*m_prebuilt->table);

switch (sql_command) {
case SQLCOM_INSERT:
case SQLCOM_INSERT_SELECT:
if (trx->is_bulk_insert()) {
/* Allow a subsequent INSERT into an empty table
if !unique_checks && !foreign_key_checks. */
break;
}
/* fall through */
default:
trx->end_bulk_insert(*m_prebuilt->table);
m_prebuilt->sql_stat_start = TRUE;
if (!trx->bulk_insert) {
break;
}
trx->bulk_insert = false;
trx->last_sql_stat_start.least_undo_no = trx->undo_no;
}

if (m_prebuilt->table->is_temporary()
&& m_mysql_has_locked
&& m_prebuilt->select_lock_type == LOCK_NONE) {
dberr_t error;

switch (thd_sql_command(thd)) {
switch (sql_command) {
case SQLCOM_INSERT:
case SQLCOM_UPDATE:
case SQLCOM_DELETE:
case SQLCOM_REPLACE:
init_table_handle_for_HANDLER();
m_prebuilt->select_lock_type = LOCK_X;
m_prebuilt->stored_select_lock_type = LOCK_X;
error = row_lock_table(m_prebuilt);

if (error != DB_SUCCESS) {
int st = convert_error_code_to_mysql(
error, 0, thd);
DBUG_RETURN(st);
if (dberr_t error = row_lock_table(m_prebuilt)) {
DBUG_RETURN(convert_error_code_to_mysql(
error, 0, thd));
}
break;
}
Expand All @@ -15229,9 +15249,9 @@ ha_innobase::start_stmt(

m_prebuilt->select_lock_type = LOCK_X;

} else if (trx->isolation_level != TRX_ISO_SERIALIZABLE
&& thd_sql_command(thd) == SQLCOM_SELECT
&& lock_type == TL_READ) {
} else if (sql_command == SQLCOM_SELECT
&& lock_type == TL_READ
&& trx->isolation_level != TRX_ISO_SERIALIZABLE) {

/* For other than temporary tables, we obtain
no lock for consistent read (plain SELECT). */
Expand Down Expand Up @@ -15339,9 +15359,11 @@ ha_innobase::external_lock(
}
}

const auto sql_command = thd_sql_command(thd);

/* Check for UPDATEs in read-only mode. */
if (srv_read_only_mode) {
switch (thd_sql_command(thd)) {
switch (sql_command) {
case SQLCOM_CREATE_TABLE:
if (lock_type != F_WRLCK) {
break;
Expand All @@ -15368,13 +15390,29 @@ ha_innobase::external_lock(
m_prebuilt->hint_need_to_fetch_extra_cols = 0;

reset_template();
trx->end_bulk_insert(*m_prebuilt->table);
switch (sql_command) {
case SQLCOM_INSERT:
case SQLCOM_INSERT_SELECT:
if (trx->is_bulk_insert()) {
/* Allow a subsequent INSERT into an empty table
if !unique_checks && !foreign_key_checks. */
break;
}
/* fall through */
default:
trx->end_bulk_insert(*m_prebuilt->table);
if (!trx->bulk_insert) {
break;
}
trx->bulk_insert = false;
trx->last_sql_stat_start.least_undo_no = trx->undo_no;
}

switch (m_prebuilt->table->quiesce) {
case QUIESCE_START:
/* Check for FLUSH TABLE t WITH READ LOCK; */
if (!srv_read_only_mode
&& thd_sql_command(thd) == SQLCOM_FLUSH
&& sql_command == SQLCOM_FLUSH
&& lock_type == F_RDLCK) {

if (!m_prebuilt->table->space) {
Expand Down Expand Up @@ -15458,7 +15496,7 @@ ha_innobase::external_lock(

if (m_prebuilt->select_lock_type != LOCK_NONE) {

if (thd_sql_command(thd) == SQLCOM_LOCK_TABLES
if (sql_command == SQLCOM_LOCK_TABLES
&& THDVAR(thd, table_locks)
&& thd_test_options(thd, OPTION_NOT_AUTOCOMMIT)
&& thd_in_lock_tables(thd)) {
Expand Down
6 changes: 0 additions & 6 deletions storage/innobase/include/lock0lock.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,6 @@ lock_table(
@param mode LOCK_X or LOCK_IX */
void lock_table_resurrect(dict_table_t *table, trx_t *trx, lock_mode mode);

/** Release a table X lock after rolling back an insert into an empty table
(which was covered by a TRX_UNDO_EMPTY record).
@param table table to be X-unlocked
@param trx transaction */
void lock_table_x_unlock(dict_table_t *table, trx_t *trx);

/** Sets a lock on a table based on the given mode.
@param[in] table table to lock
@param[in,out] trx transaction
Expand Down
3 changes: 0 additions & 3 deletions storage/innobase/include/row0ins.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,6 @@ struct ins_node_t
and buffers for sys fields in row allocated */
void vers_update_end(row_prebuilt_t *prebuilt, bool history_row);
bool vers_history_row() const; /* true if 'row' is historical */

/** Bulk insert enabled for this table */
bool bulk_insert= false;
};

/** Create an insert object.
Expand Down
10 changes: 1 addition & 9 deletions storage/innobase/include/trx0roll.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*****************************************************************************
Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
Copyright (c) 2015, 2020, MariaDB Corporation.
Copyright (c) 2015, 2021, MariaDB Corporation.
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Expand Down Expand Up @@ -34,14 +34,6 @@ Created 3/26/1996 Heikki Tuuri
extern bool trx_rollback_is_active;
extern const trx_t* trx_roll_crash_recv_trx;

/*******************************************************************//**
Returns a transaction savepoint taken at this point in time.
@return savepoint */
trx_savept_t
trx_savept_take(
/*============*/
trx_t* trx); /*!< in: transaction */

/** Report progress when rolling back a row of a recovered transaction. */
void trx_roll_report_progress();
/*******************************************************************//**
Expand Down
23 changes: 15 additions & 8 deletions storage/innobase/include/trx0trx.h
Original file line number Diff line number Diff line change
Expand Up @@ -491,12 +491,8 @@ class trx_mod_table_time_t
covered by a TRX_UNDO_EMPTY record (for the first statement to
insert into an empty table) */
static constexpr undo_no_t BULK= 1ULL << 63;
/** Flag in 'first' to indicate that some operations were
covered by a TRX_UNDO_EMPTY record (for the first statement to
insert into an empty table) */
static constexpr undo_no_t WAS_BULK= 1ULL << 62;

/** First modification of the table, possibly ORed with BULK or WAS_BULK */
/** First modification of the table, possibly ORed with BULK */
undo_no_t first;
/** First modification of a system versioned column (or NONE) */
undo_no_t first_versioned= NONE;
Expand Down Expand Up @@ -525,15 +521,13 @@ class trx_mod_table_time_t
}

/** Notify the start of a bulk insert operation */
void start_bulk_insert() { first|= BULK | WAS_BULK; }
void start_bulk_insert() { first|= BULK; }

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

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

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

/** @return whether a bulk insert into empty table is in progress */
bool is_bulk_insert() const
{
if (!bulk_insert || check_unique_secondary || check_foreigns)
return false;
for (const auto& t : mod_tables)
if (t.second.is_bulk_insert())
return true;
return false;
}

private:
/** Assign a rollback segment for modifying temporary tables.
@return the assigned rollback segment */
Expand Down
Loading

0 comments on commit 8ea923f

Please sign in to comment.