From 9618d551a77d48e0d7239730ea2e47100a6a6665 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 | 131 +++++++++++++++++- mysql-test/main/opt_context_replay_basic.test | 34 +++++ sql/opt_context_store_replay.cc | 13 +- sql/opt_context_store_replay.h | 9 +- 6 files changed, 199 insertions(+), 14 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 8d5c1fab4058b..d66ea48059b3e 100644 --- a/mysql-test/main/opt_context_replay_basic.result +++ b/mysql-test/main/opt_context_replay_basic.result @@ -124,7 +124,7 @@ SET optimizer_where_cost=0.032000; SET sql_mode='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'; -SET timestamp=1780212030.000000; +SET timestamp=1780203030.000000; select count(*) from t1; @@ -205,7 +205,7 @@ SET optimizer_trace_max_mem_size=1048576; SET optimizer_use_condition_selectivity=4; SET optimizer_where_cost=0.032000; SET sql_mode='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'; -SET timestamp=1780212030.000000; +SET timestamp=1780203030.000000; CREATE DATABASE IF NOT EXISTS db1; Warnings: Note 1007 Can't create database 'db1'; database exists @@ -456,4 +456,131 @@ trace like '%foo%' select context like '%bar%' from information_schema.optimizer_context; context like '%bar%' 1 +drop table t1; +# +# MDEV-39538: Different cost when same range is 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 c03940aa5fe5b..0b110417c89fc 100644 --- a/mysql-test/main/opt_context_replay_basic.test +++ b/mysql-test/main/opt_context_replay_basic.test @@ -224,6 +224,40 @@ select trace like '%foo%' from information_schema.optimizer_trace; explain select * from t1 where a >'foo' or a < 'bar'; select trace like '%foo%' from information_schema.optimizer_trace; select context like '%bar%' from information_schema.optimizer_context; +drop table t1; +--echo # +--echo # MDEV-39538: Different cost when same range is 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 f17edf3483c83..9bea1c8c5b733 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; }; /* @@ -266,6 +267,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); } } @@ -864,11 +866,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 @@ -1331,8 +1335,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", @@ -1341,6 +1344,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); @@ -1568,6 +1573,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; @@ -1596,6 +1602,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 fe6667f27f85d..ad22f05f39126 100644 --- a/sql/opt_context_store_replay.h +++ b/sql/opt_context_store_replay.h @@ -89,9 +89,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); @@ -151,6 +154,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.