From 89101e30723932ccc6e93a68b90f68c80184be9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= <marko.makela@mariadb.com>
Date: Fri, 1 Dec 2017 16:51:24 +0200
Subject: [PATCH] MDEV-14511 Use fewer transactions for updating InnoDB
 persistent statistics

dict_stats_exec_sql(): Expect the caller to always provide a transaction.
Remove some redundant assertions. The caller must hold dict_sys->mutex,
but holding dict_operation_lock is only necessary for accessing
data dictionary tables, which we are not accessing.

dict_stats_save_index_stat(): Acquire dict_sys->mutex
for invoking dict_stats_exec_sql().

dict_stats_save(), dict_stats_update_for_index(), dict_stats_update(),
dict_stats_drop_index(), dict_stats_delete_from_table_stats(),
dict_stats_delete_from_index_stats(), dict_stats_drop_table(),
dict_stats_rename_in_table_stats(), dict_stats_rename_in_index_stats(),
dict_stats_rename_table(): Use a single caller-provided
transaction that is started and committed or rolled back by the caller.

dict_stats_process_entry_from_recalc_pool(): Let the caller provide
a transaction object.

ha_innobase::open(): Pass a transaction to dict_stats_init().

ha_innobase::create(), ha_innobase::discard_or_import_tablespace():
Pass a transaction to dict_stats_update().

ha_innobase::rename_table(): Pass a transaction to
dict_stats_rename_table(). We do not use the same transaction
as the one that updated the data dictionary tables, because
we already released the dict_operation_lock. (FIXME: there is
a race condition; a lock wait on SYS_* tables could occur
in another DDL transaction until the data dictionary transaction
is committed.)

ha_innobase::info_low(): Pass a transaction to dict_stats_update()
when calculating persistent statistics.

alter_stats_norebuild(), alter_stats_rebuild(): Update the
persistent statistics as well. In this way, a single transaction
will be used for updating the statistics of a whole table, even
for partitioned tables.

lock_rec_enqueue_waiting(), lock_table_enqueue_waiting():
Do not allow a lock wait to occur for updating statistics
in a data dictionary transaction, such as DROP TABLE. Instead,
return the previously unused error code DB_QUE_THR_SUSPENDED.

row_merge_lock_table(), row_mysql_lock_table(): Remove dead code
for handling DB_QUE_THR_SUSPENDED.

row_drop_table_for_mysql(): Drop the statistics as part of the
data dictionary transaction.
---
 .../suite/innodb/r/innodb_stats_debug.result       |  12 +
 mysql-test/suite/innodb/t/innodb_stats_debug.test  |  13 +
 .../suite/innodb/t/innodb_stats_drop_locked.test   |   2 +-
 storage/innobase/dict/dict0stats.cc                | 474 ++++++++-------------
 storage/innobase/dict/dict0stats_bg.cc             |  14 +-
 storage/innobase/handler/ha_innodb.cc              |  63 ++-
 storage/innobase/handler/handler0alter.cc          | 123 +++---
 storage/innobase/include/dict0stats.h              | 103 ++---
 storage/innobase/include/dict0stats.ic             |  11 +-
 storage/innobase/lock/lock0lock.cc                 |  21 +-
 storage/innobase/row/row0merge.cc                  |  26 +-
 storage/innobase/row/row0mysql.cc                  |  54 +--
 storage/xtradb/dict/dict0stats.cc                  | 474 ++++++++-------------
 storage/xtradb/dict/dict0stats_bg.cc               |  14 +-
 storage/xtradb/handler/ha_innodb.cc                |  63 ++-
 storage/xtradb/handler/handler0alter.cc            | 124 +++---
 storage/xtradb/include/dict0stats.h                | 103 ++---
 storage/xtradb/include/dict0stats.ic               |  11 +-
 storage/xtradb/lock/lock0lock.cc                   |  21 +-
 storage/xtradb/row/row0merge.cc                    |  26 +-
 storage/xtradb/row/row0mysql.cc                    |  54 +--
 21 files changed, 809 insertions(+), 997 deletions(-)
 create mode 100644 mysql-test/suite/innodb/r/innodb_stats_debug.result
 create mode 100644 mysql-test/suite/innodb/t/innodb_stats_debug.test

diff --git a/mysql-test/suite/innodb/r/innodb_stats_debug.result b/mysql-test/suite/innodb/r/innodb_stats_debug.result
new file mode 100644
index 00000000000..b24fa387006
--- /dev/null
+++ b/mysql-test/suite/innodb/r/innodb_stats_debug.result
@@ -0,0 +1,12 @@
+call mtr.add_suppression("InnoDB: Cannot save table statistics for table .test.\\..t1.: Persistent statistics do not exist");
+CREATE TABLE t1 (a INT, KEY(a)) ENGINE=INNODB STATS_PERSISTENT=1;
+SET @save_debug= @@SESSION.debug_dbug;
+SET debug_dbug= '+d,stats_index_error';
+ANALYZE TABLE t1;
+Table	Op	Msg_type	Msg_text
+test.t1	analyze	status	Operation failed
+SET debug_dbug= @save_debug;
+ANALYZE TABLE t1;
+Table	Op	Msg_type	Msg_text
+test.t1	analyze	status	OK
+DROP TABLE t1;
diff --git a/mysql-test/suite/innodb/t/innodb_stats_debug.test b/mysql-test/suite/innodb/t/innodb_stats_debug.test
new file mode 100644
index 00000000000..a2bfbf3d39d
--- /dev/null
+++ b/mysql-test/suite/innodb/t/innodb_stats_debug.test
@@ -0,0 +1,13 @@
+--source include/have_innodb.inc
+--source include/have_debug.inc
+
+call mtr.add_suppression("InnoDB: Cannot save table statistics for table .test.\\..t1.: Persistent statistics do not exist");
+
+CREATE TABLE t1 (a INT, KEY(a)) ENGINE=INNODB STATS_PERSISTENT=1;
+SET @save_debug= @@SESSION.debug_dbug;
+SET debug_dbug= '+d,stats_index_error';
+ANALYZE TABLE t1;
+SET debug_dbug= @save_debug;
+ANALYZE TABLE t1;
+
+DROP TABLE t1;
diff --git a/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test b/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test
index 26367b8e6ae..ab3956e5db2 100644
--- a/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test
+++ b/mysql-test/suite/innodb/t/innodb_stats_drop_locked.test
@@ -57,5 +57,5 @@ SELECT table_name FROM mysql.innodb_index_stats
 WHERE table_name='innodb_stats_drop_locked';
 
 --disable_query_log
-call mtr.add_suppression("Unable to delete statistics for table test.innodb_stats_drop_locked: Lock wait timeout. They can be deleted later using DELETE FROM mysql.innodb_index_stats WHERE database_name");
+call mtr.add_suppression("Unable to delete statistics for table test\\.innodb_stats_drop_locked: Lock wait\\. They can be deleted later using DELETE FROM mysql+\.innodb_index_stats WHERE database_name");
 --enable_query_log
diff --git a/storage/innobase/dict/dict0stats.cc b/storage/innobase/dict/dict0stats.cc
index 0063b6b9ed4..d26fa96d377 100644
--- a/storage/innobase/dict/dict0stats.cc
+++ b/storage/innobase/dict/dict0stats.cc
@@ -1,6 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 2009, 2017, Oracle and/or its affiliates. All Rights Reserved.
+Copyright (c) 2017, 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
@@ -287,9 +288,7 @@ This function will free the pinfo object.
 @param[in,out]	pinfo	pinfo to pass to que_eval_sql() must already
 have any literals bound to it
 @param[in]	sql	SQL string to execute
-@param[in,out]	trx	in case of NULL the function will allocate and
-free the trx object. If it is not NULL then it will be rolled back
-only in the case of error, but not freed.
+@param[in,out]	trx	transaction
 @return DB_SUCCESS or error code */
 static
 dberr_t
@@ -299,49 +298,17 @@ dict_stats_exec_sql(
 	trx_t*		trx)
 {
 	dberr_t	err;
-	bool	trx_started = false;
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
 
 	if (!dict_stats_persistent_storage_check(true)) {
 		pars_info_free(pinfo);
 		return(DB_STATS_DO_NOT_EXIST);
 	}
 
-	if (trx == NULL) {
-		trx = trx_allocate_for_background();
-		trx_start_if_not_started(trx);
-		trx_started = true;
-	}
-
 	err = que_eval_sql(pinfo, sql, FALSE, trx); /* pinfo is freed here */
 
 	DBUG_EXECUTE_IF("stats_index_error",
-		if (!trx_started) {
 			err = DB_STATS_DO_NOT_EXIST;
-			trx->error_state = DB_STATS_DO_NOT_EXIST;
-		});
-
-	if (!trx_started && err == DB_SUCCESS) {
-		return(DB_SUCCESS);
-	}
-
-	if (err == DB_SUCCESS) {
-		trx_commit_for_mysql(trx);
-	} else {
-		trx->op_info = "rollback of internal trx on stats tables";
-		trx->dict_operation_lock_mode = RW_X_LATCH;
-		trx_rollback_to_savepoint(trx, NULL);
-		trx->dict_operation_lock_mode = 0;
-		trx->op_info = "";
-		ut_a(trx->error_state == DB_SUCCESS);
-	}
-
-	if (trx_started) {
-		trx_free_for_background(trx);
-	}
+			trx->error_state = DB_STATS_DO_NOT_EXIST;);
 
 	return(err);
 }
@@ -2308,11 +2275,6 @@ dict_stats_save_index_stat(
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
 	dict_fs2utf8(index->table->name, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
@@ -2338,6 +2300,8 @@ dict_stats_save_index_stat(
 	pars_info_add_str_literal(pinfo, "stat_description",
 				  stat_description);
 
+	mutex_enter(&dict_sys->mutex);
+
 	ret = dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE INDEX_STATS_SAVE () IS\n"
@@ -2364,6 +2328,8 @@ dict_stats_save_index_stat(
 		");\n"
 		"END;", trx);
 
+	mutex_exit(&dict_sys->mutex);
+
 	if (ret != DB_SUCCESS) {
 		if (innodb_index_stats_not_found == false &&
 		    index->stats_error_printed == false) {
@@ -2387,6 +2353,7 @@ dict_stats_save_index_stat(
 
 /** Save the table's statistics into the persistent statistics storage.
 @param[in] table_orig	table whose stats to save
+@param[in,out]	trx	transaction
 @param[in] only_for_index if this is non-NULL, then stats for indexes
 that are not equal to it will not be saved, if NULL, then all
 indexes' stats are saved
@@ -2394,9 +2361,9 @@ indexes' stats are saved
 static
 dberr_t
 dict_stats_save(
-/*============*/
 	dict_table_t*		table_orig,
-	const index_id_t*	only_for_index)
+	trx_t*			trx,
+	const index_id_t*	only_for_index = NULL)
 {
 	pars_info_t*	pinfo;
 	lint		now;
@@ -2410,9 +2377,6 @@ dict_stats_save(
 	dict_fs2utf8(table->name, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
-	rw_lock_x_lock(&dict_operation_lock);
-	mutex_enter(&dict_sys->mutex);
-
 	/* MySQL's timestamp is 4 byte, so we use
 	pars_info_add_int4_literal() which takes a lint arg, so "now" is
 	lint */
@@ -2429,6 +2393,8 @@ dict_stats_save(
 	pars_info_add_ull_literal(pinfo, "sum_of_other_index_sizes",
 		table->stat_sum_of_other_index_sizes);
 
+	mutex_enter(&dict_sys->mutex);
+
 	ret = dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE TABLE_STATS_SAVE () IS\n"
@@ -2449,31 +2415,16 @@ dict_stats_save(
 		":clustered_index_size,\n"
 		":sum_of_other_index_sizes\n"
 		");\n"
-		"END;", NULL);
-
-	if (ret != DB_SUCCESS) {
-		char	buf[MAX_FULL_NAME_LEN];
-		ut_print_timestamp(stderr);
-		fprintf(stderr,
-			" InnoDB: Cannot save table statistics for table "
-			"%s: %s\n",
-			ut_format_name(table->name, TRUE, buf, sizeof(buf)),
-			ut_strerr(ret));
+		"END;", trx);
 
-		mutex_exit(&dict_sys->mutex);
-		rw_lock_x_unlock(&dict_operation_lock);
+	mutex_exit(&dict_sys->mutex);
 
-		dict_stats_snapshot_free(table);
+	index_map_t	indexes;
 
-		return(ret);
+	if (ret != DB_SUCCESS) {
+		goto end;
 	}
 
-	trx_t*	trx = trx_allocate_for_background();
-	trx_start_if_not_started(trx);
-
-	dict_index_t*	index;
-	index_map_t	indexes;
-
 	/* Below we do all the modifications in innodb_index_stats in a single
 	transaction for performance reasons. Modifying more than one row in a
 	single transaction may deadlock with other transactions if they
@@ -2486,18 +2437,17 @@ dict_stats_save(
 	stat_name). This is why below we sort the indexes by name and then
 	for each index, do the mods ordered by stat_name. */
 
-	for (index = dict_table_get_first_index(table);
+	for (dict_index_t* index = dict_table_get_first_index(table);
 	     index != NULL;
 	     index = dict_table_get_next_index(index)) {
 
 		indexes[index->name] = index;
 	}
 
-	index_map_t::const_iterator	it;
-
-	for (it = indexes.begin(); it != indexes.end(); ++it) {
+	for (index_map_t::const_iterator it = indexes.begin();
+	     it != indexes.end(); ++it) {
 
-		index = it->second;
+		dict_index_t* index = it->second;
 
 		if (only_for_index != NULL && index->id != *only_for_index) {
 			continue;
@@ -2562,13 +2512,14 @@ dict_stats_save(
 		}
 	}
 
-	trx_commit_for_mysql(trx);
-
+	if (ret != DB_SUCCESS) {
+		char	buf[MAX_FULL_NAME_LEN];
 end:
-	trx_free_for_background(trx);
-
-	mutex_exit(&dict_sys->mutex);
-	rw_lock_x_unlock(&dict_operation_lock);
+		ib_logf(IB_LOG_LEVEL_ERROR,
+			"Cannot save table statistics for table %s: %s\n",
+			ut_format_name(table->name, TRUE, buf, sizeof(buf)),
+			ut_strerr(ret));
+	}
 
 	dict_stats_snapshot_free(table);
 
@@ -3045,13 +2996,13 @@ dict_stats_fetch_from_ps(
 	return(ret);
 }
 
-/*********************************************************************//**
-Fetches or calculates new estimates for index statistics. */
+/** Calculate index statistics.
+@param[in,out]	index	index tree
+@param[in,out]	trx	transaction (for persistent statistics)
+@return DB_SUCCESS or error code */
 UNIV_INTERN
-void
-dict_stats_update_for_index(
-/*========================*/
-	dict_index_t*	index)	/*!< in/out: index */
+dberr_t
+dict_stats_update_for_index(dict_index_t* index, trx_t* trx)
 {
 	DBUG_ENTER("dict_stats_update_for_index");
 
@@ -3063,8 +3014,8 @@ dict_stats_update_for_index(
 			dict_table_stats_lock(index->table, RW_X_LATCH);
 			dict_stats_analyze_index(index);
 			dict_table_stats_unlock(index->table, RW_X_LATCH);
-			dict_stats_save(index->table, &index->id);
-			DBUG_VOID_RETURN;
+			DBUG_RETURN(dict_stats_save(index->table, trx,
+						    &index->id));
 		}
 		/* else */
 
@@ -3092,23 +3043,20 @@ dict_stats_update_for_index(
 	dict_stats_update_transient_for_index(index);
 	dict_table_stats_unlock(index->table, RW_X_LATCH);
 
-	DBUG_VOID_RETURN;
+	DBUG_RETURN(DB_SUCCESS);
 }
 
-/*********************************************************************//**
-Calculates new estimates for table and index statistics. The statistics
-are used in query optimization.
-@return DB_SUCCESS or error code */
+/** Calculate new estimates for table and index statistics.
+@param[in,out]	table			table
+@param[in]	stats_upd_option	how to update statistics
+@param[in,out]	trx			transaction
+@return DB_* error code or DB_SUCCESS */
 UNIV_INTERN
 dberr_t
 dict_stats_update(
-/*==============*/
-	dict_table_t*		table,	/*!< in/out: table */
-	dict_stats_upd_option_t	stats_upd_option)
-					/*!< in: whether to (re) calc
-					the stats or to fetch them from
-					the persistent statistics
-					storage */
+	dict_table_t*		table,
+	dict_stats_upd_option_t	stats_upd_option,
+	trx_t*			trx)
 {
 	char			buf[MAX_FULL_NAME_LEN];
 
@@ -3162,7 +3110,7 @@ dict_stats_update(
 				return(err);
 			}
 
-			err = dict_stats_save(table, NULL);
+			err = dict_stats_save(table, trx);
 
 			return(err);
 		}
@@ -3199,7 +3147,7 @@ dict_stats_update(
 
 			if (dict_stats_persistent_storage_check(false)) {
 
-				return(dict_stats_save(table, NULL));
+				return(dict_stats_save(table, trx));
 			}
 
 			return(DB_STATS_DO_NOT_EXIST);
@@ -3280,9 +3228,9 @@ dict_stats_update(
 			}
 
 			if (dict_stats_auto_recalc_is_enabled(table)) {
-				return(dict_stats_update(
-						table,
-						DICT_STATS_RECALC_PERSISTENT));
+				return dict_stats_update(
+					table, DICT_STATS_RECALC_PERSISTENT,
+					trx);
 			}
 
 			ut_format_name(table->name, TRUE, buf, sizeof(buf));
@@ -3336,26 +3284,21 @@ dict_stats_update(
 	return(DB_SUCCESS);
 }
 
-/*********************************************************************//**
-Removes the information for a particular index's stats from the persistent
-storage if it exists and if there is data stored for this index.
-This function creates its own trx and commits it.
-A note from Marko why we cannot edit user and sys_* tables in one trx:
-marko: The problem is that ibuf merges should be disabled while we are
-rolling back dict transactions.
-marko: If ibuf merges are not disabled, we need to scan the *.ibd files.
-But we shouldn't open *.ibd files before we have rolled back dict
-transactions and opened the SYS_* records for the *.ibd files.
+/** Remove the persistent statistics for an index.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[in]	iname		index name
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_index(
-/*==================*/
-	const char*	db_and_table,/*!< in: db and table, e.g. 'db/table' */
-	const char*	iname,	/*!< in: index name */
-	char*		errstr, /*!< out: error message if != DB_SUCCESS
-				is returned */
-	ulint		errstr_sz)/*!< in: size of the errstr buffer */
+	const char*	db_and_table,
+	const char*	iname,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx)
 {
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
@@ -3382,7 +3325,6 @@ dict_stats_drop_index(
 
 	pars_info_add_str_literal(pinfo, "index_name", iname);
 
-	rw_lock_x_lock(&dict_operation_lock);
 	mutex_enter(&dict_sys->mutex);
 
 	ret = dict_stats_exec_sql(
@@ -3393,14 +3335,9 @@ dict_stats_drop_index(
 		"database_name = :database_name AND\n"
 		"table_name = :table_name AND\n"
 		"index_name = :index_name;\n"
-		"END;\n", NULL);
+		"END;\n", trx);
 
 	mutex_exit(&dict_sys->mutex);
-	rw_lock_x_unlock(&dict_operation_lock);
-
-	if (ret == DB_STATS_DO_NOT_EXIST) {
-		ret = DB_SUCCESS;
-	}
 
 	if (ret != DB_SUCCESS) {
 		ut_snprintf(errstr, errstr_sz,
@@ -3428,105 +3365,72 @@ dict_stats_drop_index(
 	return(ret);
 }
 
-/*********************************************************************//**
-Executes
-DELETE FROM mysql.innodb_table_stats
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Delete table statistics.
+@param[in]	db	schema name
+@param[in]	t	table name
+@param[in,out]	trx	transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
-dict_stats_delete_from_table_stats(
-/*===============================*/
-	const char*	database_name,	/*!< in: database name, e.g. 'db' */
-	const char*	table_name)	/*!< in: table name, e.g. 'table' */
+dict_stats_delete_from_table_stats(const char* db, const char* t, trx_t* trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
+	pars_info_t*	pinfo = pars_info_create();
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
+	pars_info_add_str_literal(pinfo, "database_name", db);
+	pars_info_add_str_literal(pinfo, "table_name", t);
 
-	pinfo = pars_info_create();
-
-	pars_info_add_str_literal(pinfo, "database_name", database_name);
-	pars_info_add_str_literal(pinfo, "table_name", table_name);
-
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE DELETE_FROM_TABLE_STATS () IS\n"
 		"BEGIN\n"
 		"DELETE FROM \"" TABLE_STATS_NAME "\" WHERE\n"
 		"database_name = :database_name AND\n"
 		"table_name = :table_name;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Executes
-DELETE FROM mysql.innodb_index_stats
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Delete index statistics.
+@param[in]	db	schema name
+@param[in]	t	table name
+@param[in,out]	trx	transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
-dict_stats_delete_from_index_stats(
-/*===============================*/
-	const char*	database_name,	/*!< in: database name, e.g. 'db' */
-	const char*	table_name)	/*!< in: table name, e.g. 'table' */
+dict_stats_delete_from_index_stats(const char* db, const char* t, trx_t* trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
-
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
-	pinfo = pars_info_create();
+	pars_info_t*	pinfo = pars_info_create();
 
-	pars_info_add_str_literal(pinfo, "database_name", database_name);
-	pars_info_add_str_literal(pinfo, "table_name", table_name);
+	pars_info_add_str_literal(pinfo, "database_name", db);
+	pars_info_add_str_literal(pinfo, "table_name", t);
 
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE DELETE_FROM_INDEX_STATS () IS\n"
 		"BEGIN\n"
 		"DELETE FROM \"" INDEX_STATS_NAME "\" WHERE\n"
 		"database_name = :database_name AND\n"
 		"table_name = :table_name;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Removes the statistics for a table and all of its indexes from the
-persistent statistics storage if it exists and if there is data stored for
-the table. This function creates its own transaction and commits it.
+/** Remove the persistent statistics for a table and all of its indexes.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_table(
-/*==================*/
-	const char*	db_and_table,	/*!< in: db and table, e.g. 'db/table' */
-	char*		errstr,		/*!< out: error message
-					if != DB_SUCCESS is returned */
-	ulint		errstr_sz)	/*!< in: size of errstr buffer */
+	const char*	db_and_table,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx)
 {
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
 	dberr_t		ret;
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
 	/* skip tables that do not contain a database name
 	e.g. if we are dropping SYS_TABLES */
 	if (strchr(db_and_table, '/') == NULL) {
@@ -3544,18 +3448,21 @@ dict_stats_drop_table(
 	dict_fs2utf8(db_and_table, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
-	ret = dict_stats_delete_from_table_stats(db_utf8, table_utf8);
+	ret = dict_stats_delete_from_table_stats(db_utf8, table_utf8, trx);
 
 	if (ret == DB_SUCCESS) {
-		ret = dict_stats_delete_from_index_stats(db_utf8, table_utf8);
+		ret = dict_stats_delete_from_index_stats(
+			db_utf8, table_utf8, trx);
 	}
 
-	if (ret == DB_STATS_DO_NOT_EXIST) {
-		ret = DB_SUCCESS;
-	}
-
-	if (ret != DB_SUCCESS) {
-
+	switch (ret) {
+	case DB_SUCCESS:
+	case DB_STATS_DO_NOT_EXIST:
+		return(DB_SUCCESS);
+	case DB_QUE_THR_SUSPENDED:
+		ret = DB_LOCK_WAIT;
+		/* fall through */
+	default:
 		ut_snprintf(errstr, errstr_sz,
 			    "Unable to delete statistics for table %s.%s: %s. "
 			    "They can be deleted later using "
@@ -3581,38 +3488,30 @@ dict_stats_drop_table(
 	return(ret);
 }
 
-/*********************************************************************//**
-Executes
-UPDATE mysql.innodb_table_stats SET
-database_name = '...', table_name = '...'
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Rename table statistics.
+@param[in]	old_dbname_utf8		old schema name
+@param[in]	old_tablename_utf8	old table name
+@param[in]	new_dbname_utf8		new schema name
+@param[in]	new_tablename_utf8	new schema name
+@param[in,out]	trx			transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
 dict_stats_rename_in_table_stats(
-/*=============================*/
-	const char*	old_dbname_utf8,/*!< in: database name, e.g. 'olddb' */
-	const char*	old_tablename_utf8,/*!< in: table name, e.g. 'oldtable' */
-	const char*	new_dbname_utf8,/*!< in: database name, e.g. 'newdb' */
-	const char*	new_tablename_utf8)/*!< in: table name, e.g. 'newtable' */
+	const char*	old_dbname_utf8,
+	const char*	old_tablename_utf8,
+	const char*	new_dbname_utf8,
+	const char*	new_tablename_utf8,
+	trx_t*		trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
-
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
-	pinfo = pars_info_create();
+	pars_info_t*	pinfo = pars_info_create();
 
 	pars_info_add_str_literal(pinfo, "old_dbname_utf8", old_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "old_tablename_utf8", old_tablename_utf8);
 	pars_info_add_str_literal(pinfo, "new_dbname_utf8", new_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "new_tablename_utf8", new_tablename_utf8);
 
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE RENAME_IN_TABLE_STATS () IS\n"
 		"BEGIN\n"
@@ -3622,43 +3521,33 @@ dict_stats_rename_in_table_stats(
 		"WHERE\n"
 		"database_name = :old_dbname_utf8 AND\n"
 		"table_name = :old_tablename_utf8;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Executes
-UPDATE mysql.innodb_index_stats SET
-database_name = '...', table_name = '...'
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Rename index statistics.
+@param[in]	old_dbname_utf8		old schema name
+@param[in]	old_tablename_utf8	old table name
+@param[in]	new_dbname_utf8		new schema name
+@param[in]	new_tablename_utf8	new schema name
+@param[in,out]	trx			transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
 dict_stats_rename_in_index_stats(
-/*=============================*/
-	const char*	old_dbname_utf8,/*!< in: database name, e.g. 'olddb' */
-	const char*	old_tablename_utf8,/*!< in: table name, e.g. 'oldtable' */
-	const char*	new_dbname_utf8,/*!< in: database name, e.g. 'newdb' */
-	const char*	new_tablename_utf8)/*!< in: table name, e.g. 'newtable' */
+	const char*	old_dbname_utf8,
+	const char*	old_tablename_utf8,
+	const char*	new_dbname_utf8,
+	const char*	new_tablename_utf8,
+	trx_t*		trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
-
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
-	pinfo = pars_info_create();
+	pars_info_t*	pinfo = pars_info_create();
 
 	pars_info_add_str_literal(pinfo, "old_dbname_utf8", old_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "old_tablename_utf8", old_tablename_utf8);
 	pars_info_add_str_literal(pinfo, "new_dbname_utf8", new_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "new_tablename_utf8", new_tablename_utf8);
 
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE RENAME_IN_INDEX_STATS () IS\n"
 		"BEGIN\n"
@@ -3668,24 +3557,24 @@ dict_stats_rename_in_index_stats(
 		"WHERE\n"
 		"database_name = :old_dbname_utf8 AND\n"
 		"table_name = :old_tablename_utf8;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Renames a table in InnoDB persistent stats storage.
-This function creates its own transaction and commits it.
+/** Rename a table in the InnoDB persistent statistics storage.
+@param[in]	old_name	old schema and table name, e.g., 'db/table'
+@param[in]	new_name	new schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_rename_table(
-/*====================*/
-	const char*	old_name,	/*!< in: old name, e.g. 'db/table' */
-	const char*	new_name,	/*!< in: new name, e.g. 'db/table' */
-	char*		errstr,		/*!< out: error string if != DB_SUCCESS
-					is returned */
-	size_t		errstr_sz)	/*!< in: errstr size */
+	const char*	old_name,
+	const char*	new_name,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx)
 {
 	char		old_db_utf8[MAX_DB_UTF8_LEN];
 	char		new_db_utf8[MAX_DB_UTF8_LEN];
@@ -3693,11 +3582,6 @@ dict_stats_rename_table(
 	char		new_table_utf8[MAX_TABLE_UTF8_LEN];
 	dberr_t		ret;
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(!rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(!mutex_own(&dict_sys->mutex));
-
 	/* skip innodb_table_stats and innodb_index_stats themselves */
 	if (strcmp(old_name, TABLE_STATS_NAME) == 0
 	    || strcmp(old_name, INDEX_STATS_NAME) == 0
@@ -3713,98 +3597,86 @@ dict_stats_rename_table(
 	dict_fs2utf8(new_name, new_db_utf8, sizeof(new_db_utf8),
 		     new_table_utf8, sizeof(new_table_utf8));
 
-	rw_lock_x_lock(&dict_operation_lock);
-	mutex_enter(&dict_sys->mutex);
-
 	ulint	n_attempts = 0;
 	do {
 		n_attempts++;
 
+		trx_savept_t savept = trx_savept_take(trx);
+
+		mutex_enter(&dict_sys->mutex);
+
 		ret = dict_stats_rename_in_table_stats(
 			old_db_utf8, old_table_utf8,
-			new_db_utf8, new_table_utf8);
+			new_db_utf8, new_table_utf8, trx);
+
+		mutex_exit(&dict_sys->mutex);
 
 		if (ret == DB_DUPLICATE_KEY) {
+			trx_rollback_to_savepoint(trx, &savept);
+			mutex_enter(&dict_sys->mutex);
 			dict_stats_delete_from_table_stats(
-				new_db_utf8, new_table_utf8);
+				new_db_utf8, new_table_utf8, trx);
+			mutex_exit(&dict_sys->mutex);
 		}
 
 		if (ret == DB_STATS_DO_NOT_EXIST) {
 			ret = DB_SUCCESS;
+			break;
 		}
 
 		if (ret != DB_SUCCESS) {
-			mutex_exit(&dict_sys->mutex);
-			rw_lock_x_unlock(&dict_operation_lock);
 			os_thread_sleep(200000 /* 0.2 sec */);
-			rw_lock_x_lock(&dict_operation_lock);
-			mutex_enter(&dict_sys->mutex);
 		}
 	} while ((ret == DB_DEADLOCK
 		  || ret == DB_DUPLICATE_KEY
 		  || ret == DB_LOCK_WAIT_TIMEOUT)
 		 && n_attempts < 5);
 
-	if (ret != DB_SUCCESS) {
-		ut_snprintf(errstr, errstr_sz,
-			    "Unable to rename statistics from "
-			    "%s.%s to %s.%s in %s: %s. "
-			    "They can be renamed later using "
-
-			    "UPDATE %s SET "
-			    "database_name = '%s', "
-			    "table_name = '%s' "
-			    "WHERE "
-			    "database_name = '%s' AND "
-			    "table_name = '%s';",
-
-			    old_db_utf8, old_table_utf8,
-			    new_db_utf8, new_table_utf8,
-			    TABLE_STATS_NAME_PRINT,
-			    ut_strerr(ret),
+	const char* table_name = TABLE_STATS_NAME_PRINT;
 
-			    TABLE_STATS_NAME_PRINT,
-			    new_db_utf8, new_table_utf8,
-			    old_db_utf8, old_table_utf8);
-		mutex_exit(&dict_sys->mutex);
-		rw_lock_x_unlock(&dict_operation_lock);
-		return(ret);
+	if (ret != DB_SUCCESS) {
+		goto err_exit;
 	}
-	/* else */
+
+	table_name = INDEX_STATS_NAME_PRINT;
 
 	n_attempts = 0;
 	do {
 		n_attempts++;
 
+		trx_savept_t savept = trx_savept_take(trx);
+
+		mutex_enter(&dict_sys->mutex);
+
 		ret = dict_stats_rename_in_index_stats(
 			old_db_utf8, old_table_utf8,
-			new_db_utf8, new_table_utf8);
+			new_db_utf8, new_table_utf8, trx);
+
+		mutex_exit(&dict_sys->mutex);
 
 		if (ret == DB_DUPLICATE_KEY) {
+			trx_rollback_to_savepoint(trx, &savept);
+			mutex_enter(&dict_sys->mutex);
 			dict_stats_delete_from_index_stats(
-				new_db_utf8, new_table_utf8);
+				new_db_utf8, new_table_utf8, trx);
+			mutex_exit(&dict_sys->mutex);
 		}
 
 		if (ret == DB_STATS_DO_NOT_EXIST) {
 			ret = DB_SUCCESS;
+			break;
 		}
 
 		if (ret != DB_SUCCESS) {
-			mutex_exit(&dict_sys->mutex);
-			rw_lock_x_unlock(&dict_operation_lock);
 			os_thread_sleep(200000 /* 0.2 sec */);
-			rw_lock_x_lock(&dict_operation_lock);
-			mutex_enter(&dict_sys->mutex);
 		}
 	} while ((ret == DB_DEADLOCK
 		  || ret == DB_DUPLICATE_KEY
 		  || ret == DB_LOCK_WAIT_TIMEOUT)
 		 && n_attempts < 5);
 
-	mutex_exit(&dict_sys->mutex);
-	rw_lock_x_unlock(&dict_operation_lock);
-
 	if (ret != DB_SUCCESS) {
+err_exit:
 		ut_snprintf(errstr, errstr_sz,
 			    "Unable to rename statistics from "
 			    "%s.%s to %s.%s in %s: %s. "
@@ -3819,10 +3691,10 @@ dict_stats_rename_table(
 
 			    old_db_utf8, old_table_utf8,
 			    new_db_utf8, new_table_utf8,
-			    INDEX_STATS_NAME_PRINT,
+			    table_name,
 			    ut_strerr(ret),
 
-			    INDEX_STATS_NAME_PRINT,
+			    table_name,
 			    new_db_utf8, new_table_utf8,
 			    old_db_utf8, old_table_utf8);
 	}
diff --git a/storage/innobase/dict/dict0stats_bg.cc b/storage/innobase/dict/dict0stats_bg.cc
index f2760bd6a56..7ca9fc11687 100644
--- a/storage/innobase/dict/dict0stats_bg.cc
+++ b/storage/innobase/dict/dict0stats_bg.cc
@@ -276,8 +276,7 @@ Get the first table that has been added for auto recalc and eventually
 update its stats. */
 static
 void
-dict_stats_process_entry_from_recalc_pool()
-/*=======================================*/
+dict_stats_process_entry_from_recalc_pool(trx_t* trx)
 {
 	table_id_t	table_id;
 
@@ -330,8 +329,10 @@ dict_stats_process_entry_from_recalc_pool()
 		dict_stats_recalc_pool_add(table);
 
 	} else {
-
-		dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT);
+		dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT, trx);
+		if (trx->state != TRX_STATE_NOT_STARTED) {
+			trx_commit_for_mysql(trx);
+		}
 	}
 
 	mutex_enter(&dict_sys->mutex);
@@ -358,6 +359,8 @@ DECLARE_THREAD(dict_stats_thread)(
 	my_thread_init();
 	ut_a(!srv_read_only_mode);
 
+	trx_t* trx = trx_allocate_for_background();
+
 	while (!dict_stats_start_shutdown) {
 
 		/* Wake up periodically even if not signaled. This is
@@ -372,11 +375,12 @@ DECLARE_THREAD(dict_stats_thread)(
 			break;
 		}
 
-		dict_stats_process_entry_from_recalc_pool();
+		dict_stats_process_entry_from_recalc_pool(trx);
 
 		os_event_reset(dict_stats_event);
 	}
 
+	trx_free_for_background(trx);
 	srv_dict_stats_thread_active = false;
 
 	os_event_set(dict_stats_shutdown_event);
diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc
index 4aab1b8d713..f734284874e 100644
--- a/storage/innobase/handler/ha_innodb.cc
+++ b/storage/innobase/handler/ha_innodb.cc
@@ -5425,7 +5425,19 @@ ha_innobase::open(
 
 	innobase_copy_frm_flags_from_table_share(ib_table, table->s);
 
-	dict_stats_init(ib_table);
+	{
+		trx_t* trx = check_trx_exists(thd);
+		bool alloc = !trx_state_eq(trx, TRX_STATE_NOT_STARTED);
+
+		if (alloc) {
+			trx = trx_allocate_for_background();
+		}
+		dict_stats_init(ib_table, trx);
+		innobase_commit_low(trx);
+		if (alloc) {
+			trx_free_for_background(trx);
+		}
+	}
 
 	MONITOR_INC(MONITOR_TABLE_OPEN);
 
@@ -10433,7 +10445,9 @@ ha_innobase::create(
 
 	innobase_copy_frm_flags_from_create_info(innobase_table, create_info);
 
-	dict_stats_update(innobase_table, DICT_STATS_EMPTY_TABLE);
+	++trx->will_lock;
+	dict_stats_update(innobase_table, DICT_STATS_EMPTY_TABLE, trx);
+	innobase_commit_low(trx);
 
 	if (innobase_table) {
 		/* We update the highest file format in the system table
@@ -10607,7 +10621,8 @@ ha_innobase::discard_or_import_tablespace(
 
 		/* Adjust the persistent statistics. */
 		ret = dict_stats_update(dict_table,
-					DICT_STATS_RECALC_PERSISTENT);
+					DICT_STATS_RECALC_PERSISTENT,
+					prebuilt->trx);
 
 		if (ret != DB_SUCCESS) {
 			push_warning_printf(
@@ -10615,8 +10630,11 @@ ha_innobase::discard_or_import_tablespace(
 				Sql_condition::WARN_LEVEL_WARN,
 				ER_ALTER_INFO,
 				"Error updating stats for table '%s'"
-				" after table rebuild: %s",
+				" after table import: %s",
 				dict_table->name, ut_strerr(ret));
+			trx_rollback_to_savepoint(prebuilt->trx, NULL);
+		} else {
+			trx_commit_for_mysql(prebuilt->trx);
 		}
 	}
 
@@ -11016,7 +11034,6 @@ ha_innobase::rename_table(
 	DEBUG_SYNC(thd, "after_innobase_rename_table");
 
 	innobase_commit_low(trx);
-	trx_free_for_mysql(trx);
 
 	if (error == DB_SUCCESS) {
 		char	norm_from[MAX_FULL_NAME_LEN];
@@ -11027,18 +11044,24 @@ ha_innobase::rename_table(
 		normalize_table_name(norm_from, from);
 		normalize_table_name(norm_to, to);
 
+		++trx->will_lock;
 		ret = dict_stats_rename_table(norm_from, norm_to,
-					      errstr, sizeof(errstr));
+					      errstr, sizeof errstr, trx);
 
 		if (ret != DB_SUCCESS) {
+			trx_rollback_to_savepoint(trx, NULL);
 			ut_print_timestamp(stderr);
 			fprintf(stderr, " InnoDB: %s\n", errstr);
 
 			push_warning(thd, Sql_condition::WARN_LEVEL_WARN,
 				     ER_LOCK_WAIT_TIMEOUT, errstr);
+		} else {
+			innobase_commit_low(trx);
 		}
 	}
 
+	trx_free_for_mysql(trx);
+
 	/* Add a special case to handle the Duplicated Key error
 	and return DB_ERROR instead.
 	This is to avoid a possible SIGSEGV error from mysql error
@@ -11554,17 +11577,30 @@ ha_innobase::info_low(
 			}
 
 			ut_ad(!mutex_own(&dict_sys->mutex));
-			ret = dict_stats_update(ib_table, opt);
+			/* Do not use prebuilt->trx in case this is
+			called in the middle of a transaction. We
+			should commit the transaction after
+			dict_stats_update() in order not to hog locks
+			on the mysql.innodb_table_stats,
+			mysql.innodb_index_stats tables. */
+			trx_t* trx = trx_allocate_for_background();
+			ret = dict_stats_update(ib_table, opt, trx);
 
 			if (ret != DB_SUCCESS) {
 				prebuilt->trx->op_info = "";
-				DBUG_RETURN(HA_ERR_GENERIC);
+				trx_rollback_to_savepoint(trx, NULL);
+			} else {
+				prebuilt->trx->op_info =
+					"returning various info to MySQL";
+				trx_commit_for_mysql(trx);
 			}
 
-			prebuilt->trx->op_info =
-				"returning various info to MySQL";
-		}
+			trx_free_for_background(trx);
 
+			if (ret != DB_SUCCESS) {
+				DBUG_RETURN(HA_ERR_GENERIC);
+			}
+		}
 	}
 
 	if (flag & HA_STATUS_VARIABLE) {
@@ -11812,7 +11848,10 @@ ha_innobase::info_low(
 
 					/* This is better than
 					assert on below function */
-					dict_stats_init(index->table);
+					dict_stats_update(
+						index->table,
+						DICT_STATS_RECALC_TRANSIENT,
+						NULL);
 				}
 
 				rec_per_key = innodb_rec_per_key(
diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc
index 82eedddb280..8d22fd93c90 100644
--- a/storage/innobase/handler/handler0alter.cc
+++ b/storage/innobase/handler/handler0alter.cc
@@ -5483,27 +5483,25 @@ 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 altered_table	MySQL table that is being altered
 @param table_name	Table name in MySQL
-@param thd		MySQL connection
-*/
+@param trx		transaction
+@return error code */
 static
-void
+dberr_t
 alter_stats_norebuild(
-/*==================*/
 	Alter_inplace_info*		ha_alter_info,
 	ha_innobase_inplace_ctx*	ctx,
-	TABLE*				altered_table,
 	const char*			table_name,
-	THD*				thd)
+	trx_t*				trx)
 {
+	dberr_t	err = DB_SUCCESS;
 	ulint	i;
 
 	DBUG_ENTER("alter_stats_norebuild");
 	DBUG_ASSERT(!ctx->need_rebuild());
 
 	if (!dict_stats_is_persistent_enabled(ctx->new_table)) {
-		DBUG_VOID_RETURN;
+		DBUG_RETURN(err);
 	}
 
 	/* TODO: This will not drop the (unused) statistics for
@@ -5520,10 +5518,13 @@ alter_stats_norebuild(
 
 		char	errstr[1024];
 
-		if (dict_stats_drop_index(
-			    ctx->new_table->name, key->name,
-			    errstr, sizeof errstr) != DB_SUCCESS) {
-			push_warning(thd,
+		dberr_t err2 = dict_stats_drop_index(
+			ctx->new_table->name, key->name,
+			errstr, sizeof errstr, trx);
+
+		if (err2 != DB_SUCCESS) {
+			err = err2;
+			push_warning(trx->mysql_thd,
 				     Sql_condition::WARN_LEVEL_WARN,
 				     ER_LOCK_WAIT_TIMEOUT, errstr);
 		}
@@ -5534,12 +5535,15 @@ alter_stats_norebuild(
 		DBUG_ASSERT(index->table == ctx->new_table);
 
 		if (!(index->type & DICT_FTS)) {
-			dict_stats_init(ctx->new_table);
-			dict_stats_update_for_index(index);
+			dict_stats_init(ctx->new_table, trx);
+			dberr_t err2 = dict_stats_update_for_index(index, trx);
+			if (err2 != DB_SUCCESS) {
+				err = err2;
+			}
 		}
 	}
 
-	DBUG_VOID_RETURN;
+	DBUG_RETURN(err);
 }
 
 /** Adjust the persistent statistics after rebuilding ALTER TABLE.
@@ -5547,30 +5551,44 @@ 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
-*/
+@param trx		transaction
+@return error code */
 static
-void
+dberr_t
 alter_stats_rebuild(
-/*================*/
 	dict_table_t*	table,
 	const char*	table_name,
-	THD*		thd)
+	trx_t*		trx)
 {
 	DBUG_ENTER("alter_stats_rebuild");
 
 	if (dict_table_is_discarded(table)
 	    || !dict_stats_is_persistent_enabled(table)) {
-		DBUG_VOID_RETURN;
+		DBUG_RETURN(DB_SUCCESS);
 	}
 
-	dberr_t	ret;
+	char	errstr[1024];
+	mutex_enter(&dict_sys->mutex);
+	dberr_t	ret = dict_stats_drop_table(table->name,
+					    errstr, sizeof errstr, trx);
+	mutex_exit(&dict_sys->mutex);
+	if (ret != DB_SUCCESS) {
+		push_warning_printf(
+			trx->mysql_thd,
+			Sql_condition::WARN_LEVEL_WARN,
+			ER_ALTER_INFO,
+			"Deleting persistent statistics"
+			" for rebuilt table '%s' in"
+			" InnoDB failed: %s",
+			table_name, errstr);
+		DBUG_RETURN(ret);
+	}
 
-	ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT);
+	ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT, trx);
 
 	if (ret != DB_SUCCESS) {
 		push_warning_printf(
-			thd,
+			trx->mysql_thd,
 			Sql_condition::WARN_LEVEL_WARN,
 			ER_ALTER_INFO,
 			"Error updating stats for table '%s' "
@@ -5578,7 +5596,7 @@ alter_stats_rebuild(
 			table_name, ut_strerr(ret));
 	}
 
-	DBUG_VOID_RETURN;
+	DBUG_RETURN(ret);
 }
 
 #ifndef DBUG_OFF
@@ -6133,31 +6151,6 @@ ha_innobase::commit_inplace_alter_table(
 		}
 #endif
 		if (new_clustered) {
-			/* Since the table has been rebuilt, we remove
-			all persistent statistics corresponding to the
-			old copy of the table (which was renamed to
-			ctx->tmp_name). */
-
-			char	errstr[1024];
-
-			DBUG_ASSERT(0 == strcmp(ctx->old_table->name,
-						ctx->tmp_name));
-
-			if (dict_stats_drop_table(
-				    ctx->new_table->name,
-				    errstr, sizeof(errstr))
-			    != DB_SUCCESS) {
-				push_warning_printf(
-					user_thd,
-					Sql_condition::WARN_LEVEL_WARN,
-					ER_ALTER_INFO,
-					"Deleting persistent statistics"
-					" for rebuilt table '%s' in"
-					" InnoDB failed: %s",
-					table->s->table_name.str,
-					errstr);
-			}
-
 			DBUG_EXECUTE_IF("ib_ddl_crash_before_commit",
 					DBUG_SUICIDE(););
 
@@ -6186,11 +6179,13 @@ ha_innobase::commit_inplace_alter_table(
 	}
 
 	row_mysql_unlock_data_dictionary(trx);
-	trx_free_for_mysql(trx);
+	++trx->will_lock;
 
 	/* TODO: The following code could be executed
 	while allowing concurrent access to the table
 	(MDL downgrade). */
+	trx->mysql_thd = user_thd;
+	dberr_t stats_err = DB_SUCCESS;
 
 	if (new_clustered) {
 		for (inplace_alter_handler_ctx** pctx = ctx_array;
@@ -6199,10 +6194,11 @@ ha_innobase::commit_inplace_alter_table(
 				= static_cast<ha_innobase_inplace_ctx*>
 				(*pctx);
 			DBUG_ASSERT(ctx->need_rebuild());
-
-			alter_stats_rebuild(
-				ctx->new_table, table->s->table_name.str,
-				user_thd);
+			stats_err = alter_stats_rebuild(
+				ctx->new_table, table->s->table_name.str, trx);
+			if (stats_err != DB_SUCCESS) {
+				break;
+			}
 			DBUG_INJECT_CRASH("ib_commit_inplace_crash",
 					  crash_inject_count++);
 		}
@@ -6214,14 +6210,25 @@ ha_innobase::commit_inplace_alter_table(
 				(*pctx);
 			DBUG_ASSERT(!ctx->need_rebuild());
 
-			alter_stats_norebuild(
-				ha_alter_info, ctx, altered_table,
-				table->s->table_name.str, user_thd);
+			stats_err = alter_stats_norebuild(
+				ha_alter_info, ctx,
+				table->s->table_name.str, trx);
+			if (stats_err != DB_SUCCESS) {
+				break;
+			}
 			DBUG_INJECT_CRASH("ib_commit_inplace_crash",
 					  crash_inject_count++);
 		}
 	}
 
+	if (stats_err != DB_SUCCESS) {
+		trx_rollback_to_savepoint(trx, NULL);
+	} else {
+		trx_commit_for_mysql(trx);
+	}
+
+	trx_free_for_mysql(trx);
+
 	/* TODO: Also perform DROP TABLE and DROP INDEX after
 	the MDL downgrade. */
 
diff --git a/storage/innobase/include/dict0stats.h b/storage/innobase/include/dict0stats.h
index 35ee1a00d8a..c6e8e114449 100644
--- a/storage/innobase/include/dict0stats.h
+++ b/storage/innobase/include/dict0stats.h
@@ -1,6 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 2009, 2016, Oracle and/or its affiliates. All Rights Reserved.
+Copyright (c) 2017, 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
@@ -111,13 +112,12 @@ dict_stats_auto_recalc_is_enabled(
 /*==============================*/
 	const dict_table_t*	table);	/*!< in: table */
 
-/*********************************************************************//**
-Initialize table's stats for the first time when opening a table. */
+/** Initialize table statistics for the first time when opening a table.
+@param[in,out]	table	freshly opened table
+@param[in,out]	trx	transaction */
 UNIV_INLINE
 void
-dict_stats_init(
-/*============*/
-	dict_table_t*	table);	/*!< in/out: table */
+dict_stats_init(dict_table_t* table, trx_t* trx);
 
 /*********************************************************************//**
 Deinitialize table's stats after the last close of the table. This is
@@ -129,71 +129,72 @@ dict_stats_deinit(
 	dict_table_t*	table)	/*!< in/out: table */
 	MY_ATTRIBUTE((nonnull));
 
-/*********************************************************************//**
-Calculates new estimates for table and index statistics. The statistics
-are used in query optimization.
+/** Calculate new estimates for table and index statistics.
+@param[in,out]	table			table
+@param[in]	stats_upd_option	how to update statistics
+@param[in,out]	trx			transaction
 @return DB_* error code or DB_SUCCESS */
 UNIV_INTERN
 dberr_t
 dict_stats_update(
-/*==============*/
-	dict_table_t*		table,	/*!< in/out: table */
-	dict_stats_upd_option_t	stats_upd_option);
-					/*!< in: whether to (re) calc
-					the stats or to fetch them from
-					the persistent storage */
-
-/*********************************************************************//**
-Removes the information for a particular index's stats from the persistent
-storage if it exists and if there is data stored for this index.
-This function creates its own trx and commits it.
+	dict_table_t*		table,
+	dict_stats_upd_option_t	stats_upd_option,
+	trx_t*			trx);
+
+/** Remove the persistent statistics for an index.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[in]	iname		index name
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_index(
-/*==================*/
-	const char*	tname,	/*!< in: table name */
-	const char*	iname,	/*!< in: index name */
-	char*		errstr, /*!< out: error message if != DB_SUCCESS
-				is returned */
-	ulint		errstr_sz);/*!< in: size of the errstr buffer */
-
-/*********************************************************************//**
-Removes the statistics for a table and all of its indexes from the
-persistent storage if it exists and if there is data stored for the table.
-This function creates its own transaction and commits it.
+	const char*	db_and_table,
+	const char*	iname,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx);
+
+/** Remove the persistent statistics for a table and all of its indexes.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_table(
-/*==================*/
-	const char*	table_name,	/*!< in: table name */
-	char*		errstr,		/*!< out: error message
-					if != DB_SUCCESS is returned */
-	ulint		errstr_sz);	/*!< in: size of errstr buffer */
-
-/*********************************************************************//**
-Fetches or calculates new estimates for index statistics. */
+	const char*	db_and_table,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx);
+
+/** Calculate index statistics.
+@param[in,out]	index	index tree
+@param[in,out]	trx	transaction (for persistent statistics)
+@return DB_SUCCESS or error code */
 UNIV_INTERN
-void
-dict_stats_update_for_index(
-/*========================*/
-	dict_index_t*	index)	/*!< in/out: index */
+dberr_t
+dict_stats_update_for_index(dict_index_t* index, trx_t* trx)
 	MY_ATTRIBUTE((nonnull));
 
-/*********************************************************************//**
-Renames a table in InnoDB persistent stats storage.
-This function creates its own transaction and commits it.
+/** Rename a table in the InnoDB persistent statistics storage.
+@param[in]	old_name	old schema and table name, e.g., 'db/table'
+@param[in]	new_name	new schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_rename_table(
-/*====================*/
-	const char*	old_name,	/*!< in: old table name */
-	const char*	new_name,	/*!< in: new table name */
-	char*		errstr,		/*!< out: error string if != DB_SUCCESS
-					is returned */
-	size_t		errstr_sz);	/*!< in: errstr size */
+	const char*	old_name,
+	const char*	new_name,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx);
 
 #ifndef UNIV_NONINL
 #include "dict0stats.ic"
diff --git a/storage/innobase/include/dict0stats.ic b/storage/innobase/include/dict0stats.ic
index ec9a9065470..48c53fa0576 100644
--- a/storage/innobase/include/dict0stats.ic
+++ b/storage/innobase/include/dict0stats.ic
@@ -149,13 +149,12 @@ dict_stats_auto_recalc_is_enabled(
 	}
 }
 
-/*********************************************************************//**
-Initialize table's stats for the first time when opening a table. */
+/** Initialize table statistics for the first time when opening a table.
+@param[in,out]	table	freshly opened table
+@param[in,out]	trx	transaction */
 UNIV_INLINE
 void
-dict_stats_init(
-/*============*/
-	dict_table_t*	table)	/*!< in/out: table */
+dict_stats_init(dict_table_t* table, trx_t* trx)
 {
 	ut_ad(!mutex_own(&dict_sys->mutex));
 
@@ -171,7 +170,7 @@ dict_stats_init(
 		opt = DICT_STATS_RECALC_TRANSIENT;
 	}
 
-	dict_stats_update(table, opt);
+	dict_stats_update(table, opt, trx);
 }
 
 /*********************************************************************//**
diff --git a/storage/innobase/lock/lock0lock.cc b/storage/innobase/lock/lock0lock.cc
index f5b7b35d749..8489bf45f79 100644
--- a/storage/innobase/lock/lock0lock.cc
+++ b/storage/innobase/lock/lock0lock.cc
@@ -1,7 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
-Copyright (c) 2014, 2016, MariaDB Corporation
+Copyright (c) 2014, 2017, 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
@@ -1974,6 +1974,16 @@ lock_rec_enqueue_waiting(
 		break;
 	case TRX_DICT_OP_TABLE:
 	case TRX_DICT_OP_INDEX:
+		if (trx->dict_operation_lock_mode != RW_X_LATCH) {
+		} else if (!strcmp(index->table->name,
+				   "mysql/innodb_table_stats")
+			   || !strcmp(index->table->name,
+				      "mysql/innodb_index_stats")) {
+			/* Statistics can be updated as part of a DDL
+			transaction, but only as the very last operation. */
+			return(DB_QUE_THR_SUSPENDED);
+		}
+
 		ut_print_timestamp(stderr);
 		fputs("  InnoDB: Error: a record lock wait happens"
 		      " in a dictionary operation!\n"
@@ -4431,6 +4441,15 @@ lock_table_enqueue_waiting(
 		break;
 	case TRX_DICT_OP_TABLE:
 	case TRX_DICT_OP_INDEX:
+		if (trx->dict_operation_lock_mode != RW_X_LATCH) {
+		} else if (!strcmp(table->name, "mysql/innodb_table_stats")
+			   || !strcmp(table->name,
+				      "mysql/innodb_index_stats")) {
+			/* Statistics can be updated as part of a DDL
+			transaction, but only as the very last operation. */
+			return(DB_QUE_THR_SUSPENDED);
+		}
+
 		ut_print_timestamp(stderr);
 		fputs("  InnoDB: Error: a table lock wait happens"
 		      " in a dictionary operation!\n"
diff --git a/storage/innobase/row/row0merge.cc b/storage/innobase/row/row0merge.cc
index fe845d60101..4d0c3d62e71 100644
--- a/storage/innobase/row/row0merge.cc
+++ b/storage/innobase/row/row0merge.cc
@@ -2716,31 +2716,9 @@ row_merge_lock_table(
 		que_thr_stop_for_mysql_no_error(thr, trx);
 	} else {
 		que_thr_stop_for_mysql(thr);
+		ut_ad(err != DB_QUE_THR_SUSPENDED);
 
-		if (err != DB_QUE_THR_SUSPENDED) {
-			bool	was_lock_wait;
-
-			was_lock_wait = row_mysql_handle_errors(
-				&err, trx, thr, NULL);
-
-			if (was_lock_wait) {
-				goto run_again;
-			}
-		} else {
-			que_thr_t*	run_thr;
-			que_node_t*	parent;
-
-			parent = que_node_get_parent(thr);
-
-			run_thr = que_fork_start_command(
-				static_cast<que_fork_t*>(parent));
-
-			ut_a(run_thr == thr);
-
-			/* There was a lock wait but the thread was not
-			in a ready to run or running state. */
-			trx->error_state = DB_LOCK_WAIT;
-
+		if (row_mysql_handle_errors(&err, trx, thr, NULL)) {
 			goto run_again;
 		}
 	}
diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc
index 74a17b9a1c3..e17e8a28161 100644
--- a/storage/innobase/row/row0mysql.cc
+++ b/storage/innobase/row/row0mysql.cc
@@ -1142,7 +1142,7 @@ row_update_statistics_if_needed(
 
 		ut_ad(!mutex_own(&dict_sys->mutex));
 		/* this will reset table->stat_modified_counter to 0 */
-		dict_stats_update(table, DICT_STATS_RECALC_TRANSIENT);
+		dict_stats_update(table, DICT_STATS_RECALC_TRANSIENT, NULL);
 	}
 }
 
@@ -3253,31 +3253,9 @@ row_mysql_lock_table(
 		que_thr_stop_for_mysql_no_error(thr, trx);
 	} else {
 		que_thr_stop_for_mysql(thr);
+		ut_ad(err != DB_QUE_THR_SUSPENDED);
 
-		if (err != DB_QUE_THR_SUSPENDED) {
-			ibool	was_lock_wait;
-
-			was_lock_wait = row_mysql_handle_errors(
-				&err, trx, thr, NULL);
-
-			if (was_lock_wait) {
-				goto run_again;
-			}
-		} else {
-			que_thr_t*	run_thr;
-			que_node_t*	parent;
-
-			parent = que_node_get_parent(thr);
-
-			run_thr = que_fork_start_command(
-				static_cast<que_fork_t*>(parent));
-
-			ut_a(run_thr == thr);
-
-			/* There was a lock wait but the thread was not
-			in a ready to run or running state. */
-			trx->error_state = DB_LOCK_WAIT;
-
+		if (row_mysql_handle_errors(&err, trx, thr, NULL)) {
 			goto run_again;
 		}
 	}
@@ -3782,7 +3760,8 @@ row_truncate_table_for_mysql(
 
 	row_mysql_unlock_data_dictionary(trx);
 
-	dict_stats_update(table, DICT_STATS_EMPTY_TABLE);
+	dict_stats_update(table, DICT_STATS_EMPTY_TABLE, trx);
+	trx_commit_for_mysql(trx);
 
 	trx->op_info = "";
 
@@ -3970,17 +3949,6 @@ row_drop_table_for_mysql(
 	if (!dict_table_is_temporary(table)) {
 
 		dict_stats_recalc_pool_del(table);
-
-		/* Remove stats for this table and all of its indexes from the
-		persistent storage if it exists and if there are stats for this
-		table in there. This function creates its own trx and commits
-		it. */
-		char	errstr[1024];
-		err = dict_stats_drop_table(name, errstr, sizeof(errstr));
-
-		if (err != DB_SUCCESS) {
-			ib_logf(IB_LOG_LEVEL_WARN, "%s", errstr);
-		}
 	}
 
 	/* Move the table the the non-LRU list so that it isn't
@@ -4282,14 +4250,20 @@ row_drop_table_for_mysql(
 		ibool	is_temp;
 
 	case DB_SUCCESS:
-		/* Clone the name, in case it has been allocated
-		from table->heap, which will be freed by
-		dict_table_remove_from_cache(table) below. */
 		space_id = table->space;
 		ibd_file_missing = table->ibd_file_missing;
 
 		is_temp = DICT_TF2_FLAG_IS_SET(table, DICT_TF2_TEMPORARY);
 
+		if (!is_temp) {
+			char	errstr[1024];
+			if (dict_stats_drop_table(name, errstr, sizeof errstr,
+						  trx)
+			    != DB_SUCCESS) {
+				ib_logf(IB_LOG_LEVEL_WARN, "%s", errstr);
+			}
+		}
+
 		/* If there is a temp path then the temp flag is set.
 		However, during recovery or reloading the table object
 		after eviction from data dictionary cache, we might
diff --git a/storage/xtradb/dict/dict0stats.cc b/storage/xtradb/dict/dict0stats.cc
index 0063b6b9ed4..d26fa96d377 100644
--- a/storage/xtradb/dict/dict0stats.cc
+++ b/storage/xtradb/dict/dict0stats.cc
@@ -1,6 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 2009, 2017, Oracle and/or its affiliates. All Rights Reserved.
+Copyright (c) 2017, 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
@@ -287,9 +288,7 @@ This function will free the pinfo object.
 @param[in,out]	pinfo	pinfo to pass to que_eval_sql() must already
 have any literals bound to it
 @param[in]	sql	SQL string to execute
-@param[in,out]	trx	in case of NULL the function will allocate and
-free the trx object. If it is not NULL then it will be rolled back
-only in the case of error, but not freed.
+@param[in,out]	trx	transaction
 @return DB_SUCCESS or error code */
 static
 dberr_t
@@ -299,49 +298,17 @@ dict_stats_exec_sql(
 	trx_t*		trx)
 {
 	dberr_t	err;
-	bool	trx_started = false;
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
 
 	if (!dict_stats_persistent_storage_check(true)) {
 		pars_info_free(pinfo);
 		return(DB_STATS_DO_NOT_EXIST);
 	}
 
-	if (trx == NULL) {
-		trx = trx_allocate_for_background();
-		trx_start_if_not_started(trx);
-		trx_started = true;
-	}
-
 	err = que_eval_sql(pinfo, sql, FALSE, trx); /* pinfo is freed here */
 
 	DBUG_EXECUTE_IF("stats_index_error",
-		if (!trx_started) {
 			err = DB_STATS_DO_NOT_EXIST;
-			trx->error_state = DB_STATS_DO_NOT_EXIST;
-		});
-
-	if (!trx_started && err == DB_SUCCESS) {
-		return(DB_SUCCESS);
-	}
-
-	if (err == DB_SUCCESS) {
-		trx_commit_for_mysql(trx);
-	} else {
-		trx->op_info = "rollback of internal trx on stats tables";
-		trx->dict_operation_lock_mode = RW_X_LATCH;
-		trx_rollback_to_savepoint(trx, NULL);
-		trx->dict_operation_lock_mode = 0;
-		trx->op_info = "";
-		ut_a(trx->error_state == DB_SUCCESS);
-	}
-
-	if (trx_started) {
-		trx_free_for_background(trx);
-	}
+			trx->error_state = DB_STATS_DO_NOT_EXIST;);
 
 	return(err);
 }
@@ -2308,11 +2275,6 @@ dict_stats_save_index_stat(
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
 	dict_fs2utf8(index->table->name, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
@@ -2338,6 +2300,8 @@ dict_stats_save_index_stat(
 	pars_info_add_str_literal(pinfo, "stat_description",
 				  stat_description);
 
+	mutex_enter(&dict_sys->mutex);
+
 	ret = dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE INDEX_STATS_SAVE () IS\n"
@@ -2364,6 +2328,8 @@ dict_stats_save_index_stat(
 		");\n"
 		"END;", trx);
 
+	mutex_exit(&dict_sys->mutex);
+
 	if (ret != DB_SUCCESS) {
 		if (innodb_index_stats_not_found == false &&
 		    index->stats_error_printed == false) {
@@ -2387,6 +2353,7 @@ dict_stats_save_index_stat(
 
 /** Save the table's statistics into the persistent statistics storage.
 @param[in] table_orig	table whose stats to save
+@param[in,out]	trx	transaction
 @param[in] only_for_index if this is non-NULL, then stats for indexes
 that are not equal to it will not be saved, if NULL, then all
 indexes' stats are saved
@@ -2394,9 +2361,9 @@ indexes' stats are saved
 static
 dberr_t
 dict_stats_save(
-/*============*/
 	dict_table_t*		table_orig,
-	const index_id_t*	only_for_index)
+	trx_t*			trx,
+	const index_id_t*	only_for_index = NULL)
 {
 	pars_info_t*	pinfo;
 	lint		now;
@@ -2410,9 +2377,6 @@ dict_stats_save(
 	dict_fs2utf8(table->name, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
-	rw_lock_x_lock(&dict_operation_lock);
-	mutex_enter(&dict_sys->mutex);
-
 	/* MySQL's timestamp is 4 byte, so we use
 	pars_info_add_int4_literal() which takes a lint arg, so "now" is
 	lint */
@@ -2429,6 +2393,8 @@ dict_stats_save(
 	pars_info_add_ull_literal(pinfo, "sum_of_other_index_sizes",
 		table->stat_sum_of_other_index_sizes);
 
+	mutex_enter(&dict_sys->mutex);
+
 	ret = dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE TABLE_STATS_SAVE () IS\n"
@@ -2449,31 +2415,16 @@ dict_stats_save(
 		":clustered_index_size,\n"
 		":sum_of_other_index_sizes\n"
 		");\n"
-		"END;", NULL);
-
-	if (ret != DB_SUCCESS) {
-		char	buf[MAX_FULL_NAME_LEN];
-		ut_print_timestamp(stderr);
-		fprintf(stderr,
-			" InnoDB: Cannot save table statistics for table "
-			"%s: %s\n",
-			ut_format_name(table->name, TRUE, buf, sizeof(buf)),
-			ut_strerr(ret));
+		"END;", trx);
 
-		mutex_exit(&dict_sys->mutex);
-		rw_lock_x_unlock(&dict_operation_lock);
+	mutex_exit(&dict_sys->mutex);
 
-		dict_stats_snapshot_free(table);
+	index_map_t	indexes;
 
-		return(ret);
+	if (ret != DB_SUCCESS) {
+		goto end;
 	}
 
-	trx_t*	trx = trx_allocate_for_background();
-	trx_start_if_not_started(trx);
-
-	dict_index_t*	index;
-	index_map_t	indexes;
-
 	/* Below we do all the modifications in innodb_index_stats in a single
 	transaction for performance reasons. Modifying more than one row in a
 	single transaction may deadlock with other transactions if they
@@ -2486,18 +2437,17 @@ dict_stats_save(
 	stat_name). This is why below we sort the indexes by name and then
 	for each index, do the mods ordered by stat_name. */
 
-	for (index = dict_table_get_first_index(table);
+	for (dict_index_t* index = dict_table_get_first_index(table);
 	     index != NULL;
 	     index = dict_table_get_next_index(index)) {
 
 		indexes[index->name] = index;
 	}
 
-	index_map_t::const_iterator	it;
-
-	for (it = indexes.begin(); it != indexes.end(); ++it) {
+	for (index_map_t::const_iterator it = indexes.begin();
+	     it != indexes.end(); ++it) {
 
-		index = it->second;
+		dict_index_t* index = it->second;
 
 		if (only_for_index != NULL && index->id != *only_for_index) {
 			continue;
@@ -2562,13 +2512,14 @@ dict_stats_save(
 		}
 	}
 
-	trx_commit_for_mysql(trx);
-
+	if (ret != DB_SUCCESS) {
+		char	buf[MAX_FULL_NAME_LEN];
 end:
-	trx_free_for_background(trx);
-
-	mutex_exit(&dict_sys->mutex);
-	rw_lock_x_unlock(&dict_operation_lock);
+		ib_logf(IB_LOG_LEVEL_ERROR,
+			"Cannot save table statistics for table %s: %s\n",
+			ut_format_name(table->name, TRUE, buf, sizeof(buf)),
+			ut_strerr(ret));
+	}
 
 	dict_stats_snapshot_free(table);
 
@@ -3045,13 +2996,13 @@ dict_stats_fetch_from_ps(
 	return(ret);
 }
 
-/*********************************************************************//**
-Fetches or calculates new estimates for index statistics. */
+/** Calculate index statistics.
+@param[in,out]	index	index tree
+@param[in,out]	trx	transaction (for persistent statistics)
+@return DB_SUCCESS or error code */
 UNIV_INTERN
-void
-dict_stats_update_for_index(
-/*========================*/
-	dict_index_t*	index)	/*!< in/out: index */
+dberr_t
+dict_stats_update_for_index(dict_index_t* index, trx_t* trx)
 {
 	DBUG_ENTER("dict_stats_update_for_index");
 
@@ -3063,8 +3014,8 @@ dict_stats_update_for_index(
 			dict_table_stats_lock(index->table, RW_X_LATCH);
 			dict_stats_analyze_index(index);
 			dict_table_stats_unlock(index->table, RW_X_LATCH);
-			dict_stats_save(index->table, &index->id);
-			DBUG_VOID_RETURN;
+			DBUG_RETURN(dict_stats_save(index->table, trx,
+						    &index->id));
 		}
 		/* else */
 
@@ -3092,23 +3043,20 @@ dict_stats_update_for_index(
 	dict_stats_update_transient_for_index(index);
 	dict_table_stats_unlock(index->table, RW_X_LATCH);
 
-	DBUG_VOID_RETURN;
+	DBUG_RETURN(DB_SUCCESS);
 }
 
-/*********************************************************************//**
-Calculates new estimates for table and index statistics. The statistics
-are used in query optimization.
-@return DB_SUCCESS or error code */
+/** Calculate new estimates for table and index statistics.
+@param[in,out]	table			table
+@param[in]	stats_upd_option	how to update statistics
+@param[in,out]	trx			transaction
+@return DB_* error code or DB_SUCCESS */
 UNIV_INTERN
 dberr_t
 dict_stats_update(
-/*==============*/
-	dict_table_t*		table,	/*!< in/out: table */
-	dict_stats_upd_option_t	stats_upd_option)
-					/*!< in: whether to (re) calc
-					the stats or to fetch them from
-					the persistent statistics
-					storage */
+	dict_table_t*		table,
+	dict_stats_upd_option_t	stats_upd_option,
+	trx_t*			trx)
 {
 	char			buf[MAX_FULL_NAME_LEN];
 
@@ -3162,7 +3110,7 @@ dict_stats_update(
 				return(err);
 			}
 
-			err = dict_stats_save(table, NULL);
+			err = dict_stats_save(table, trx);
 
 			return(err);
 		}
@@ -3199,7 +3147,7 @@ dict_stats_update(
 
 			if (dict_stats_persistent_storage_check(false)) {
 
-				return(dict_stats_save(table, NULL));
+				return(dict_stats_save(table, trx));
 			}
 
 			return(DB_STATS_DO_NOT_EXIST);
@@ -3280,9 +3228,9 @@ dict_stats_update(
 			}
 
 			if (dict_stats_auto_recalc_is_enabled(table)) {
-				return(dict_stats_update(
-						table,
-						DICT_STATS_RECALC_PERSISTENT));
+				return dict_stats_update(
+					table, DICT_STATS_RECALC_PERSISTENT,
+					trx);
 			}
 
 			ut_format_name(table->name, TRUE, buf, sizeof(buf));
@@ -3336,26 +3284,21 @@ dict_stats_update(
 	return(DB_SUCCESS);
 }
 
-/*********************************************************************//**
-Removes the information for a particular index's stats from the persistent
-storage if it exists and if there is data stored for this index.
-This function creates its own trx and commits it.
-A note from Marko why we cannot edit user and sys_* tables in one trx:
-marko: The problem is that ibuf merges should be disabled while we are
-rolling back dict transactions.
-marko: If ibuf merges are not disabled, we need to scan the *.ibd files.
-But we shouldn't open *.ibd files before we have rolled back dict
-transactions and opened the SYS_* records for the *.ibd files.
+/** Remove the persistent statistics for an index.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[in]	iname		index name
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_index(
-/*==================*/
-	const char*	db_and_table,/*!< in: db and table, e.g. 'db/table' */
-	const char*	iname,	/*!< in: index name */
-	char*		errstr, /*!< out: error message if != DB_SUCCESS
-				is returned */
-	ulint		errstr_sz)/*!< in: size of the errstr buffer */
+	const char*	db_and_table,
+	const char*	iname,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx)
 {
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
@@ -3382,7 +3325,6 @@ dict_stats_drop_index(
 
 	pars_info_add_str_literal(pinfo, "index_name", iname);
 
-	rw_lock_x_lock(&dict_operation_lock);
 	mutex_enter(&dict_sys->mutex);
 
 	ret = dict_stats_exec_sql(
@@ -3393,14 +3335,9 @@ dict_stats_drop_index(
 		"database_name = :database_name AND\n"
 		"table_name = :table_name AND\n"
 		"index_name = :index_name;\n"
-		"END;\n", NULL);
+		"END;\n", trx);
 
 	mutex_exit(&dict_sys->mutex);
-	rw_lock_x_unlock(&dict_operation_lock);
-
-	if (ret == DB_STATS_DO_NOT_EXIST) {
-		ret = DB_SUCCESS;
-	}
 
 	if (ret != DB_SUCCESS) {
 		ut_snprintf(errstr, errstr_sz,
@@ -3428,105 +3365,72 @@ dict_stats_drop_index(
 	return(ret);
 }
 
-/*********************************************************************//**
-Executes
-DELETE FROM mysql.innodb_table_stats
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Delete table statistics.
+@param[in]	db	schema name
+@param[in]	t	table name
+@param[in,out]	trx	transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
-dict_stats_delete_from_table_stats(
-/*===============================*/
-	const char*	database_name,	/*!< in: database name, e.g. 'db' */
-	const char*	table_name)	/*!< in: table name, e.g. 'table' */
+dict_stats_delete_from_table_stats(const char* db, const char* t, trx_t* trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
+	pars_info_t*	pinfo = pars_info_create();
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
+	pars_info_add_str_literal(pinfo, "database_name", db);
+	pars_info_add_str_literal(pinfo, "table_name", t);
 
-	pinfo = pars_info_create();
-
-	pars_info_add_str_literal(pinfo, "database_name", database_name);
-	pars_info_add_str_literal(pinfo, "table_name", table_name);
-
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE DELETE_FROM_TABLE_STATS () IS\n"
 		"BEGIN\n"
 		"DELETE FROM \"" TABLE_STATS_NAME "\" WHERE\n"
 		"database_name = :database_name AND\n"
 		"table_name = :table_name;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Executes
-DELETE FROM mysql.innodb_index_stats
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Delete index statistics.
+@param[in]	db	schema name
+@param[in]	t	table name
+@param[in,out]	trx	transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
-dict_stats_delete_from_index_stats(
-/*===============================*/
-	const char*	database_name,	/*!< in: database name, e.g. 'db' */
-	const char*	table_name)	/*!< in: table name, e.g. 'table' */
+dict_stats_delete_from_index_stats(const char* db, const char* t, trx_t* trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
-
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
-	pinfo = pars_info_create();
+	pars_info_t*	pinfo = pars_info_create();
 
-	pars_info_add_str_literal(pinfo, "database_name", database_name);
-	pars_info_add_str_literal(pinfo, "table_name", table_name);
+	pars_info_add_str_literal(pinfo, "database_name", db);
+	pars_info_add_str_literal(pinfo, "table_name", t);
 
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE DELETE_FROM_INDEX_STATS () IS\n"
 		"BEGIN\n"
 		"DELETE FROM \"" INDEX_STATS_NAME "\" WHERE\n"
 		"database_name = :database_name AND\n"
 		"table_name = :table_name;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Removes the statistics for a table and all of its indexes from the
-persistent statistics storage if it exists and if there is data stored for
-the table. This function creates its own transaction and commits it.
+/** Remove the persistent statistics for a table and all of its indexes.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_table(
-/*==================*/
-	const char*	db_and_table,	/*!< in: db and table, e.g. 'db/table' */
-	char*		errstr,		/*!< out: error message
-					if != DB_SUCCESS is returned */
-	ulint		errstr_sz)	/*!< in: size of errstr buffer */
+	const char*	db_and_table,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx)
 {
 	char		db_utf8[MAX_DB_UTF8_LEN];
 	char		table_utf8[MAX_TABLE_UTF8_LEN];
 	dberr_t		ret;
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
 	/* skip tables that do not contain a database name
 	e.g. if we are dropping SYS_TABLES */
 	if (strchr(db_and_table, '/') == NULL) {
@@ -3544,18 +3448,21 @@ dict_stats_drop_table(
 	dict_fs2utf8(db_and_table, db_utf8, sizeof(db_utf8),
 		     table_utf8, sizeof(table_utf8));
 
-	ret = dict_stats_delete_from_table_stats(db_utf8, table_utf8);
+	ret = dict_stats_delete_from_table_stats(db_utf8, table_utf8, trx);
 
 	if (ret == DB_SUCCESS) {
-		ret = dict_stats_delete_from_index_stats(db_utf8, table_utf8);
+		ret = dict_stats_delete_from_index_stats(
+			db_utf8, table_utf8, trx);
 	}
 
-	if (ret == DB_STATS_DO_NOT_EXIST) {
-		ret = DB_SUCCESS;
-	}
-
-	if (ret != DB_SUCCESS) {
-
+	switch (ret) {
+	case DB_SUCCESS:
+	case DB_STATS_DO_NOT_EXIST:
+		return(DB_SUCCESS);
+	case DB_QUE_THR_SUSPENDED:
+		ret = DB_LOCK_WAIT;
+		/* fall through */
+	default:
 		ut_snprintf(errstr, errstr_sz,
 			    "Unable to delete statistics for table %s.%s: %s. "
 			    "They can be deleted later using "
@@ -3581,38 +3488,30 @@ dict_stats_drop_table(
 	return(ret);
 }
 
-/*********************************************************************//**
-Executes
-UPDATE mysql.innodb_table_stats SET
-database_name = '...', table_name = '...'
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Rename table statistics.
+@param[in]	old_dbname_utf8		old schema name
+@param[in]	old_tablename_utf8	old table name
+@param[in]	new_dbname_utf8		new schema name
+@param[in]	new_tablename_utf8	new schema name
+@param[in,out]	trx			transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
 dict_stats_rename_in_table_stats(
-/*=============================*/
-	const char*	old_dbname_utf8,/*!< in: database name, e.g. 'olddb' */
-	const char*	old_tablename_utf8,/*!< in: table name, e.g. 'oldtable' */
-	const char*	new_dbname_utf8,/*!< in: database name, e.g. 'newdb' */
-	const char*	new_tablename_utf8)/*!< in: table name, e.g. 'newtable' */
+	const char*	old_dbname_utf8,
+	const char*	old_tablename_utf8,
+	const char*	new_dbname_utf8,
+	const char*	new_tablename_utf8,
+	trx_t*		trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
-
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
-	pinfo = pars_info_create();
+	pars_info_t*	pinfo = pars_info_create();
 
 	pars_info_add_str_literal(pinfo, "old_dbname_utf8", old_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "old_tablename_utf8", old_tablename_utf8);
 	pars_info_add_str_literal(pinfo, "new_dbname_utf8", new_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "new_tablename_utf8", new_tablename_utf8);
 
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE RENAME_IN_TABLE_STATS () IS\n"
 		"BEGIN\n"
@@ -3622,43 +3521,33 @@ dict_stats_rename_in_table_stats(
 		"WHERE\n"
 		"database_name = :old_dbname_utf8 AND\n"
 		"table_name = :old_tablename_utf8;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Executes
-UPDATE mysql.innodb_index_stats SET
-database_name = '...', table_name = '...'
-WHERE database_name = '...' AND table_name = '...';
-Creates its own transaction and commits it.
+/** Rename index statistics.
+@param[in]	old_dbname_utf8		old schema name
+@param[in]	old_tablename_utf8	old table name
+@param[in]	new_dbname_utf8		new schema name
+@param[in]	new_tablename_utf8	new schema name
+@param[in,out]	trx			transaction
 @return DB_SUCCESS or error code */
 UNIV_INLINE
 dberr_t
 dict_stats_rename_in_index_stats(
-/*=============================*/
-	const char*	old_dbname_utf8,/*!< in: database name, e.g. 'olddb' */
-	const char*	old_tablename_utf8,/*!< in: table name, e.g. 'oldtable' */
-	const char*	new_dbname_utf8,/*!< in: database name, e.g. 'newdb' */
-	const char*	new_tablename_utf8)/*!< in: table name, e.g. 'newtable' */
+	const char*	old_dbname_utf8,
+	const char*	old_tablename_utf8,
+	const char*	new_dbname_utf8,
+	const char*	new_tablename_utf8,
+	trx_t*		trx)
 {
-	pars_info_t*	pinfo;
-	dberr_t		ret;
-
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(mutex_own(&dict_sys->mutex));
-
-	pinfo = pars_info_create();
+	pars_info_t*	pinfo = pars_info_create();
 
 	pars_info_add_str_literal(pinfo, "old_dbname_utf8", old_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "old_tablename_utf8", old_tablename_utf8);
 	pars_info_add_str_literal(pinfo, "new_dbname_utf8", new_dbname_utf8);
 	pars_info_add_str_literal(pinfo, "new_tablename_utf8", new_tablename_utf8);
 
-	ret = dict_stats_exec_sql(
+	return dict_stats_exec_sql(
 		pinfo,
 		"PROCEDURE RENAME_IN_INDEX_STATS () IS\n"
 		"BEGIN\n"
@@ -3668,24 +3557,24 @@ dict_stats_rename_in_index_stats(
 		"WHERE\n"
 		"database_name = :old_dbname_utf8 AND\n"
 		"table_name = :old_tablename_utf8;\n"
-		"END;\n", NULL);
-
-	return(ret);
+		"END;\n", trx);
 }
 
-/*********************************************************************//**
-Renames a table in InnoDB persistent stats storage.
-This function creates its own transaction and commits it.
+/** Rename a table in the InnoDB persistent statistics storage.
+@param[in]	old_name	old schema and table name, e.g., 'db/table'
+@param[in]	new_name	new schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_rename_table(
-/*====================*/
-	const char*	old_name,	/*!< in: old name, e.g. 'db/table' */
-	const char*	new_name,	/*!< in: new name, e.g. 'db/table' */
-	char*		errstr,		/*!< out: error string if != DB_SUCCESS
-					is returned */
-	size_t		errstr_sz)	/*!< in: errstr size */
+	const char*	old_name,
+	const char*	new_name,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx)
 {
 	char		old_db_utf8[MAX_DB_UTF8_LEN];
 	char		new_db_utf8[MAX_DB_UTF8_LEN];
@@ -3693,11 +3582,6 @@ dict_stats_rename_table(
 	char		new_table_utf8[MAX_TABLE_UTF8_LEN];
 	dberr_t		ret;
 
-#ifdef UNIV_SYNC_DEBUG
-	ut_ad(!rw_lock_own(&dict_operation_lock, RW_LOCK_EX));
-#endif /* UNIV_SYNC_DEBUG */
-	ut_ad(!mutex_own(&dict_sys->mutex));
-
 	/* skip innodb_table_stats and innodb_index_stats themselves */
 	if (strcmp(old_name, TABLE_STATS_NAME) == 0
 	    || strcmp(old_name, INDEX_STATS_NAME) == 0
@@ -3713,98 +3597,86 @@ dict_stats_rename_table(
 	dict_fs2utf8(new_name, new_db_utf8, sizeof(new_db_utf8),
 		     new_table_utf8, sizeof(new_table_utf8));
 
-	rw_lock_x_lock(&dict_operation_lock);
-	mutex_enter(&dict_sys->mutex);
-
 	ulint	n_attempts = 0;
 	do {
 		n_attempts++;
 
+		trx_savept_t savept = trx_savept_take(trx);
+
+		mutex_enter(&dict_sys->mutex);
+
 		ret = dict_stats_rename_in_table_stats(
 			old_db_utf8, old_table_utf8,
-			new_db_utf8, new_table_utf8);
+			new_db_utf8, new_table_utf8, trx);
+
+		mutex_exit(&dict_sys->mutex);
 
 		if (ret == DB_DUPLICATE_KEY) {
+			trx_rollback_to_savepoint(trx, &savept);
+			mutex_enter(&dict_sys->mutex);
 			dict_stats_delete_from_table_stats(
-				new_db_utf8, new_table_utf8);
+				new_db_utf8, new_table_utf8, trx);
+			mutex_exit(&dict_sys->mutex);
 		}
 
 		if (ret == DB_STATS_DO_NOT_EXIST) {
 			ret = DB_SUCCESS;
+			break;
 		}
 
 		if (ret != DB_SUCCESS) {
-			mutex_exit(&dict_sys->mutex);
-			rw_lock_x_unlock(&dict_operation_lock);
 			os_thread_sleep(200000 /* 0.2 sec */);
-			rw_lock_x_lock(&dict_operation_lock);
-			mutex_enter(&dict_sys->mutex);
 		}
 	} while ((ret == DB_DEADLOCK
 		  || ret == DB_DUPLICATE_KEY
 		  || ret == DB_LOCK_WAIT_TIMEOUT)
 		 && n_attempts < 5);
 
-	if (ret != DB_SUCCESS) {
-		ut_snprintf(errstr, errstr_sz,
-			    "Unable to rename statistics from "
-			    "%s.%s to %s.%s in %s: %s. "
-			    "They can be renamed later using "
-
-			    "UPDATE %s SET "
-			    "database_name = '%s', "
-			    "table_name = '%s' "
-			    "WHERE "
-			    "database_name = '%s' AND "
-			    "table_name = '%s';",
-
-			    old_db_utf8, old_table_utf8,
-			    new_db_utf8, new_table_utf8,
-			    TABLE_STATS_NAME_PRINT,
-			    ut_strerr(ret),
+	const char* table_name = TABLE_STATS_NAME_PRINT;
 
-			    TABLE_STATS_NAME_PRINT,
-			    new_db_utf8, new_table_utf8,
-			    old_db_utf8, old_table_utf8);
-		mutex_exit(&dict_sys->mutex);
-		rw_lock_x_unlock(&dict_operation_lock);
-		return(ret);
+	if (ret != DB_SUCCESS) {
+		goto err_exit;
 	}
-	/* else */
+
+	table_name = INDEX_STATS_NAME_PRINT;
 
 	n_attempts = 0;
 	do {
 		n_attempts++;
 
+		trx_savept_t savept = trx_savept_take(trx);
+
+		mutex_enter(&dict_sys->mutex);
+
 		ret = dict_stats_rename_in_index_stats(
 			old_db_utf8, old_table_utf8,
-			new_db_utf8, new_table_utf8);
+			new_db_utf8, new_table_utf8, trx);
+
+		mutex_exit(&dict_sys->mutex);
 
 		if (ret == DB_DUPLICATE_KEY) {
+			trx_rollback_to_savepoint(trx, &savept);
+			mutex_enter(&dict_sys->mutex);
 			dict_stats_delete_from_index_stats(
-				new_db_utf8, new_table_utf8);
+				new_db_utf8, new_table_utf8, trx);
+			mutex_exit(&dict_sys->mutex);
 		}
 
 		if (ret == DB_STATS_DO_NOT_EXIST) {
 			ret = DB_SUCCESS;
+			break;
 		}
 
 		if (ret != DB_SUCCESS) {
-			mutex_exit(&dict_sys->mutex);
-			rw_lock_x_unlock(&dict_operation_lock);
 			os_thread_sleep(200000 /* 0.2 sec */);
-			rw_lock_x_lock(&dict_operation_lock);
-			mutex_enter(&dict_sys->mutex);
 		}
 	} while ((ret == DB_DEADLOCK
 		  || ret == DB_DUPLICATE_KEY
 		  || ret == DB_LOCK_WAIT_TIMEOUT)
 		 && n_attempts < 5);
 
-	mutex_exit(&dict_sys->mutex);
-	rw_lock_x_unlock(&dict_operation_lock);
-
 	if (ret != DB_SUCCESS) {
+err_exit:
 		ut_snprintf(errstr, errstr_sz,
 			    "Unable to rename statistics from "
 			    "%s.%s to %s.%s in %s: %s. "
@@ -3819,10 +3691,10 @@ dict_stats_rename_table(
 
 			    old_db_utf8, old_table_utf8,
 			    new_db_utf8, new_table_utf8,
-			    INDEX_STATS_NAME_PRINT,
+			    table_name,
 			    ut_strerr(ret),
 
-			    INDEX_STATS_NAME_PRINT,
+			    table_name,
 			    new_db_utf8, new_table_utf8,
 			    old_db_utf8, old_table_utf8);
 	}
diff --git a/storage/xtradb/dict/dict0stats_bg.cc b/storage/xtradb/dict/dict0stats_bg.cc
index 4d646ed3081..8825c5546ec 100644
--- a/storage/xtradb/dict/dict0stats_bg.cc
+++ b/storage/xtradb/dict/dict0stats_bg.cc
@@ -266,8 +266,7 @@ Get the first table that has been added for auto recalc and eventually
 update its stats. */
 static
 void
-dict_stats_process_entry_from_recalc_pool()
-/*=======================================*/
+dict_stats_process_entry_from_recalc_pool(trx_t* trx)
 {
 	table_id_t	table_id;
 
@@ -320,8 +319,10 @@ dict_stats_process_entry_from_recalc_pool()
 		dict_stats_recalc_pool_add(table);
 
 	} else {
-
-		dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT);
+		dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT, trx);
+		if (trx->state != TRX_STATE_NOT_STARTED) {
+			trx_commit_for_mysql(trx);
+		}
 	}
 
 	mutex_enter(&dict_sys->mutex);
@@ -348,6 +349,8 @@ DECLARE_THREAD(dict_stats_thread)(
 	my_thread_init();
 	ut_a(!srv_read_only_mode);
 
+	trx_t* trx = trx_allocate_for_background();
+
 	while (!dict_stats_start_shutdown) {
 
 		/* Wake up periodically even if not signaled. This is
@@ -362,11 +365,12 @@ DECLARE_THREAD(dict_stats_thread)(
 			break;
 		}
 
-		dict_stats_process_entry_from_recalc_pool();
+		dict_stats_process_entry_from_recalc_pool(trx);
 
 		os_event_reset(dict_stats_event);
 	}
 
+	trx_free_for_background(trx);
 	srv_dict_stats_thread_active = false;
 
 	os_event_set(dict_stats_shutdown_event);
diff --git a/storage/xtradb/handler/ha_innodb.cc b/storage/xtradb/handler/ha_innodb.cc
index f111a576d8f..249b42aea91 100644
--- a/storage/xtradb/handler/ha_innodb.cc
+++ b/storage/xtradb/handler/ha_innodb.cc
@@ -6103,7 +6103,19 @@ ha_innobase::open(
 
 	innobase_copy_frm_flags_from_table_share(ib_table, table->s);
 
-	dict_stats_init(ib_table);
+	{
+		trx_t* trx = check_trx_exists(thd);
+		bool alloc = !trx_state_eq(trx, TRX_STATE_NOT_STARTED);
+
+		if (alloc) {
+			trx = trx_allocate_for_background();
+		}
+		dict_stats_init(ib_table, trx);
+		innobase_commit_low(trx);
+		if (alloc) {
+			trx_free_for_background(trx);
+		}
+	}
 
 	MONITOR_INC(MONITOR_TABLE_OPEN);
 
@@ -11199,7 +11211,9 @@ ha_innobase::create(
 
 	innobase_copy_frm_flags_from_create_info(innobase_table, create_info);
 
-	dict_stats_update(innobase_table, DICT_STATS_EMPTY_TABLE);
+	++trx->will_lock;
+	dict_stats_update(innobase_table, DICT_STATS_EMPTY_TABLE, trx);
+	innobase_commit_low(trx);
 
 	if (innobase_table) {
 		/* We update the highest file format in the system table
@@ -11377,7 +11391,8 @@ ha_innobase::discard_or_import_tablespace(
 
 		/* Adjust the persistent statistics. */
 		ret = dict_stats_update(dict_table,
-					DICT_STATS_RECALC_PERSISTENT);
+					DICT_STATS_RECALC_PERSISTENT,
+					prebuilt->trx);
 
 		if (ret != DB_SUCCESS) {
 			push_warning_printf(
@@ -11385,8 +11400,11 @@ ha_innobase::discard_or_import_tablespace(
 				Sql_condition::WARN_LEVEL_WARN,
 				ER_ALTER_INFO,
 				"Error updating stats for table '%s'"
-				" after table rebuild: %s",
+				" after table import: %s",
 				dict_table->name, ut_strerr(ret));
+			trx_rollback_to_savepoint(prebuilt->trx, NULL);
+		} else {
+			trx_commit_for_mysql(prebuilt->trx);
 		}
 	}
 
@@ -11828,7 +11846,6 @@ ha_innobase::rename_table(
 	DEBUG_SYNC(thd, "after_innobase_rename_table");
 
 	innobase_commit_low(trx);
-	trx_free_for_mysql(trx);
 
 	if (error == DB_SUCCESS) {
 		char	norm_from[MAX_FULL_NAME_LEN];
@@ -11839,18 +11856,24 @@ ha_innobase::rename_table(
 		normalize_table_name(norm_from, from);
 		normalize_table_name(norm_to, to);
 
+		++trx->will_lock;
 		ret = dict_stats_rename_table(norm_from, norm_to,
-					      errstr, sizeof(errstr));
+					      errstr, sizeof errstr, trx);
 
 		if (ret != DB_SUCCESS) {
+			trx_rollback_to_savepoint(trx, NULL);
 			ut_print_timestamp(stderr);
 			fprintf(stderr, " InnoDB: %s\n", errstr);
 
 			push_warning(thd, Sql_condition::WARN_LEVEL_WARN,
 				     ER_LOCK_WAIT_TIMEOUT, errstr);
+		} else {
+			innobase_commit_low(trx);
 		}
 	}
 
+	trx_free_for_mysql(trx);
+
 	/* Add a special case to handle the Duplicated Key error
 	and return DB_ERROR instead.
 	This is to avoid a possible SIGSEGV error from mysql error
@@ -12378,17 +12401,30 @@ ha_innobase::info_low(
 			}
 
 			ut_ad(!mutex_own(&dict_sys->mutex));
-			ret = dict_stats_update(ib_table, opt);
+			/* Do not use prebuilt->trx in case this is
+			called in the middle of a transaction. We
+			should commit the transaction after
+			dict_stats_update() in order not to hog locks
+			on the mysql.innodb_table_stats,
+			mysql.innodb_index_stats tables. */
+			trx_t* trx = trx_allocate_for_background();
+			ret = dict_stats_update(ib_table, opt, trx);
 
 			if (ret != DB_SUCCESS) {
 				prebuilt->trx->op_info = "";
-				DBUG_RETURN(HA_ERR_GENERIC);
+				trx_rollback_to_savepoint(trx, NULL);
+			} else {
+				prebuilt->trx->op_info =
+					"returning various info to MySQL";
+				trx_commit_for_mysql(trx);
 			}
 
-			prebuilt->trx->op_info =
-				"returning various info to MySQL";
-		}
+			trx_free_for_background(trx);
 
+			if (ret != DB_SUCCESS) {
+				DBUG_RETURN(HA_ERR_GENERIC);
+			}
+		}
 	}
 
 	if (flag & HA_STATUS_VARIABLE) {
@@ -12637,7 +12673,10 @@ ha_innobase::info_low(
 
 					/* This is better than
 					assert on below function */
-					dict_stats_init(index->table);
+					dict_stats_update(
+						index->table,
+						DICT_STATS_RECALC_TRANSIENT,
+						NULL);
 				}
 
 				rec_per_key = innodb_rec_per_key(
diff --git a/storage/xtradb/handler/handler0alter.cc b/storage/xtradb/handler/handler0alter.cc
index 6d2a6e6ec66..78446bf17ad 100644
--- a/storage/xtradb/handler/handler0alter.cc
+++ b/storage/xtradb/handler/handler0alter.cc
@@ -5499,27 +5499,25 @@ 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 altered_table	MySQL table that is being altered
 @param table_name	Table name in MySQL
-@param thd		MySQL connection
-*/
+@param trx		transaction
+@return error code */
 static
-void
+dberr_t
 alter_stats_norebuild(
-/*==================*/
 	Alter_inplace_info*		ha_alter_info,
 	ha_innobase_inplace_ctx*	ctx,
-	TABLE*				altered_table,
 	const char*			table_name,
-	THD*				thd)
+	trx_t*				trx)
 {
+	dberr_t	err = DB_SUCCESS;
 	ulint	i;
 
 	DBUG_ENTER("alter_stats_norebuild");
 	DBUG_ASSERT(!ctx->need_rebuild());
 
 	if (!dict_stats_is_persistent_enabled(ctx->new_table)) {
-		DBUG_VOID_RETURN;
+		DBUG_RETURN(err);
 	}
 
 	/* TODO: This will not drop the (unused) statistics for
@@ -5536,10 +5534,13 @@ alter_stats_norebuild(
 
 		char	errstr[1024];
 
-		if (dict_stats_drop_index(
-			    ctx->new_table->name, key->name,
-			    errstr, sizeof errstr) != DB_SUCCESS) {
-			push_warning(thd,
+		dberr_t err2 = dict_stats_drop_index(
+			ctx->new_table->name, key->name,
+			errstr, sizeof errstr, trx);
+
+		if (err2 != DB_SUCCESS) {
+			err = err2;
+			push_warning(trx->mysql_thd,
 				     Sql_condition::WARN_LEVEL_WARN,
 				     ER_LOCK_WAIT_TIMEOUT, errstr);
 		}
@@ -5550,12 +5551,15 @@ alter_stats_norebuild(
 		DBUG_ASSERT(index->table == ctx->new_table);
 
 		if (!(index->type & DICT_FTS)) {
-			dict_stats_init(ctx->new_table);
-			dict_stats_update_for_index(index);
+			dict_stats_init(ctx->new_table, trx);
+			dberr_t err2 = dict_stats_update_for_index(index, trx);
+			if (err2 != DB_SUCCESS) {
+				err = err2;
+			}
 		}
 	}
 
-	DBUG_VOID_RETURN;
+	DBUG_RETURN(err);
 }
 
 /** Adjust the persistent statistics after rebuilding ALTER TABLE.
@@ -5563,30 +5567,44 @@ 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
-*/
+@param trx		transaction
+@return error code */
 static
-void
+dberr_t
 alter_stats_rebuild(
-/*================*/
 	dict_table_t*	table,
 	const char*	table_name,
-	THD*		thd)
+	trx_t*		trx)
 {
 	DBUG_ENTER("alter_stats_rebuild");
 
 	if (dict_table_is_discarded(table)
 	    || !dict_stats_is_persistent_enabled(table)) {
-		DBUG_VOID_RETURN;
+		DBUG_RETURN(DB_SUCCESS);
 	}
 
-	dberr_t	ret;
+	char	errstr[1024];
+	mutex_enter(&dict_sys->mutex);
+	dberr_t	ret = dict_stats_drop_table(table->name,
+					    errstr, sizeof errstr, trx);
+	mutex_exit(&dict_sys->mutex);
+	if (ret != DB_SUCCESS) {
+		push_warning_printf(
+			trx->mysql_thd,
+			Sql_condition::WARN_LEVEL_WARN,
+			ER_ALTER_INFO,
+			"Deleting persistent statistics"
+			" for rebuilt table '%s' in"
+			" InnoDB failed: %s",
+			table_name, errstr);
+		DBUG_RETURN(ret);
+	}
 
-	ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT);
+	ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT, trx);
 
 	if (ret != DB_SUCCESS) {
 		push_warning_printf(
-			thd,
+			trx->mysql_thd,
 			Sql_condition::WARN_LEVEL_WARN,
 			ER_ALTER_INFO,
 			"Error updating stats for table '%s' "
@@ -5594,7 +5612,7 @@ alter_stats_rebuild(
 			table_name, ut_strerr(ret));
 	}
 
-	DBUG_VOID_RETURN;
+	DBUG_RETURN(ret);
 }
 
 #ifndef DBUG_OFF
@@ -6104,31 +6122,6 @@ ha_innobase::commit_inplace_alter_table(
 		ut_a(fts_check_cached_index(ctx->new_table));
 
 		if (new_clustered) {
-			/* Since the table has been rebuilt, we remove
-			all persistent statistics corresponding to the
-			old copy of the table (which was renamed to
-			ctx->tmp_name). */
-
-			char	errstr[1024];
-
-			DBUG_ASSERT(0 == strcmp(ctx->old_table->name,
-						ctx->tmp_name));
-
-			if (dict_stats_drop_table(
-				    ctx->new_table->name,
-				    errstr, sizeof(errstr))
-			    != DB_SUCCESS) {
-				push_warning_printf(
-					user_thd,
-					Sql_condition::WARN_LEVEL_WARN,
-					ER_ALTER_INFO,
-					"Deleting persistent statistics"
-					" for rebuilt table '%s' in"
-					" InnoDB failed: %s",
-					table->s->table_name.str,
-					errstr);
-			}
-
 			DBUG_EXECUTE_IF("ib_ddl_crash_before_commit",
 					DBUG_SUICIDE(););
 
@@ -6157,7 +6150,6 @@ ha_innobase::commit_inplace_alter_table(
 	}
 
 	row_mysql_unlock_data_dictionary(trx);
-	trx_free_for_mysql(trx);
 
 	/* Rebuild index translation table now for temporary tables if we are
 	restoring secondary keys, as ha_innobase::open will not be called for
@@ -6170,13 +6162,17 @@ ha_innobase::commit_inplace_alter_table(
 						      ctx0->new_table,
 						      share)) {
 			MONITOR_ATOMIC_DEC(MONITOR_PENDING_ALTER_TABLE);
+			trx_free_for_mysql(trx);
 			DBUG_RETURN(true);
 		}
 	}
 
+	++trx->will_lock;
 	/* TODO: The following code could be executed
 	while allowing concurrent access to the table
 	(MDL downgrade). */
+	trx->mysql_thd = user_thd;
+	dberr_t stats_err = DB_SUCCESS;
 
 	if (new_clustered) {
 		for (inplace_alter_handler_ctx** pctx = ctx_array;
@@ -6185,10 +6181,11 @@ ha_innobase::commit_inplace_alter_table(
 				= static_cast<ha_innobase_inplace_ctx*>
 				(*pctx);
 			DBUG_ASSERT(ctx->need_rebuild());
-
-			alter_stats_rebuild(
-				ctx->new_table, table->s->table_name.str,
-				user_thd);
+			stats_err = alter_stats_rebuild(
+				ctx->new_table, table->s->table_name.str, trx);
+			if (stats_err != DB_SUCCESS) {
+				break;
+			}
 			DBUG_INJECT_CRASH("ib_commit_inplace_crash",
 					  crash_inject_count++);
 		}
@@ -6200,14 +6197,25 @@ ha_innobase::commit_inplace_alter_table(
 				(*pctx);
 			DBUG_ASSERT(!ctx->need_rebuild());
 
-			alter_stats_norebuild(
-				ha_alter_info, ctx, altered_table,
-				table->s->table_name.str, user_thd);
+			stats_err = alter_stats_norebuild(
+				ha_alter_info, ctx,
+				table->s->table_name.str, trx);
+			if (stats_err != DB_SUCCESS) {
+				break;
+			}
 			DBUG_INJECT_CRASH("ib_commit_inplace_crash",
 					  crash_inject_count++);
 		}
 	}
 
+	if (stats_err != DB_SUCCESS) {
+		trx_rollback_to_savepoint(trx, NULL);
+	} else {
+		trx_commit_for_mysql(trx);
+	}
+
+	trx_free_for_mysql(trx);
+
 	/* TODO: Also perform DROP TABLE and DROP INDEX after
 	the MDL downgrade. */
 
diff --git a/storage/xtradb/include/dict0stats.h b/storage/xtradb/include/dict0stats.h
index 35ee1a00d8a..c6e8e114449 100644
--- a/storage/xtradb/include/dict0stats.h
+++ b/storage/xtradb/include/dict0stats.h
@@ -1,6 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 2009, 2016, Oracle and/or its affiliates. All Rights Reserved.
+Copyright (c) 2017, 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
@@ -111,13 +112,12 @@ dict_stats_auto_recalc_is_enabled(
 /*==============================*/
 	const dict_table_t*	table);	/*!< in: table */
 
-/*********************************************************************//**
-Initialize table's stats for the first time when opening a table. */
+/** Initialize table statistics for the first time when opening a table.
+@param[in,out]	table	freshly opened table
+@param[in,out]	trx	transaction */
 UNIV_INLINE
 void
-dict_stats_init(
-/*============*/
-	dict_table_t*	table);	/*!< in/out: table */
+dict_stats_init(dict_table_t* table, trx_t* trx);
 
 /*********************************************************************//**
 Deinitialize table's stats after the last close of the table. This is
@@ -129,71 +129,72 @@ dict_stats_deinit(
 	dict_table_t*	table)	/*!< in/out: table */
 	MY_ATTRIBUTE((nonnull));
 
-/*********************************************************************//**
-Calculates new estimates for table and index statistics. The statistics
-are used in query optimization.
+/** Calculate new estimates for table and index statistics.
+@param[in,out]	table			table
+@param[in]	stats_upd_option	how to update statistics
+@param[in,out]	trx			transaction
 @return DB_* error code or DB_SUCCESS */
 UNIV_INTERN
 dberr_t
 dict_stats_update(
-/*==============*/
-	dict_table_t*		table,	/*!< in/out: table */
-	dict_stats_upd_option_t	stats_upd_option);
-					/*!< in: whether to (re) calc
-					the stats or to fetch them from
-					the persistent storage */
-
-/*********************************************************************//**
-Removes the information for a particular index's stats from the persistent
-storage if it exists and if there is data stored for this index.
-This function creates its own trx and commits it.
+	dict_table_t*		table,
+	dict_stats_upd_option_t	stats_upd_option,
+	trx_t*			trx);
+
+/** Remove the persistent statistics for an index.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[in]	iname		index name
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_index(
-/*==================*/
-	const char*	tname,	/*!< in: table name */
-	const char*	iname,	/*!< in: index name */
-	char*		errstr, /*!< out: error message if != DB_SUCCESS
-				is returned */
-	ulint		errstr_sz);/*!< in: size of the errstr buffer */
-
-/*********************************************************************//**
-Removes the statistics for a table and all of its indexes from the
-persistent storage if it exists and if there is data stored for the table.
-This function creates its own transaction and commits it.
+	const char*	db_and_table,
+	const char*	iname,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx);
+
+/** Remove the persistent statistics for a table and all of its indexes.
+@param[in]	db_and_table	schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_drop_table(
-/*==================*/
-	const char*	table_name,	/*!< in: table name */
-	char*		errstr,		/*!< out: error message
-					if != DB_SUCCESS is returned */
-	ulint		errstr_sz);	/*!< in: size of errstr buffer */
-
-/*********************************************************************//**
-Fetches or calculates new estimates for index statistics. */
+	const char*	db_and_table,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx);
+
+/** Calculate index statistics.
+@param[in,out]	index	index tree
+@param[in,out]	trx	transaction (for persistent statistics)
+@return DB_SUCCESS or error code */
 UNIV_INTERN
-void
-dict_stats_update_for_index(
-/*========================*/
-	dict_index_t*	index)	/*!< in/out: index */
+dberr_t
+dict_stats_update_for_index(dict_index_t* index, trx_t* trx)
 	MY_ATTRIBUTE((nonnull));
 
-/*********************************************************************//**
-Renames a table in InnoDB persistent stats storage.
-This function creates its own transaction and commits it.
+/** Rename a table in the InnoDB persistent statistics storage.
+@param[in]	old_name	old schema and table name, e.g., 'db/table'
+@param[in]	new_name	new schema and table name, e.g., 'db/table'
+@param[out]	errstr		error message (when not returning DB_SUCCESS)
+@param[in]	errstr_sz	sizeof errstr
+@param[in,out]	trx		transaction
 @return DB_SUCCESS or error code */
 UNIV_INTERN
 dberr_t
 dict_stats_rename_table(
-/*====================*/
-	const char*	old_name,	/*!< in: old table name */
-	const char*	new_name,	/*!< in: new table name */
-	char*		errstr,		/*!< out: error string if != DB_SUCCESS
-					is returned */
-	size_t		errstr_sz);	/*!< in: errstr size */
+	const char*	old_name,
+	const char*	new_name,
+	char*		errstr,
+	size_t		errstr_sz,
+	trx_t*		trx);
 
 #ifndef UNIV_NONINL
 #include "dict0stats.ic"
diff --git a/storage/xtradb/include/dict0stats.ic b/storage/xtradb/include/dict0stats.ic
index ec9a9065470..48c53fa0576 100644
--- a/storage/xtradb/include/dict0stats.ic
+++ b/storage/xtradb/include/dict0stats.ic
@@ -149,13 +149,12 @@ dict_stats_auto_recalc_is_enabled(
 	}
 }
 
-/*********************************************************************//**
-Initialize table's stats for the first time when opening a table. */
+/** Initialize table statistics for the first time when opening a table.
+@param[in,out]	table	freshly opened table
+@param[in,out]	trx	transaction */
 UNIV_INLINE
 void
-dict_stats_init(
-/*============*/
-	dict_table_t*	table)	/*!< in/out: table */
+dict_stats_init(dict_table_t* table, trx_t* trx)
 {
 	ut_ad(!mutex_own(&dict_sys->mutex));
 
@@ -171,7 +170,7 @@ dict_stats_init(
 		opt = DICT_STATS_RECALC_TRANSIENT;
 	}
 
-	dict_stats_update(table, opt);
+	dict_stats_update(table, opt, trx);
 }
 
 /*********************************************************************//**
diff --git a/storage/xtradb/lock/lock0lock.cc b/storage/xtradb/lock/lock0lock.cc
index 1a2b15422bb..3947c6a603d 100644
--- a/storage/xtradb/lock/lock0lock.cc
+++ b/storage/xtradb/lock/lock0lock.cc
@@ -1,7 +1,7 @@
 /*****************************************************************************
 
 Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
-Copyright (c) 2014, 2015, MariaDB Corporation
+Copyright (c) 2014, 2017, 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
@@ -2001,6 +2001,16 @@ lock_rec_enqueue_waiting(
 		break;
 	case TRX_DICT_OP_TABLE:
 	case TRX_DICT_OP_INDEX:
+		if (trx->dict_operation_lock_mode != RW_X_LATCH) {
+		} else if (!strcmp(index->table->name,
+				   "mysql/innodb_table_stats")
+			   || !strcmp(index->table->name,
+				      "mysql/innodb_index_stats")) {
+			/* Statistics can be updated as part of a DDL
+			transaction, but only as the very last operation. */
+			return(DB_QUE_THR_SUSPENDED);
+		}
+
 		ut_print_timestamp(stderr);
 		fputs("  InnoDB: Error: a record lock wait happens"
 		      " in a dictionary operation!\n"
@@ -4468,6 +4478,15 @@ lock_table_enqueue_waiting(
 		break;
 	case TRX_DICT_OP_TABLE:
 	case TRX_DICT_OP_INDEX:
+		if (trx->dict_operation_lock_mode != RW_X_LATCH) {
+		} else if (!strcmp(table->name, "mysql/innodb_table_stats")
+			   || !strcmp(table->name,
+				      "mysql/innodb_index_stats")) {
+			/* Statistics can be updated as part of a DDL
+			transaction, but only as the very last operation. */
+			return(DB_QUE_THR_SUSPENDED);
+		}
+
 		ut_print_timestamp(stderr);
 		fputs("  InnoDB: Error: a table lock wait happens"
 		      " in a dictionary operation!\n"
diff --git a/storage/xtradb/row/row0merge.cc b/storage/xtradb/row/row0merge.cc
index c5c2e992b6f..f4ff5fbdf9c 100644
--- a/storage/xtradb/row/row0merge.cc
+++ b/storage/xtradb/row/row0merge.cc
@@ -2720,31 +2720,9 @@ row_merge_lock_table(
 		que_thr_stop_for_mysql_no_error(thr, trx);
 	} else {
 		que_thr_stop_for_mysql(thr);
+		ut_ad(err != DB_QUE_THR_SUSPENDED);
 
-		if (err != DB_QUE_THR_SUSPENDED) {
-			bool	was_lock_wait;
-
-			was_lock_wait = row_mysql_handle_errors(
-				&err, trx, thr, NULL);
-
-			if (was_lock_wait) {
-				goto run_again;
-			}
-		} else {
-			que_thr_t*	run_thr;
-			que_node_t*	parent;
-
-			parent = que_node_get_parent(thr);
-
-			run_thr = que_fork_start_command(
-				static_cast<que_fork_t*>(parent));
-
-			ut_a(run_thr == thr);
-
-			/* There was a lock wait but the thread was not
-			in a ready to run or running state. */
-			trx->error_state = DB_LOCK_WAIT;
-
+		if (row_mysql_handle_errors(&err, trx, thr, NULL)) {
 			goto run_again;
 		}
 	}
diff --git a/storage/xtradb/row/row0mysql.cc b/storage/xtradb/row/row0mysql.cc
index ebdee381713..06f1d9be5cb 100644
--- a/storage/xtradb/row/row0mysql.cc
+++ b/storage/xtradb/row/row0mysql.cc
@@ -1140,7 +1140,7 @@ row_update_statistics_if_needed(
 
 		ut_ad(!mutex_own(&dict_sys->mutex));
 		/* this will reset table->stat_modified_counter to 0 */
-		dict_stats_update(table, DICT_STATS_RECALC_TRANSIENT);
+		dict_stats_update(table, DICT_STATS_RECALC_TRANSIENT, NULL);
 	}
 }
 
@@ -3271,31 +3271,9 @@ row_mysql_lock_table(
 		que_thr_stop_for_mysql_no_error(thr, trx);
 	} else {
 		que_thr_stop_for_mysql(thr);
+		ut_ad(err != DB_QUE_THR_SUSPENDED);
 
-		if (err != DB_QUE_THR_SUSPENDED) {
-			ibool	was_lock_wait;
-
-			was_lock_wait = row_mysql_handle_errors(
-				&err, trx, thr, NULL);
-
-			if (was_lock_wait) {
-				goto run_again;
-			}
-		} else {
-			que_thr_t*	run_thr;
-			que_node_t*	parent;
-
-			parent = que_node_get_parent(thr);
-
-			run_thr = que_fork_start_command(
-				static_cast<que_fork_t*>(parent));
-
-			ut_a(run_thr == thr);
-
-			/* There was a lock wait but the thread was not
-			in a ready to run or running state. */
-			trx->error_state = DB_LOCK_WAIT;
-
+		if (row_mysql_handle_errors(&err, trx, thr, NULL)) {
 			goto run_again;
 		}
 	}
@@ -3806,7 +3784,8 @@ row_truncate_table_for_mysql(
 
 	row_mysql_unlock_data_dictionary(trx);
 
-	dict_stats_update(table, DICT_STATS_EMPTY_TABLE);
+	dict_stats_update(table, DICT_STATS_EMPTY_TABLE, trx);
+	trx_commit_for_mysql(trx);
 
 	trx->op_info = "";
 
@@ -3984,17 +3963,6 @@ row_drop_table_for_mysql(
 	if (!dict_table_is_temporary(table)) {
 
 		dict_stats_recalc_pool_del(table);
-
-		/* Remove stats for this table and all of its indexes from the
-		persistent storage if it exists and if there are stats for this
-		table in there. This function creates its own trx and commits
-		it. */
-		char	errstr[1024];
-		err = dict_stats_drop_table(name, errstr, sizeof(errstr));
-
-		if (err != DB_SUCCESS) {
-			ib_logf(IB_LOG_LEVEL_WARN, "%s", errstr);
-		}
 	}
 
 	/* Move the table the the non-LRU list so that it isn't
@@ -4296,14 +4264,20 @@ row_drop_table_for_mysql(
 		ibool	is_temp;
 
 	case DB_SUCCESS:
-		/* Clone the name, in case it has been allocated
-		from table->heap, which will be freed by
-		dict_table_remove_from_cache(table) below. */
 		space_id = table->space;
 		ibd_file_missing = table->ibd_file_missing;
 
 		is_temp = DICT_TF2_FLAG_IS_SET(table, DICT_TF2_TEMPORARY);
 
+		if (!is_temp) {
+			char	errstr[1024];
+			if (dict_stats_drop_table(name, errstr, sizeof errstr,
+						  trx)
+			    != DB_SUCCESS) {
+				ib_logf(IB_LOG_LEVEL_WARN, "%s", errstr);
+			}
+		}
+
 		/* If there is a temp path then the temp flag is set.
 		However, during recovery or reloading the table object
 		after eviction from data dictionary cache, we might
-- 
2.15.0

