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

MDEV-25848: record/replay hlindex records_in_range() in optimizer context

check_quick_select_array() now goes through hook_hl_records_in_range(),
which records or replaces both the row estimate and the cost
(record_hl_records_in_range / infuse_hl_records_in_range). The calls are
stored in a new per-table "hl_records_in_range_calls" array.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Sergei Petrunia
2026-10-09 13:50:01 +03:00
parent b608db55f6
commit 05228b483b
7 changed files with 372 additions and 19 deletions

View File

@@ -197,6 +197,106 @@ let $opt_context_schema=
"copy_cost"
]
}
},
"records_in_range_calls": {
"type": "array",
"uniqueItems": false,
"minItems": 1,
"items": {
"type": "object",
"properties": {
"key_number": {
"type": "number"
},
"min_key": {
"type": "string"
},
"max_key": {
"type": "string"
},
"num_records": {
"type": "number"
}
},
"required": [
"key_number",
"min_key",
"max_key",
"num_records"
]
}
},
"hl_records_in_range_calls": {
"type": "array",
"uniqueItems": false,
"minItems": 1,
"items": {
"type": "object",
"properties": {
"key_number": {
"type": "number"
},
"key": {
"type": "string"
},
"num_records": {
"type": "number"
},
"cost": {
"type": "object",
"properties": {
"avg_io_cost": {
"type": "number"
},
"cpu_cost": {
"type": "number"
},
"comp_cost": {
"type": "number"
},
"copy_cost": {
"type": "number"
},
"limit_cost": {
"type": "number"
},
"setup_cost": {
"type": "number"
},
"index_cost_io": {
"type": "number"
},
"index_cost_cpu": {
"type": "number"
},
"row_cost_io": {
"type": "number"
},
"row_cost_cpu": {
"type": "number"
}
},
"required": [
"avg_io_cost",
"cpu_cost",
"comp_cost",
"copy_cost",
"limit_cost",
"setup_cost",
"index_cost_io",
"index_cost_cpu",
"row_cost_io",
"row_cost_cpu"
]
}
},
"required": [
"key_number",
"key",
"num_records",
"cost"
]
}
}
"subquery_runs": {
"type": "array",

View File

@@ -278,7 +278,7 @@ set @opt_context=json_remove(@saved_opt_context_1, '$.tables[0].name');
select * from t1 where a > 10;
a b
Warnings:
Warning 4269 Failed to parse saved optimizer context: "name" element not present at offset 1583.
Warning 4269 Failed to parse saved optimizer context: "name" element not present at offset 1616.
set @opt_context=json_remove(@saved_opt_context_1, '$.tables[0].ddl');
select * from t1 where a > 10;
a b
@@ -289,7 +289,7 @@ set @opt_context=json_remove(@saved_opt_context_1, '$.tables[0].file_stat_record
select * from t1 where a > 10;
a b
Warnings:
Warning 4269 Failed to parse saved optimizer context: "file_stat_records" element not present at offset 1576.
Warning 4269 Failed to parse saved optimizer context: "file_stat_records" element not present at offset 1609.
set @opt_context=json_remove(@saved_opt_context_1, '$.tables[0].indexes[0].index_name');
select * from t1 where a > 10;
a b

View File

@@ -0,0 +1,45 @@
#
# Optimizer context record/replay of the ARRAY (hlindex) records_in_range call
#
set @opt_context_var_name= 'opt_context';
create database db1;
use db1;
CREATE TABLE t1 (
id INT PRIMARY KEY,
tags JSON NOT NULL,
INDEX arr_idx (CAST(tags AS UNSIGNED ARRAY))
);
INSERT INTO t1 VALUES (1,'[1,2,3]'),(2,'[4,5,6]'),(3,'[3,6,9]'),(4,'[]'),(5,'[7]');
set optimizer_record_context=ON;
EXPLAIN SELECT id FROM t1 WHERE 3 MEMBER OF (tags);
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range arr_idx arr_idx 258 NULL 2 Using where
set @opt_context=
(select REGEXP_SUBSTR(
context,
'(?<=set @opt_context=\')([\n\r].*)*(?=\'\;#opt_context_ends)')
from information_schema.optimizer_context);
set @saved_ctx= @opt_context;
set optimizer_record_context=OFF;
# The call was recorded, with its cost:
select json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].key_number') as key_number,
json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].key') as k,
json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].num_records') as num_records,
json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].cost') is not null as has_cost;
key_number k num_records has_cost
1 "3" 2 1
# Replay with a different row count: EXPLAIN uses it
set optimizer_replay_context=@opt_context_var_name;
set @opt_context= json_replace(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].num_records', 4);
EXPLAIN SELECT id FROM t1 WHERE 3 MEMBER OF (tags);
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range arr_idx arr_idx 258 NULL 4 Using where
# Replay with an unrecorded value: match-failed warning
set @opt_context= @saved_ctx;
EXPLAIN SELECT id FROM t1 WHERE 5 MEMBER OF (tags);
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range arr_idx arr_idx 258 NULL 1 Using where
Warnings:
Warning 4270 Failed to match the stats from replay context with the optimizer stats: db1.t1 with key_number:1 with key:5 doesn't exist in hl_records_in_range_calls
set optimizer_replay_context='';
drop database db1;

View File

@@ -0,0 +1,38 @@
--source include/not_embedded.inc
--disable_replay testfile Dont replay a replay test
--echo #
--echo # Optimizer context record/replay of the ARRAY (hlindex) records_in_range call
--echo #
set @opt_context_var_name= 'opt_context';
create database db1;
use db1;
CREATE TABLE t1 (
id INT PRIMARY KEY,
tags JSON NOT NULL,
INDEX arr_idx (CAST(tags AS UNSIGNED ARRAY))
);
INSERT INTO t1 VALUES (1,'[1,2,3]'),(2,'[4,5,6]'),(3,'[3,6,9]'),(4,'[]'),(5,'[7]');
set optimizer_record_context=ON;
EXPLAIN SELECT id FROM t1 WHERE 3 MEMBER OF (tags);
--source include/opt_context_save_in_var.inc
set @saved_ctx= @opt_context;
set optimizer_record_context=OFF;
--echo # The call was recorded, with its cost:
select json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].key_number') as key_number,
json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].key') as k,
json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].num_records') as num_records,
json_extract(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].cost') is not null as has_cost;
--echo # Replay with a different row count: EXPLAIN uses it
set optimizer_replay_context=@opt_context_var_name;
set @opt_context= json_replace(@saved_ctx, '$.tables[0].hl_records_in_range_calls[0].num_records', 4);
EXPLAIN SELECT id FROM t1 WHERE 3 MEMBER OF (tags);
--echo # Replay with an unrecorded value: match-failed warning
set @opt_context= @saved_ctx;
EXPLAIN SELECT id FROM t1 WHERE 5 MEMBER OF (tags);
set optimizer_replay_context='';
drop database db1;

View File

@@ -20,6 +20,7 @@
#include "set_var.h"
#include "sql_class.h"
#include "opt_context_store_replay.h"
#include "index/hlindex.h" // hlindex (TABLE::hli)
#include "sql_show.h"
#include "my_json_writer.h"
#include "hash.h"
@@ -164,6 +165,23 @@ public:
String *err_buf);
};
/*
A record to hold one hlindex records_in_range() call (ARRAY keys):
*/
class hl_records_in_range_call_record : public Sql_alloc
{
public:
uint keynr;
char *key;
ha_rows records;
Cost_estimate cost;
static
hl_records_in_range_call_record* parse(MEM_ROOT *mem_root,
json_engine_t *je,
String *err_buf);
};
/*
structure to store all the index range records,
and the cost for reading indexes, pertaining to a table
@@ -177,6 +195,7 @@ public:
List<Multi_range_read_const_call_record> mrr_list;
List<cost_index_read_call_record> irc_list;
List<records_in_range_call_record> rir_list;
List<hl_records_in_range_call_record> hl_rir_list;
List<char> const_tbl_ins_stmt_list;
};
@@ -243,6 +262,22 @@ static bool is_base_table(const TABLE_LIST *tbl)
tbl->table->s->tmp_table != SYSTEM_TMP_TABLE);
}
static void dump_cost_estimate(const Cost_estimate *cost,
Json_writer *ctx_writer)
{
Json_writer_object obj(ctx_writer, "cost");
obj.add("avg_io_cost", cost->avg_io_cost);
obj.add("cpu_cost", cost->cpu_cost);
obj.add("comp_cost", cost->comp_cost);
obj.add("copy_cost", cost->copy_cost);
obj.add("limit_cost", cost->limit_cost);
obj.add("setup_cost", cost->setup_cost);
obj.add("index_cost_io", cost->index_cost.io);
obj.add("index_cost_cpu", cost->index_cost.cpu);
obj.add("row_cost_io", cost->row_cost.io);
obj.add("row_cost_cpu", cost->row_cost.cpu);
}
static
void dump_mrr_info_calls(List<Multi_range_read_const_call_record> *mrr_list,
Json_writer *ctx_writer)
@@ -263,19 +298,7 @@ void dump_mrr_info_calls(List<Multi_range_read_const_call_record> *mrr_list,
}
irc_wrapper.add("num_rows", irc->rows);
{
Json_writer_object obj(ctx_writer, "cost");
obj.add("avg_io_cost", irc->cost.avg_io_cost);
obj.add("cpu_cost", irc->cost.cpu_cost);
obj.add("comp_cost", irc->cost.comp_cost);
obj.add("copy_cost", irc->cost.copy_cost);
obj.add("limit_cost", irc->cost.limit_cost);
obj.add("setup_cost", irc->cost.setup_cost);
obj.add("index_cost_io", irc->cost.index_cost.io);
obj.add("index_cost_cpu", irc->cost.index_cost.cpu);
obj.add("row_cost_io", irc->cost.row_cost.io);
obj.add("row_cost_cpu", irc->cost.row_cost.cpu);
}
dump_cost_estimate(&irc->cost, ctx_writer);
irc_wrapper.add("mrr_mode", irc->mrr_mode);
irc_wrapper.add("max_index_blocks", irc->max_index_blocks);
@@ -325,6 +348,23 @@ void dump_records_in_range_calls(List<records_in_range_call_record> *rir_list,
}
}
static
void dump_hl_records_in_range_calls(
List<hl_records_in_range_call_record> *list, Json_writer *ctx_writer)
{
Json_writer_array wrapper(ctx_writer, "hl_records_in_range_calls");
List_iterator li(*list);
while (hl_records_in_range_call_record *rec= li++)
{
Json_writer_object obj(ctx_writer);
obj.add("key_number", rec->keynr);
obj.add("key", rec->key);
obj.add("num_records", rec->records);
dump_cost_estimate(&rec->cost, ctx_writer);
}
}
void Optimizer_context_recorder::dump_recorded_table_calls(
table_context_for_store *tbl,
Json_writer *writer)
@@ -332,6 +372,7 @@ void Optimizer_context_recorder::dump_recorded_table_calls(
dump_mrr_info_calls(&tbl->mrr_list, writer);
dump_index_read_calls(&tbl->irc_list, writer);
dump_records_in_range_calls(&tbl->rir_list, writer);
dump_hl_records_in_range_calls(&tbl->hl_rir_list, writer);
}
/*
@@ -1155,6 +1196,30 @@ void Optimizer_context_recorder::record_records_in_range(
table_ctx->rir_list.push_back(rec_in_range_ctx, mem_root);
}
void Optimizer_context_recorder::record_hl_records_in_range(
const TABLE *tbl, uint keynr, const uchar *value, size_t value_len,
ha_rows records, const Cost_estimate *cost)
{
auto *rec= new (mem_root) hl_records_in_range_call_record;
if (unlikely(!rec))
return; // OOM
rec->keynr= keynr;
StringBuffer<128> key(system_charset_info);
tbl->hli->print_key(&key, value, value_len);
if (!(rec->key= strdup_root(mem_root, &key)))
return; // OOM
rec->records= records;
rec->cost= *cost;
table_context_for_store *table_ctx= get_table_context(tbl);
if (unlikely(!table_ctx))
return; // OOM
table_ctx->hl_rir_list.push_back(rec, mem_root);
}
void Optimizer_context_recorder::record_table_row(TABLE *tbl, int row_index)
{
StringBuffer<512> output(&my_charset_utf8mb4_bin);
@@ -1267,6 +1332,7 @@ public:
List<Multi_range_read_const_call_record> ranges_list;
List<cost_index_read_call_record> irc_list;
List<records_in_range_call_record> rir_list;
List<hl_records_in_range_call_record> hl_rir_list;
static
table_context_for_replay *parse(MEM_ROOT *mem_root, json_engine_t *je,
@@ -1410,6 +1476,11 @@ table_context_for_replay::parse(MEM_ROOT *mem_root, json_engine_t *je,
Read_object_array<records_in_range_call_record>(
mem_root, &table_ctx->rir_list, records_in_range_call_record::parse),
true},
{"hl_records_in_range_calls",
Read_object_array<hl_records_in_range_call_record>(
mem_root, &table_ctx->hl_rir_list,
hl_records_in_range_call_record::parse),
true},
{NULL, Read_double(NULL), true}
};
@@ -1620,6 +1691,29 @@ records_in_range_call_record::parse(MEM_ROOT *mem_root,
return out;
}
hl_records_in_range_call_record*
hl_records_in_range_call_record::parse(MEM_ROOT *mem_root,
json_engine_t *je,
String *err_buf)
{
auto out= new (mem_root) hl_records_in_range_call_record;
if (!out)
return NULL;
Read_named_member members[]=
{
{"key_number", Read_non_neg_integer<uint, UINT_MAX>(&out->keynr), false},
{"key", Read_string(mem_root, &out->key), false},
{"num_records",
Read_non_neg_integer<ha_rows, ULONGLONG_MAX>(&out->records), false},
{"cost", Read_range_cost_estimate(mem_root, &out->cost), false},
{NULL, Read_double(NULL), true}
};
if (json_read_object(je, members, err_buf))
return NULL;
return out;
}
Optimizer_context_replay::Optimizer_context_replay(THD *thd_arg) : thd(thd_arg)
{
parse(); // TODO: error handling?
@@ -2007,6 +2101,47 @@ bool Optimizer_context_replay::infuse_records_in_range(
return true;
}
bool Optimizer_context_replay::infuse_hl_records_in_range(
const TABLE *tbl, uint keynr, const uchar *value, size_t value_len,
ha_rows *records, Cost_estimate *cost)
{
if (!has_records() || !is_base_table(tbl->pos_in_table_list))
return true;
StringBuffer<128> key(system_charset_info);
String tbl_name;
tbl->hli->print_key(&key, value, value_len);
append_base_table_name(tbl, &tbl_name);
if (table_context_for_replay *tbl_ctx=
find_table_context(tbl_name.c_ptr_safe()))
{
List_iterator<hl_records_in_range_call_record> iter(tbl_ctx->hl_rir_list);
while (hl_records_in_range_call_record *rec= iter++)
{
if (rec->keynr == keynr && !strcmp(rec->key, key.c_ptr_safe()))
{
*records= rec->records;
*cost= rec->cost;
return false;
}
}
}
String warn_msg(256);
warn_msg.append(tbl_name);
warn_msg.append(STRING_WITH_LEN(" with key_number:"));
warn_msg.append_ulonglong(keynr);
warn_msg.append(STRING_WITH_LEN(" with key:"));
warn_msg.append(key);
push_warning_printf(
thd, Sql_condition::WARN_LEVEL_WARN,
ER_JSON_OPTIMIZER_REPLAY_CONTEXT_MATCH_FAILED,
ER_THD(thd, ER_JSON_OPTIMIZER_REPLAY_CONTEXT_MATCH_FAILED),
warn_msg.c_ptr_safe(), "hl_records_in_range_calls");
return true;
}
/*
@brief
restore the saved stats for the tables, and indexes that were

View File

@@ -78,6 +78,9 @@ public:
const KEY_PART_INFO *key_part, uint keynr,
const key_range *min_range,
const key_range *max_range, ha_rows records);
void record_hl_records_in_range(const TABLE *tbl, uint keynr,
const uchar *value, size_t value_len,
ha_rows records, const Cost_estimate *cost);
void record_current_table_row(TABLE *tbl)
{
/* use table->record[0] */
@@ -212,6 +215,9 @@ public:
bool infuse_records_in_range(const TABLE *tbl, const KEY_PART_INFO *key_part,
uint keynr, const key_range *min_range,
const key_range *max_range, ha_rows *records);
bool infuse_hl_records_in_range(const TABLE *tbl, uint keynr,
const uchar *value, size_t value_len,
ha_rows *records, Cost_estimate *cost);
private:
THD *thd;

View File

@@ -489,6 +489,9 @@ static ha_rows hook_records_in_range(MEM_ROOT *mem_root, THD *thd,
const key_range *min_range,
const key_range *max_range,
page_range *pages);
static ha_rows hook_hl_records_in_range(THD *thd, TABLE *table, uint keynr,
const uchar *value, size_t value_len,
Cost_estimate *cost);
/*
SEL_IMERGE is a list of possible ways to do index merge, i.e. it is
@@ -12654,10 +12657,9 @@ ha_rows check_quick_select_array(PARAM *param, uint keynr, SEL_ARG *tree,
if (param->table->hlindex_open(keynr) || param->table->hlindex_lock(keynr))
return HA_POS_ERROR;
rows= param->table->hli->records_in_range(param->table,
param->table->key_info + keynr,
tree->min_value,
JSON_INDEX_KEY_MAX_LEN, cost);
rows= hook_hl_records_in_range(param->thd, param->table, keynr,
tree->min_value, JSON_INDEX_KEY_MAX_LEN,
cost);
param->quick_rows[keynr]= rows;
if (rows == HA_POS_ERROR)
return HA_POS_ERROR;
@@ -18157,3 +18159,30 @@ static ha_rows hook_records_in_range(MEM_ROOT *mem_root, THD *thd,
}
return records;
}
/*
@brief
Call hli->records_in_range(). If necessary,
- Replace its return value and cost from Optimizer Context, and/or
- Save them in the Optimizer Context we're recording.
@detail
This is the hlindex counterpart of hook_records_in_range().
*/
static ha_rows hook_hl_records_in_range(THD *thd, TABLE *table, uint keynr,
const uchar *value, size_t value_len,
Cost_estimate *cost)
{
ha_rows records= table->hli->records_in_range(table, table->key_info + keynr,
value, value_len, cost);
if (Optimizer_context_replay *repl= thd->opt_ctx_replay)
repl->infuse_hl_records_in_range(table, keynr, value, value_len, &records,
cost);
if (Optimizer_context_recorder *recorder= thd->opt_ctx_recorder)
recorder->record_hl_records_in_range(table, keynr, value, value_len,
records, cost);
return records;
}