Browse Source

MDEV-35163 InnoDB persistent statistics fail to update after ALTER TABLE...ALGORITHM=COPY

Problem:
=======
- InnoDB statistics calculation for the table is done after
every 10 seconds by default in background thread dict_stats_thread()

- Doing multiple ALTER TABLE..ALGORITHM=COPY causes the
dict_stats_thread() to lag behind, therefore calculation of stats
for newly created intermediate table gets delayed

Fix:
====
- Stats calculation for newly created intermediate table is made
independent of background thread. After copying gets completed,
stats for new table is calculated as part of ALTER TABLE ... ALGORITHM=COPY.

dict_stats_rename_table(): Rename the table statistics from
intermediate table to new table

alter_stats_rebuild(): Removes the table name from the warning.
Because this warning can print for intermediate table as well.

Alter table using copy algorithm now calls alter_stats_rebuild()
under a shared MDL lock on a temporary #sql-alter- table,
differing from its previous use only during ALGORITHM=INPLACE
operations on user-visible tables.

dict_stats_schema_check(): Added a separate check for table
readability before checking for tablespace existence.
This could lead to detect of existence of persistent statistics
storage eariler and fallback to transient statistics.

This is a cherry-pick fix of mysql commit@cfe5f287ae99d004e8532a30003a7e8e77d379e3
pull/4088/head
Thirunarayanan Balathandayuthapani 2 weeks ago
parent
commit
687b18648c
  1. 105
      mysql-test/suite/innodb/r/alter_copy_stats.result
  2. 5
      mysql-test/suite/innodb/r/innodb-alter-debug.result
  3. 2
      mysql-test/suite/innodb/r/xap_release_locks_on_dict_stats_table.result
  4. 90
      mysql-test/suite/innodb/t/alter_copy_stats.test
  5. 2
      mysql-test/suite/innodb/t/innodb-alter-debug.test
  6. 2
      mysql-test/suite/innodb/t/xap_release_locks_on_dict_stats_table.test
  7. 3
      mysql-test/suite/innodb_fts/t/versioning.test
  8. 15
      storage/innobase/dict/dict0stats.cc
  9. 43
      storage/innobase/handler/ha_innodb.cc
  10. 7
      storage/innobase/handler/ha_innodb.h
  11. 43
      storage/innobase/handler/handler0alter.cc

105
mysql-test/suite/innodb/r/alter_copy_stats.result

@ -0,0 +1,105 @@
CREATE TABLE t1 (
c1 INT PRIMARY KEY,
c2 VARCHAR(50),
c3 VARCHAR(50),
c4 VARCHAR(50))ENGINE=InnoDB;
INSERT INTO t1 VALUES(1, 'AAA_2', 'AAA_3', 'AAA_4'),
(2, 'BBB_2', 'BBB_3', 'BBB_4'),
(3, 'CCC_2', 'CCC_3', 'CCC_4'),
(4, 'DDD_2', 'DDD_3', 'DDD_4'),
(5, 'EEE_2', 'EEE_3', 'EEE_4');
ALTER TABLE t1 CONVERT TO CHARACTER SET 'utf8mb4',ALGORITHM=COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 ADD COLUMN c5 VARCHAR(15), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 DROP COLUMN c5, ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 ADD COLUMN c5 VARCHAR(15), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 DROP COLUMN c5, ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 DROP PRIMARY KEY, ADD PRIMARY KEY (c4), ALGORITHM=COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 DROP PRIMARY KEY, ADD PRIMARY KEY (c1), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 ADD COLUMN c5 VARCHAR(15), ALGORITHM = INPLACE;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
ALTER TABLE t1 DROP COLUMN c5, ALGORITHM = INPLACE;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
n_rows database_name lower(table_name)
5 test t1
CREATE TABLE t2 (
c1 INT PRIMARY KEY,
c2 VARCHAR(50),
c3 VARCHAR(50),
c4 VARCHAR(50)
) ENGINE=InnoDB PARTITION BY RANGE (c1) (
PARTITION p1 VALUES LESS THAN (6),
PARTITION p2 VALUES LESS THAN (11),
PARTITION p3 VALUES LESS THAN (16),
PARTITION p4 VALUES LESS THAN (21)
);
INSERT INTO t2 VALUES(1, 'AAA_2', 'AAA_3', 'AAA_4'),
(2, 'BBB_2', 'BBB_3', 'BBB_4'),
(3, 'CCC_2', 'CCC_3', 'CCC_4'),
(4, 'DDD_2', 'DDD_3', 'DDD_4'),
(5, 'EEE_2', 'EEE_3', 'EEE_4'),
(6, 'FFF_2', 'DDD_3', 'DDD_4'),
(7, 'GGG_2', 'DDD_3', 'DDD_4'),
(8, 'HHH_2', 'DDD_3', 'DDD_4'),
(9, 'III_2', 'DDD_3', 'DDD_4'),
(10, 'JJJ_2', 'DDD_3', 'DDD_4'),
(13, 'KKK_2', 'DDD_3', 'DDD_4'),
(20, 'LLL_2', 'DDD_3', 'DDD_4');
ALTER TABLE t2 CONVERT TO CHARACTER SET 'utf8mb4',ALGORITHM=COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name LIKE '%t2%';
n_rows database_name lower(table_name)
5 test t2#p#p1
5 test t2#p#p2
1 test t2#p#p3
1 test t2#p#p4
ALTER TABLE t2 ADD COLUMN c5 VARCHAR(15), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name LIKE '%t2%';
n_rows database_name lower(table_name)
5 test t2#p#p1
5 test t2#p#p2
1 test t2#p#p3
1 test t2#p#p4
ALTER TABLE t2 DROP COLUMN c5, ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name LIKE '%t2%';
n_rows database_name lower(table_name)
5 test t2#p#p1
5 test t2#p#p2
1 test t2#p#p3
1 test t2#p#p4
# Test Cleanup.
DROP TABLE t1;
DROP TABLE t2;

5
mysql-test/suite/innodb/r/innodb-alter-debug.result

@ -117,8 +117,9 @@ ALTER TABLE t1 FORCE, ALGORITHM=COPY;
connection default;
SET DEBUG_SYNC='now WAIT_FOR blocked';
BEGIN;
SELECT * FROM mysql.innodb_table_stats FOR UPDATE;
database_name table_name last_update n_rows clustered_index_size sum_of_other_index_sizes
SELECT database_name, table_name FROM mysql.innodb_table_stats FOR UPDATE;
database_name table_name
test t1
SET DEBUG_SYNC='now SIGNAL go';
connection con1;
connection default;

2
mysql-test/suite/innodb/r/xap_release_locks_on_dict_stats_table.result

@ -6,7 +6,7 @@ SET @old_debug_dbug = @@global.debug_dbug;
XA START 'a';
INSERT INTO mysql.innodb_index_stats SELECT '','' AS table_name,index_name,LAST_UPDATE,stat_name,0 AS stat_value,sample_size,stat_description FROM mysql.innodb_index_stats WHERE table_name='dummy' FOR UPDATE;
SET GLOBAL debug_dbug = "+d,dict_stats_save_exit_notify";
INSERT INTO t VALUES (1);
INSERT INTO t VALUES (1), (2);
XA END 'a';
XA PREPARE 'a';
SET DEBUG_SYNC="now WAIT_FOR dict_stats_save_finished";

90
mysql-test/suite/innodb/t/alter_copy_stats.test

@ -0,0 +1,90 @@
--source include/have_innodb.inc
--source include/have_partition.inc
CREATE TABLE t1 (
c1 INT PRIMARY KEY,
c2 VARCHAR(50),
c3 VARCHAR(50),
c4 VARCHAR(50))ENGINE=InnoDB;
INSERT INTO t1 VALUES(1, 'AAA_2', 'AAA_3', 'AAA_4'),
(2, 'BBB_2', 'BBB_3', 'BBB_4'),
(3, 'CCC_2', 'CCC_3', 'CCC_4'),
(4, 'DDD_2', 'DDD_3', 'DDD_4'),
(5, 'EEE_2', 'EEE_3', 'EEE_4');
ALTER TABLE t1 CONVERT TO CHARACTER SET 'utf8mb4',ALGORITHM=COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 ADD COLUMN c5 VARCHAR(15), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 DROP COLUMN c5, ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 ADD COLUMN c5 VARCHAR(15), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 DROP COLUMN c5, ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 DROP PRIMARY KEY, ADD PRIMARY KEY (c4), ALGORITHM=COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 DROP PRIMARY KEY, ADD PRIMARY KEY (c1), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 ADD COLUMN c5 VARCHAR(15), ALGORITHM = INPLACE;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
ALTER TABLE t1 DROP COLUMN c5, ALGORITHM = INPLACE;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name IN ('t1');
CREATE TABLE t2 (
c1 INT PRIMARY KEY,
c2 VARCHAR(50),
c3 VARCHAR(50),
c4 VARCHAR(50)
) ENGINE=InnoDB PARTITION BY RANGE (c1) (
PARTITION p1 VALUES LESS THAN (6),
PARTITION p2 VALUES LESS THAN (11),
PARTITION p3 VALUES LESS THAN (16),
PARTITION p4 VALUES LESS THAN (21)
);
INSERT INTO t2 VALUES(1, 'AAA_2', 'AAA_3', 'AAA_4'),
(2, 'BBB_2', 'BBB_3', 'BBB_4'),
(3, 'CCC_2', 'CCC_3', 'CCC_4'),
(4, 'DDD_2', 'DDD_3', 'DDD_4'),
(5, 'EEE_2', 'EEE_3', 'EEE_4'),
(6, 'FFF_2', 'DDD_3', 'DDD_4'),
(7, 'GGG_2', 'DDD_3', 'DDD_4'),
(8, 'HHH_2', 'DDD_3', 'DDD_4'),
(9, 'III_2', 'DDD_3', 'DDD_4'),
(10, 'JJJ_2', 'DDD_3', 'DDD_4'),
(13, 'KKK_2', 'DDD_3', 'DDD_4'),
(20, 'LLL_2', 'DDD_3', 'DDD_4');
ALTER TABLE t2 CONVERT TO CHARACTER SET 'utf8mb4',ALGORITHM=COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name LIKE '%t2%';
ALTER TABLE t2 ADD COLUMN c5 VARCHAR(15), ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name LIKE '%t2%';
ALTER TABLE t2 DROP COLUMN c5, ALGORITHM = COPY;
SELECT n_rows, database_name, lower(table_name)
FROM mysql.innodb_table_stats WHERE table_name LIKE '%t2%';
--echo # Test Cleanup.
DROP TABLE t1;
DROP TABLE t2;

2
mysql-test/suite/innodb/t/innodb-alter-debug.test

@ -156,7 +156,7 @@ ALTER TABLE t1 FORCE, ALGORITHM=COPY;
connection default;
SET DEBUG_SYNC='now WAIT_FOR blocked';
BEGIN;
SELECT * FROM mysql.innodb_table_stats FOR UPDATE;
SELECT database_name, table_name FROM mysql.innodb_table_stats FOR UPDATE;
SET DEBUG_SYNC='now SIGNAL go';
connection con1;

2
mysql-test/suite/innodb/t/xap_release_locks_on_dict_stats_table.test

@ -20,7 +20,7 @@ SET @old_debug_dbug = @@global.debug_dbug;
XA START 'a';
INSERT INTO mysql.innodb_index_stats SELECT '','' AS table_name,index_name,LAST_UPDATE,stat_name,0 AS stat_value,sample_size,stat_description FROM mysql.innodb_index_stats WHERE table_name='dummy' FOR UPDATE; # Note the SELECT is empty
SET GLOBAL debug_dbug = "+d,dict_stats_save_exit_notify";
INSERT INTO t VALUES (1);
INSERT INTO t VALUES (1), (2);
XA END 'a';
XA PREPARE 'a';

3
mysql-test/suite/innodb_fts/t/versioning.test

@ -65,6 +65,9 @@ if ($MTR_COMBINATION_UPGRADE)
{
--disable_query_log
call mtr.add_suppression("InnoDB: Table `mysql`.\`innodb_(table|index)_stats`");
call mtr.add_suppression("InnoDB: Table mysql\\.innodb_table_stats is not readable");
call mtr.add_suppression("InnoDB: Fetch of persistent statistics requested for table `test`\\.`articles[0-9]*` but the required system tables mysql.innodb_table_stats and mysql.innodb_index_stats are not present or have unexpected structure. Using transient stats instead.");
call mtr.add_suppression("InnoDB: Recalculation of persistent statistics requested for table `test`\\.`#sql-alter.*` but the required persistent statistics storage is not present or is corrupted. Using transient stats instead.");
--enable_query_log
--source include/shutdown_mysqld.inc
--exec rm -f $datadir/test/*.ibd $datadir/ib*

15
storage/innobase/dict/dict0stats.cc

@ -380,7 +380,15 @@ dict_table_schema_check(
return DB_TABLE_NOT_FOUND;
}
if (!table->is_readable() && !table->space) {
if (!table->is_readable()) {
/* table is not readable */
snprintf(errstr, errstr_sz,
"Table %s is not readable.",
req_schema->table_name_sql);
return DB_ERROR;
}
if (!table->space) {
/* missing tablespace */
snprintf(errstr, errstr_sz,
"Tablespace for table %s is missing.",
@ -3921,8 +3929,9 @@ dberr_t dict_stats_rename_table(const char *old_name, const char *new_name,
dict_fs2utf8(old_name, old_db, sizeof old_db, old_table, sizeof old_table);
dict_fs2utf8(new_name, new_db, sizeof new_db, new_table, sizeof new_table);
if (dict_table_t::is_temporary_name(old_name) ||
dict_table_t::is_temporary_name(new_name))
/* Delete the stats only if renaming the table from old table to
intermediate table during COPY algorithm */
if (dict_table_t::is_temporary_name(new_name))
{
if (dberr_t e= dict_stats_delete_from_table_stats(old_db, old_table, trx))
return e;

43
storage/innobase/handler/ha_innodb.cc

@ -15787,16 +15787,17 @@ ha_innobase::extra(
/* Warning: since it is not sure that MariaDB calls external_lock()
before calling this function, m_prebuilt->trx can be obsolete! */
trx_t* trx;
THD* thd = ha_thd();
switch (operation) {
case HA_EXTRA_FLUSH:
(void)check_trx_exists(ha_thd());
(void)check_trx_exists(thd);
if (m_prebuilt->blob_heap) {
row_mysql_prebuilt_free_blob_heap(m_prebuilt);
}
break;
case HA_EXTRA_RESET_STATE:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
reset_template();
trx->duplicates = 0;
stmt_boundary:
@ -15804,23 +15805,23 @@ ha_innobase::extra(
trx->bulk_insert = false;
break;
case HA_EXTRA_NO_KEYREAD:
(void)check_trx_exists(ha_thd());
(void)check_trx_exists(thd);
m_prebuilt->read_just_key = 0;
break;
case HA_EXTRA_KEYREAD:
(void)check_trx_exists(ha_thd());
(void)check_trx_exists(thd);
m_prebuilt->read_just_key = 1;
break;
case HA_EXTRA_KEYREAD_PRESERVE_FIELDS:
(void)check_trx_exists(ha_thd());
(void)check_trx_exists(thd);
m_prebuilt->keep_other_fields_on_keyread = 1;
break;
case HA_EXTRA_INSERT_WITH_UPDATE:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
trx->duplicates |= TRX_DUP_IGNORE;
goto stmt_boundary;
case HA_EXTRA_NO_IGNORE_DUP_KEY:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
trx->duplicates &= ~TRX_DUP_IGNORE;
if (trx->is_bulk_insert()) {
/* Allow a subsequent INSERT into an empty table
@ -15829,11 +15830,11 @@ ha_innobase::extra(
}
goto stmt_boundary;
case HA_EXTRA_WRITE_CAN_REPLACE:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
trx->duplicates |= TRX_DUP_REPLACE;
goto stmt_boundary;
case HA_EXTRA_WRITE_CANNOT_REPLACE:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
trx->duplicates &= ~TRX_DUP_REPLACE;
if (trx->is_bulk_insert()) {
/* Allow a subsequent INSERT into an empty table
@ -15842,7 +15843,7 @@ ha_innobase::extra(
}
goto stmt_boundary;
case HA_EXTRA_BEGIN_ALTER_COPY:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
m_prebuilt->table->skip_alter_undo = 1;
if (m_prebuilt->table->is_temporary()
|| !m_prebuilt->table->versioned_by_id()) {
@ -15855,7 +15856,7 @@ ha_innobase::extra(
.first->second.set_versioned(0);
break;
case HA_EXTRA_END_ALTER_COPY:
trx = check_trx_exists(ha_thd());
trx = check_trx_exists(thd);
if (!m_prebuilt->table->skip_alter_undo) {
/* This could be invoked inside INSERT...SELECT.
We do not want any extra log writes, because
@ -15874,6 +15875,7 @@ ha_innobase::extra(
handler::extra(HA_EXTRA_BEGIN_ALTER_COPY). */
log_buffer_flush_to_disk();
}
alter_stats_rebuild(m_prebuilt->table, thd);
break;
default:/* Do nothing */
;
@ -21291,3 +21293,22 @@ ulint buf_pool_size_align(ulint size) noexcept
return (ulint)((size / m + 1) * m);
}
}
/** Adjust the persistent statistics after rebuilding ALTER TABLE.
Remove statistics for dropped indexes, add statistics for created indexes
and rename statistics for renamed indexes.
@param table InnoDB table that was rebuilt by ALTER TABLE
@param thd alter table thread */
void alter_stats_rebuild(dict_table_t *table, THD *thd)
{
DBUG_ENTER("alter_stats_rebuild");
if (!table->space || !dict_stats_is_persistent_enabled(table))
DBUG_VOID_RETURN;
dberr_t ret= dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT);
if (ret != DB_SUCCESS)
push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN,
ER_ALTER_INFO, "Error updating stats for table after"
" table rebuild: %s", ut_strerr(ret));
DBUG_VOID_RETURN;
}

7
storage/innobase/handler/ha_innodb.h

@ -942,3 +942,10 @@ ib_push_frm_error(
@return true if index column length exceeds limit */
MY_ATTRIBUTE((warn_unused_result))
bool too_big_key_part_length(size_t max_field_len, const KEY& key);
/** Adjust the persistent statistics after rebuilding ALTER TABLE.
Remove statistics for dropped indexes, add statistics for created indexes
and rename statistics for renamed indexes.
@param table_name Table name in MySQL
@param thd alter table thread */
void alter_stats_rebuild(dict_table_t *table, THD *thd);

43
storage/innobase/handler/handler0alter.cc

@ -11176,7 +11176,7 @@ Remove statistics for dropped indexes, add statistics for created indexes
and rename statistics for renamed indexes.
@param ha_alter_info Data used during in-place alter
@param ctx In-place ALTER TABLE context
@param thd MySQL connection
@param thd alter table thread
*/
static
void
@ -11206,43 +11206,6 @@ alter_stats_norebuild(
DBUG_VOID_RETURN;
}
/** Adjust the persistent statistics after rebuilding ALTER TABLE.
Remove statistics for dropped indexes, add statistics for created indexes
and rename statistics for renamed indexes.
@param table InnoDB table that was rebuilt by ALTER TABLE
@param table_name Table name in MySQL
@param thd MySQL connection
*/
static
void
alter_stats_rebuild(
/*================*/
dict_table_t* table,
const char* table_name,
THD* thd)
{
DBUG_ENTER("alter_stats_rebuild");
if (!table->space
|| !dict_stats_is_persistent_enabled(table)) {
DBUG_VOID_RETURN;
}
dberr_t ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT);
if (ret != DB_SUCCESS) {
push_warning_printf(
thd,
Sql_condition::WARN_LEVEL_WARN,
ER_ALTER_INFO,
"Error updating stats for table '%s'"
" after table rebuild: %s",
table_name, ut_strerr(ret));
}
DBUG_VOID_RETURN;
}
/** Apply the log for the table rebuild operation.
@param[in] ctx Inplace Alter table context
@param[in] altered_table MySQL table that is being altered
@ -11908,9 +11871,7 @@ foreign_fail:
(*pctx);
DBUG_ASSERT(ctx->need_rebuild());
alter_stats_rebuild(
ctx->new_table, table->s->table_name.str,
m_user_thd);
alter_stats_rebuild(ctx->new_table, m_user_thd);
}
} else {
for (inplace_alter_handler_ctx** pctx = ctx_array;

Loading…
Cancel
Save