From c2c3b5e54e80ea926d3dd64afd5033763df84d0f Mon Sep 17 00:00:00 2001 From: bsrikanth-mariadb Date: Tue, 26 May 2026 14:14:59 +0530 Subject: [PATCH] MDEV-39538: Different costs when same range is read twice When same range is used as a filter, once in the outer query block, and the second inside a sub query such as: - select * from t1 where year(a) = 2010 and c < (select count(*) from t1 where year(a) = 2010); The Optimizer Context had two records for multi_range_read_info_const() call, but had different cost vector members. The cause is that the first table considered index-only scan on the range, while the second considered non-index only scan. However, when replaying the context, the same range got matched twice, and the costs corresponding to that got returned twice. Hence the costs in the explain plan output differed as well. Solution ======== Include a new field called "call_number", while recording range contexts into the overall context. This way, we could even match the call_number and return the appropriate cost during replay. --- .../main/opt_context_load_stats_basic.result | 23 ++-- .../main/opt_context_load_stats_basic.test | 3 + .../main/opt_context_replay_basic.result | 126 ++++++++++++++++++ mysql-test/main/opt_context_replay_basic.test | 35 +++++ sql/opt_context_store_replay.cc | 13 +- sql/opt_context_store_replay.h | 9 +- 6 files changed, 197 insertions(+), 12 deletions(-) diff --git a/mysql-test/main/opt_context_load_stats_basic.result b/mysql-test/main/opt_context_load_stats_basic.result index 054dc827ed7aa..516e5f6199aa6 100644 --- a/mysql-test/main/opt_context_load_stats_basic.result +++ b/mysql-test/main/opt_context_load_stats_basic.result @@ -343,7 +343,7 @@ set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].name'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "name" element not present at offset 1387. +Warning 4253 Failed to parse saved optimizer context: "name" element not present at offset 1441. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].ddl'); select * from t1 where a > 10; a b @@ -354,12 +354,12 @@ set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].file_stat select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "file_stat_records" element not present at offset 1380. +Warning 4253 Failed to parse saved optimizer context: "file_stat_records" element not present at offset 1434. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].file_stat_records'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "file_stat_records" element not present at offset 1380. +Warning 4253 Failed to parse saved optimizer context: "file_stat_records" element not present at offset 1434. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].indexes[0].index_name'); select * from t1 where a > 10; a b @@ -374,32 +374,37 @@ set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_rang select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "index_name" element not present at offset 621. +Warning 4253 Failed to parse saved optimizer context: "index_name" element not present at offset 639. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].ranges'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "ranges" element not present at offset 613. +Warning 4253 Failed to parse saved optimizer context: "ranges" element not present at offset 631. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].num_rows'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "num_rows" element not present at offset 631. +Warning 4253 Failed to parse saved optimizer context: "num_rows" element not present at offset 649. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].cost'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "cost" element not present at offset 415. +Warning 4253 Failed to parse saved optimizer context: "cost" element not present at offset 433. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].max_index_blocks'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "max_index_blocks" element not present at offset 624. +Warning 4253 Failed to parse saved optimizer context: "max_index_blocks" element not present at offset 642. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].max_row_blocks'); select * from t1 where a > 10; a b Warnings: -Warning 4253 Failed to parse saved optimizer context: "max_row_blocks" element not present at offset 626. +Warning 4253 Failed to parse saved optimizer context: "max_row_blocks" element not present at offset 644. +set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].call_number'); +select * from t1 where a > 10; +a b +Warnings: +Warning 4253 Failed to parse saved optimizer context: "call_number" element not present at offset 647. set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].indexes[0]'); select * from t1 where a > 10; a b diff --git a/mysql-test/main/opt_context_load_stats_basic.test b/mysql-test/main/opt_context_load_stats_basic.test index 323648fe625cd..5a3e8f1c7108a 100644 --- a/mysql-test/main/opt_context_load_stats_basic.test +++ b/mysql-test/main/opt_context_load_stats_basic.test @@ -253,6 +253,9 @@ select * from t1 where a > 10; set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].max_row_blocks'); select * from t1 where a > 10; +set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].list_ranges[0].call_number'); +select * from t1 where a > 10; + set @opt_context=json_remove(@saved_opt_context_1, '$.list_contexts[0].indexes[0]'); select * from t1 where a > 10; diff --git a/mysql-test/main/opt_context_replay_basic.result b/mysql-test/main/opt_context_replay_basic.result index 6ffa4f5a55e37..101b02214915e 100644 --- a/mysql-test/main/opt_context_replay_basic.result +++ b/mysql-test/main/opt_context_replay_basic.result @@ -409,4 +409,130 @@ id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE t1 ALL btn NULL NULL NULL 4 Using where set optimizer_replay_context=''; drop table t1; +# +# MDEV-39538: Different cost when same rangeis read twice +# +create table t1 (pk int primary key, a datetime, c int, key(a)); +insert into t1 (pk,a,c) values (1,'2009-11-29 13:43:32', 2); +insert into t1 (pk,a,c) values (2,'2009-11-29 03:23:32', 2); +insert into t1 (pk,a,c) values (3,'2009-10-16 05:56:32', 2); +insert into t1 (pk,a,c) values (4,'2010-11-29 13:43:32', 2); +insert into t1 (pk,a,c) values (5,'2010-10-16 05:56:32', 2); +insert into t1 (pk,a,c) values (6,'2011-11-29 13:43:32', 2); +insert into t1 (pk,a,c) values (7,'2012-10-16 05:56:32', 2); +set optimizer_record_context=1; +explain format=json select * from t1 +where year(a) = 2010 and c < (select count(*) from t1 where year(a) = 2010); +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.003808422, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "range", + "possible_keys": ["a"], + "key": "a", + "key_length": "6", + "used_key_parts": ["a"], + "loops": 1, + "rows": 2, + "cost": 0.003808422, + "filtered": 100, + "index_condition": "t1.a between '2010-01-01 00:00:00' and '2010-12-31 23:59:59'", + "attached_condition": "t1.c < (subquery#2)" + } + } + ], + "subqueries": [ + { + "query_block": { + "select_id": 2, + "cost": 0.001617224, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "range", + "possible_keys": ["a"], + "key": "a", + "key_length": "6", + "used_key_parts": ["a"], + "loops": 1, + "rows": 2, + "cost": 0.001617224, + "filtered": 100, + "attached_condition": "t1.a between '2010-01-01 00:00:00' and '2010-12-31 23:59:59'", + "using_index": true + } + } + ] + } + } + ] + } +} +select context into dumpfile "../../tmp/dump1.sql" +from information_schema.optimizer_context; +set optimizer_record_context=0; +drop table t1; +set optimizer_replay_context='opt_context'; +# Same query as above, must have same explain cost: +explain format=json select * from t1 +where year(a) = 2010 and c < (select count(*) from t1 where year(a) = 2010); +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.003808422, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "range", + "possible_keys": ["a"], + "key": "a", + "key_length": "6", + "used_key_parts": ["a"], + "loops": 1, + "rows": 2, + "cost": 0.003808422, + "filtered": 100, + "index_condition": "t1.a between '2010-01-01 00:00:00' and '2010-12-31 23:59:59'", + "attached_condition": "t1.c < (subquery#2)" + } + } + ], + "subqueries": [ + { + "query_block": { + "select_id": 2, + "cost": 0.001617224, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "range", + "possible_keys": ["a"], + "key": "a", + "key_length": "6", + "used_key_parts": ["a"], + "loops": 1, + "rows": 2, + "cost": 0.001617224, + "filtered": 100, + "attached_condition": "t1.a between '2010-01-01 00:00:00' and '2010-12-31 23:59:59'", + "using_index": true + } + } + ] + } + } + ] + } +} +set optimizer_replay_context=''; +drop table t1; drop database db1; diff --git a/mysql-test/main/opt_context_replay_basic.test b/mysql-test/main/opt_context_replay_basic.test index 70b712ac825df..e2d13965f8681 100644 --- a/mysql-test/main/opt_context_replay_basic.test +++ b/mysql-test/main/opt_context_replay_basic.test @@ -200,4 +200,39 @@ set optimizer_replay_context=''; --remove_file "$MYSQLTEST_VARDIR/tmp/dump1.sql" drop table t1; +--echo # +--echo # MDEV-39538: Different cost when same rangeis read twice +--echo # +create table t1 (pk int primary key, a datetime, c int, key(a)); + +insert into t1 (pk,a,c) values (1,'2009-11-29 13:43:32', 2); +insert into t1 (pk,a,c) values (2,'2009-11-29 03:23:32', 2); +insert into t1 (pk,a,c) values (3,'2009-10-16 05:56:32', 2); +insert into t1 (pk,a,c) values (4,'2010-11-29 13:43:32', 2); +insert into t1 (pk,a,c) values (5,'2010-10-16 05:56:32', 2); +insert into t1 (pk,a,c) values (6,'2011-11-29 13:43:32', 2); +insert into t1 (pk,a,c) values (7,'2012-10-16 05:56:32', 2); + +set optimizer_record_context=1; +explain format=json select * from t1 + where year(a) = 2010 and c < (select count(*) from t1 where year(a) = 2010); +select context into dumpfile "../../tmp/dump1.sql" +from information_schema.optimizer_context; +set optimizer_record_context=0; +drop table t1; +--disable_query_log +--disable_result_log +--source "$MYSQLTEST_VARDIR/tmp/dump1.sql" +--enable_query_log +--enable_result_log +set optimizer_replay_context='opt_context'; +--echo # Same query as above, must have same explain cost: +explain format=json select * from t1 + where year(a) = 2010 and c < (select count(*) from t1 where year(a) = 2010); + +set optimizer_replay_context=''; +--remove_file "$MYSQLTEST_VARDIR/tmp/dump1.sql" +drop table t1; + + drop database db1; diff --git a/sql/opt_context_store_replay.cc b/sql/opt_context_store_replay.cc index 7692b34d9a1e5..06c3f4f45324d 100644 --- a/sql/opt_context_store_replay.cc +++ b/sql/opt_context_store_replay.cc @@ -112,6 +112,7 @@ class Multi_range_read_const_call_record : public Sql_alloc Cost_estimate cost; ha_rows max_index_blocks; ha_rows max_row_blocks; + ulong call_number; }; /* @@ -257,6 +258,7 @@ void dump_mrr_info_calls(List *mrr_list, irc_wrapper.add("max_index_blocks", irc->max_index_blocks); irc_wrapper.add("max_row_blocks", irc->max_row_blocks); + irc_wrapper.add("call_number", irc->call_number); } } @@ -818,11 +820,13 @@ void Optimizer_context_recorder::record_multi_range_read_info_const( if (current_thd->lex->explain->is_query_plan_ready()) return; + mrr_counter++; auto *range_ctx= new (mem_root) Multi_range_read_const_call_record; if (unlikely(!range_ctx)) return; // OOM + range_ctx->call_number= mrr_counter; const char *index_name= tbl->table->key_info[keynr].name.str; if (!(range_ctx->idx_name= strdup_root(mem_root, index_name))) return; // OOM @@ -1285,8 +1289,7 @@ static int parse_range_context(MEM_ROOT *mem_root, json_engine_t *je, String *er Read_named_member array[]= { {"index_name", Read_string(mem_root, &out->idx_name), false}, {"ranges", Read_array_of_strings(mem_root, &out->range_list), false}, - {"num_rows", - Read_non_neg_integer(&out->rows), + {"num_rows", Read_non_neg_integer(&out->rows), false}, {"cost", Read_range_cost_estimate(mem_root, &out->cost), false}, {"max_index_blocks", @@ -1295,6 +1298,8 @@ static int parse_range_context(MEM_ROOT *mem_root, json_engine_t *je, String *er {"max_row_blocks", Read_non_neg_integer(&out->max_row_blocks), false}, + {"call_number", + Read_non_neg_integer(&out->call_number), false}, {NULL, Read_double(NULL), true}}; return parse_context_obj_from_json_array(je, err_buf, err_msg, array); @@ -1516,6 +1521,7 @@ bool Optimizer_context_replay::infuse_range_stats( if (!has_records() || !is_base_table(table->pos_in_table_list)) return true; + mrr_counter++; KEY *keyinfo= table->key_info + keynr; const char *idx_name= keyinfo->name.str; const KEY_PART_INFO *key_part= keyinfo->key_part; @@ -1544,6 +1550,9 @@ bool Optimizer_context_replay::infuse_range_stats( List_iterator range_ctx_itr(mrr_const_calls); while (Multi_range_read_const_call_record *range_ctx= range_ctx_itr++) { + if (range_ctx->call_number != mrr_counter) + continue; + List_iterator range_itr(range_ctx->range_list); seq_it= seq_if->init((void *) seq, 0, 0); bool matched= true; diff --git a/sql/opt_context_store_replay.h b/sql/opt_context_store_replay.h index 1ec2d7a84e80a..c9dc877f4a1fe 100644 --- a/sql/opt_context_store_replay.h +++ b/sql/opt_context_store_replay.h @@ -88,9 +88,12 @@ class Optimizer_context_recorder table_context_for_store *get_table_context(const TABLE_LIST *tbl); static const uchar *get_tbl_ctx_key(const void *entry_, size_t *length, my_bool flags); + /* + counter that tracks record_multi_range_read_info_const() calls + */ + ulong mrr_counter= 0; }; - /* Save the collected context into optimizer_context IS table */ bool store_optimizer_context(THD *thd); @@ -150,6 +153,10 @@ class Optimizer_context_replay bool infuse_table_rows(const TABLE *tbl, ha_rows *rows); THD *thd; + /* + counter that tracks infuse_range_stats() calls + */ + ulong mrr_counter= 0; /* Statistics that tables had before we've replaced them with values from the saved context. To be used to restore the original values.