From bb6a72c0843bdbf5929fe277bb16e00e452adb56 Mon Sep 17 00:00:00 2001 From: Sergei Golubchik Date: Wed, 14 Oct 2026 22:55:30 +0200 Subject: [PATCH] replay loses infused statistics in stored functions Every sub-statement (stored function, trigger) replaced thd->opt_ctx_replay, leaking the top-level one, so the top-level tables were never restored and kept key_info->read_stats pointing into the freed statement mem_root. Saved and infused statistics were allocated on whatever arena was active, e.g. the stored function execution arena, freed before the restore. * create the replay object only for a top-level statement, and let sub-statements use it; restore only at the end of the top-level one * allocate everything the replay object owns in its own MEM_ROOT * empty the saved list after restoring Assisted-by: Claude:claude-5.5-opus --- .../main/opt_context_load_stats_innodb.result | 41 +++++++++++++++++++ .../main/opt_context_load_stats_innodb.test | 23 +++++++++++ sql/opt_context_store_replay.cc | 37 ++++++++++++----- sql/opt_context_store_replay.h | 8 +++- sql/sql_parse.cc | 2 +- 5 files changed, 98 insertions(+), 13 deletions(-) diff --git a/mysql-test/main/opt_context_load_stats_innodb.result b/mysql-test/main/opt_context_load_stats_innodb.result index 8341be2ec0d..0bf7c5be694 100644 --- a/mysql-test/main/opt_context_load_stats_innodb.result +++ b/mysql-test/main/opt_context_load_stats_innodb.result @@ -1396,4 +1396,45 @@ select json_extract(@opt_context, '$.tables[0].indexes[0].rec_per_key[0]'); json_extract(@opt_context, '$.tables[0].indexes[0].rec_per_key[0]') "10" drop table t1; +# +# index statistics are restored after a replayed statement +# that runs a stored function +# +create table t1 (a int, b int, key(a)) engine=innodb; +insert into t1 select seq%10, seq from seq_1_to_100; +create table t2 (a int, key(a)) engine=innodb; +insert into t2 select seq%10 from seq_1_to_100; +analyze table t1, t2 persistent for all; +Table Op Msg_type Msg_text +test.t1 analyze status Engine-independent statistics collected +test.t1 analyze status OK +test.t2 analyze status Engine-independent statistics collected +test.t2 analyze status OK +create function f1() returns int return (select count(*) from t2); +select count(*) from t2; +count(*) +100 +set @opt_context= +(select REGEXP_SUBSTR( +context, +'(?<=set @opt_context=\')([\n\r].*)*(?=\'\;#opt_context_ends)') +from information_schema.optimizer_context); +set @opt_context_t2= @opt_context; +select count(*) from t1 where a=3; +count(*) +10 +set @opt_context= +(select REGEXP_SUBSTR( +context, +'(?<=set @opt_context=\')([\n\r].*)*(?=\'\;#opt_context_ends)') +from information_schema.optimizer_context); +set @opt_context_rpk= json_array_append(json_replace(@opt_context, +'$.tables[0].indexes[0].rec_per_key[0]', 77), +'$.tables', json_extract(@opt_context_t2, '$.tables[0]')); +set optimizer_replay_context='opt_context_rpk'; +select f1() from t1 where a=3 limit 1; +f1() +100 +drop function f1; +drop table t1, t2; # End of 13.1 tests diff --git a/mysql-test/main/opt_context_load_stats_innodb.test b/mysql-test/main/opt_context_load_stats_innodb.test index 5b956f8b2ec..59f96c4cdf5 100644 --- a/mysql-test/main/opt_context_load_stats_innodb.test +++ b/mysql-test/main/opt_context_load_stats_innodb.test @@ -420,4 +420,27 @@ select count(*) from t1 where a=3; select json_extract(@opt_context, '$.tables[0].indexes[0].rec_per_key[0]'); drop table t1; +--echo # +--echo # index statistics are restored after a replayed statement +--echo # that runs a stored function +--echo # +create table t1 (a int, b int, key(a)) engine=innodb; +insert into t1 select seq%10, seq from seq_1_to_100; +create table t2 (a int, key(a)) engine=innodb; +insert into t2 select seq%10 from seq_1_to_100; +analyze table t1, t2 persistent for all; +create function f1() returns int return (select count(*) from t2); +select count(*) from t2; +--source include/opt_context_save_in_var.inc +set @opt_context_t2= @opt_context; +select count(*) from t1 where a=3; +--source include/opt_context_save_in_var.inc +set @opt_context_rpk= json_array_append(json_replace(@opt_context, + '$.tables[0].indexes[0].rec_per_key[0]', 77), + '$.tables', json_extract(@opt_context_t2, '$.tables[0]')); +set optimizer_replay_context='opt_context_rpk'; +select f1() from t1 where a=3 limit 1; +drop function f1; +drop table t1, t2; + --echo # End of 13.1 tests diff --git a/sql/opt_context_store_replay.cc b/sql/opt_context_store_replay.cc index 80eaf89d041..225701cd7f0 100644 --- a/sql/opt_context_store_replay.cc +++ b/sql/opt_context_store_replay.cc @@ -1239,7 +1239,7 @@ static void append_table_or_view_name(const TABLE_LIST *tbl, String *buf) class index_context_for_replay : public Sql_alloc { public: - index_context_for_replay() : rec_per_key(current_thd->mem_root) {} + index_context_for_replay(MEM_ROOT *mem_root) : rec_per_key(mem_root) {} char *idx_name= NULL; Mem_root_dynamic_array rec_per_key; static index_context_for_replay *parse(MEM_ROOT *mem_root, @@ -1447,7 +1447,7 @@ index_context_for_replay* index_context_for_replay::parse(MEM_ROOT *mem_root, json_engine_t *je, String *err_buf) { - auto index_ctx= new (mem_root) index_context_for_replay; + auto index_ctx= new (mem_root) index_context_for_replay(mem_root); if (!index_ctx) return NULL; @@ -1480,7 +1480,7 @@ Multi_range_read_const_call_record * Multi_range_read_const_call_record::parse(MEM_ROOT *mem_root, json_engine_t *je, String *err_buf) { - auto out= new Multi_range_read_const_call_record; + auto out= new (mem_root) Multi_range_read_const_call_record; if (!out) return NULL; @@ -1631,7 +1631,8 @@ records_in_range_call_record::parse(MEM_ROOT *mem_root, return out; } -Optimizer_context_replay::Optimizer_context_replay(THD *thd_arg) : thd(thd_arg) +Optimizer_context_replay::Optimizer_context_replay(THD *thd_arg) + : thd(thd_arg), mem_root(thd_arg->mem_root) { parse(); // TODO: error handling? } @@ -1648,6 +1649,17 @@ static bool sql_command_uses_opt_context(enum_sql_command sql_command) void init_optimizer_context_replay_if_needed(THD *thd) { + /* + Sub-statements use the replay object of the top-level statement, which + restores the infused statistics of all their tables and frees it. + Statements of a stored procedure are not replayed. + */ + if (thd->spcont || thd->in_sub_stmt) + return; + + // Replay is cleaned up after query in THD::cleanup_after_query() + DBUG_ASSERT(!thd->opt_ctx_replay); + /* If @@optimizer_replay_context is not empty, start the replay */ if (thd->variables.optimizer_replay_context && strlen(thd->variables.optimizer_replay_context) > 0 && @@ -1912,14 +1924,14 @@ void Optimizer_context_replay::infuse_table_stats(TABLE *table) if (!has_records() || !is_base_table(table->pos_in_table_list)) return; - Saved_table_stats *saved_ts= new Saved_table_stats(); + Saved_table_stats *saved_ts= new (mem_root) Saved_table_stats(); if (unlikely(!saved_ts)) return; // OOM saved_ts->table= table; - if (saved_table_stats.push_front(saved_ts)) + if (saved_table_stats.push_front(saved_ts, mem_root)) return; KEY *key_info, *key_info_end; @@ -1932,7 +1944,7 @@ void Optimizer_context_replay::infuse_table_stats(TABLE *table) if (!index_freq_list || !index_freq_list->size()) continue; - Saved_index_stats *saved_is= new Saved_index_stats(); + Saved_index_stats *saved_is= new (mem_root) Saved_index_stats(); if (unlikely(!saved_is)) return; // OOM @@ -1941,16 +1953,18 @@ void Optimizer_context_replay::infuse_table_stats(TABLE *table) Index_statistics *original_read_stats= key_info->read_stats; bool original_is_statistics_from_stat_tables= key_info->is_statistics_from_stat_tables; - Index_statistics *new_read_stats= new Index_statistics(); + Index_statistics *new_read_stats= new (mem_root) Index_statistics(); if (unlikely(!new_read_stats)) return; // OOM + size_t frequencies_size= sizeof(ulonglong) * num_key_parts; ulonglong *frequencies= - (ulonglong *) thd->calloc(sizeof(ulonglong) * num_key_parts); + (ulonglong *) alloc_root(mem_root, frequencies_size); if (unlikely(!frequencies)) return; // OOM + bzero(frequencies, frequencies_size); new_read_stats->init_avg_frequency(frequencies); key_info->read_stats= new_read_stats; @@ -1968,7 +1982,7 @@ void Optimizer_context_replay::infuse_table_stats(TABLE *table) saved_is->original_is_statistics_from_stat_tables= original_is_statistics_from_stat_tables; saved_is->original_read_stats= original_read_stats; - saved_ts->saved_index_stats.push_back(saved_is); + saved_ts->saved_index_stats.push_back(saved_is, mem_root); } } @@ -2042,6 +2056,7 @@ void Optimizer_context_replay::restore_modified_table_stats() key->read_stats= saved_is->original_read_stats; } } + saved_table_stats.empty(); } /* @@ -2072,7 +2087,7 @@ bool Optimizer_context_replay::parse() Read_named_member members[]= { {"tables", - Read_object_array(thd->mem_root, &ctx_list, + Read_object_array(mem_root, &ctx_list, table_context_for_replay::parse), false }, diff --git a/sql/opt_context_store_replay.h b/sql/opt_context_store_replay.h index c8d136cf092..eba45321526 100644 --- a/sql/opt_context_store_replay.h +++ b/sql/opt_context_store_replay.h @@ -1,5 +1,5 @@ /* - Copyright (c) 2025, MariaDB + Copyright (c) 2025, 2026, MariaDB plc 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 @@ -177,6 +177,11 @@ void init_optimizer_context_replay_if_needed(THD *thd); thd->opt_ctx_replay->infuse_XXX() to get "infuse" the statistics records from the context. + + - The object is created by a top-level statement and is used by its + sub-statements (stored functions, triggers) too. Everything it allocates + is on the mem_root of the top-level statement, which outlives the object, + independently of the arena that is active when a statistic is infused. */ class Optimizer_context_replay @@ -215,6 +220,7 @@ public: private: THD *thd; + MEM_ROOT *mem_root; /* counter that tracks infuse_range_stats() calls */ diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index 93d61f32b5c..bd26bc1397a 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -5922,7 +5922,7 @@ finish: if (!thd->is_error() && !res) res= store_optimizer_context(thd); - if (thd->opt_ctx_replay) + if (thd->opt_ctx_replay && !thd->spcont && !thd->in_sub_stmt) thd->opt_ctx_replay->restore_modified_table_stats(); if (res || thd->is_error())