1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

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
This commit is contained in:
Sergei Golubchik
2026-10-14 22:55:30 +02:00
parent 0e2780afc6
commit bb6a72c084
5 changed files with 98 additions and 13 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<double> 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<table_context_for_replay>(thd->mem_root, &ctx_list,
Read_object_array<table_context_for_replay>(mem_root, &ctx_list,
table_context_for_replay::parse),
false
},

View File

@@ -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
*/

View File

@@ -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())