diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 18:07:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 18:07:14 +0000 |
commit | a175314c3e5827eb193872241446f2f8f5c9d33c (patch) | |
tree | cd3d60ca99ae00829c52a6ca79150a5b6e62528b /sql/opt_trace.cc | |
parent | Initial commit. (diff) | |
download | mariadb-10.5-upstream.tar.xz mariadb-10.5-upstream.zip |
Adding upstream version 1:10.5.12.upstream/1%10.5.12upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sql/opt_trace.cc')
-rw-r--r-- | sql/opt_trace.cc | 748 |
1 files changed, 748 insertions, 0 deletions
diff --git a/sql/opt_trace.cc b/sql/opt_trace.cc new file mode 100644 index 00000000..ddec6d5e --- /dev/null +++ b/sql/opt_trace.cc @@ -0,0 +1,748 @@ +/* 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 + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "mariadb.h" +#include "sql_array.h" +#include "sql_string.h" +#include "sql_class.h" +#include "sql_show.h" +#include "field.h" +#include "sql_i_s.h" +#include "opt_trace.h" +#include "sql_parse.h" +#include "set_var.h" +#include "my_json_writer.h" +#include "sp_head.h" + +#include "rowid_filter.h" + +const char I_S_table_name[]= "OPTIMIZER_TRACE"; + +/** + Whether a list of tables contains information_schema.OPTIMIZER_TRACE. + @param tbl list of tables + + Can we do better than this here?? + @note this does not catch that a stored routine or view accesses + the OPTIMIZER_TRACE table. So using a stored routine or view to read + OPTIMIZER_TRACE will overwrite OPTIMIZER_TRACE as it runs and provide + uninteresting info. +*/ +bool list_has_optimizer_trace_table(const TABLE_LIST *tbl) +{ + for (; tbl; tbl= tbl->next_global) + { + if (tbl->schema_table && + 0 == strcmp(tbl->schema_table->table_name, I_S_table_name)) + return true; + } + return false; +} + +/* + Returns if a query has a set command with optimizer_trace being switched on/off. + True: Don't trace the query(uninteresting) +*/ + +bool sets_var_optimizer_trace(enum enum_sql_command sql_command, + List<set_var_base> *set_vars) +{ + if (sql_command == SQLCOM_SET_OPTION) + { + List_iterator_fast<set_var_base> it(*set_vars); + const set_var_base *var; + while ((var= it++)) + if (var->is_var_optimizer_trace()) return true; + } + return false; +} + + +namespace Show { + + +ST_FIELD_INFO optimizer_trace_info[]= +{ + Column("QUERY", Longtext(65535), NOT_NULL), + Column("TRACE", Longtext(65535), NOT_NULL), + Column("MISSING_BYTES_BEYOND_MAX_MEM_SIZE", SLong(20), NOT_NULL), + Column("INSUFFICIENT_PRIVILEGES", STiny(1), NOT_NULL), + CEnd() +}; + +} // namespace Show + + +/* + TODO: one-line needs to be implemented seperately +*/ +const char *Opt_trace_context::flag_names[]= {"enabled", "default", + NullS}; + +/* + Returns if a particular command will be traced or not +*/ + +inline bool sql_command_can_be_traced(enum enum_sql_command sql_command) +{ + /* + For first iteration we are only allowing select queries. + TODO: change to allow other queries. + */ + return sql_command == SQLCOM_SELECT || + sql_command == SQLCOM_UPDATE || + sql_command == SQLCOM_DELETE || + sql_command == SQLCOM_DELETE_MULTI || + sql_command == SQLCOM_UPDATE_MULTI; +} + +void opt_trace_print_expanded_query(THD *thd, SELECT_LEX *select_lex, + Json_writer_object *writer) + +{ + DBUG_ASSERT(thd->trace_started()); + + StringBuffer<1024> str(system_charset_info); + ulonglong save_option_bits= thd->variables.option_bits; + thd->variables.option_bits &= ~OPTION_QUOTE_SHOW_CREATE; + select_lex->print(thd, &str, + enum_query_type(QT_TO_SYSTEM_CHARSET | + QT_SHOW_SELECT_NUMBER | + QT_ITEM_IDENT_SKIP_DB_NAMES | + QT_VIEW_INTERNAL)); + thd->variables.option_bits= save_option_bits; + /* + The output is not very pretty lots of back-ticks, the output + is as the one in explain extended , lets try to improved it here. + */ + writer->add("expanded_query", str.c_ptr_safe(), str.length()); +} + +void opt_trace_disable_if_no_security_context_access(THD *thd) +{ + if (likely(!(thd->variables.optimizer_trace & + Opt_trace_context::FLAG_ENABLED)) || // (1) + thd->system_thread) // (2) + { + /* + (1) We know that the routine's execution starts with "enabled=off". + If it stays so until the routine ends, we needn't do security checks on + the routine. + If it does not stay so, it means the definer sets it to "on" somewhere + in the routine's body. Then it is his conscious decision to generate + traces, thus it is still correct to skip the security check. + + (2) Threads of the Events Scheduler have an unusual security context + (thd->m_main_security_ctx.priv_user==NULL, see comment in + Security_context::change_security_context()). + */ + return; + } + Opt_trace_context *const trace= &thd->opt_trace; + if (!thd->trace_started()) + { + /* + @@optimizer_trace has "enabled=on" but trace is not started. + Either Opt_trace_start ctor was not called for our statement (3), or it + was called but at that time, the variable had "enabled=off" (4). + + There are no known cases of (3). + + (4) suggests that the user managed to change the variable during + execution of the statement, and this statement is using + view/routine (note that we have not been able to provoke this, maybe + this is impossible). If it happens it is suspicious. + + We disable I_S output. And we cannot do otherwise: we have no place to + store a possible "missing privilege" information (no Opt_trace_stmt, as + is_started() is false), so cannot do security checks, so cannot safely + do tracing, so have to disable I_S output. And even then, we don't know + when to re-enable I_S output, as we have no place to store the + information "re-enable tracing at the end of this statement", and we + don't even have a notion of statement here (statements in the optimizer + trace world mean an Opt_trace_stmt object, and there is none here). So + we must disable for the session's life. + + COM_FIELD_LIST opens views, thus used to be a case of (3). To avoid + disabling I_S output for the session's life when this command is issued + (like in: "SET OPTIMIZER_TRACE='ENABLED=ON';USE somedb;" in the 'mysql' + command-line client), we have decided to create a Opt_trace_start for + this command. The command itself is not traced though + (SQLCOM_SHOW_FIELDS does not have CF_OPTIMIZER_TRACE). + */ + return; + } + /* + Note that thd->main_security_ctx.master_access is probably invariant + accross the life of THD: GRANT/REVOKE don't affect global privileges of an + existing connection, per the manual. + */ + if (!(thd->main_security_ctx.check_access(GLOBAL_ACLS & ~GRANT_ACL)) && + (0 != strcmp(thd->main_security_ctx.priv_user, + thd->security_context()->priv_user) || + 0 != my_strcasecmp(system_charset_info, + thd->main_security_ctx.priv_host, + thd->security_context()->priv_host))) + trace->missing_privilege(); +} + +void opt_trace_disable_if_no_stored_proc_func_access(THD *thd, sp_head *sp) +{ + if (likely(!(thd->variables.optimizer_trace & + Opt_trace_context::FLAG_ENABLED)) || + thd->system_thread || + !thd->trace_started()) + return; + + Opt_trace_context *const trace= &thd->opt_trace; + bool full_access; + Security_context *const backup_thd_sctx= thd->security_context(); + thd->set_security_context(&thd->main_security_ctx); + const bool rc= check_show_routine_access(thd, sp, &full_access) || !full_access; + thd->set_security_context(backup_thd_sctx); + if (rc) + trace->missing_privilege(); +} + +/** + If tracing is on, checks additional privileges on a list of tables/views, + to make sure that the user has the right to do SHOW CREATE TABLE/VIEW and + "SELECT *". For that: + - this functions checks table-level SELECT + - which is sufficient for SHOW CREATE TABLE and "SELECT *", if a base table + - if a view, if the view has not been identified as such then + opt_trace_disable_if_no_view_access() will be later called and check SHOW + VIEW; other we check SHOW VIEW here; SHOW VIEW + SELECT is sufficient for + SHOW CREATE VIEW. + If a privilege is missing, notifies the trace system. + + @param thd + @param tbl list of tables to check +*/ + +void opt_trace_disable_if_no_tables_access(THD *thd, TABLE_LIST *tbl) +{ + if (likely(!(thd->variables.optimizer_trace & + Opt_trace_context::FLAG_ENABLED)) || + thd->system_thread || + !thd->trace_started()) + return; + + Opt_trace_context *const trace= &thd->opt_trace; + Security_context *const backup_thd_sctx= thd->security_context(); + thd->set_security_context(&thd->main_security_ctx); + const TABLE_LIST *const first_not_own_table= thd->lex->first_not_own_table(); + for (TABLE_LIST *t= tbl; t != NULL && t != first_not_own_table; + t= t->next_global) + { + /* + Anonymous derived tables (as in + "SELECT ... FROM (SELECT ...)") don't have their grant.privilege set. + */ + if (!t->is_anonymous_derived_table()) + { + const GRANT_INFO backup_grant_info= t->grant; + Security_context *const backup_table_sctx= t->security_ctx; + t->security_ctx= NULL; + /* + (1) check_table_access() fills t->grant.privilege. + (2) Because SELECT privileges can be column-based, + check_table_access() will return 'false' as long as there is SELECT + privilege on one column. But we want a table-level privilege. + */ + + bool rc = + check_table_access(thd, SELECT_ACL, t, false, 1, true) || // (1) + ((t->grant.privilege & SELECT_ACL) == NO_ACL); // (2) + if (t->is_view()) + { + /* + It's a view which has already been opened: we are executing a + prepared statement. The view has been unfolded in the global list of + tables. So underlying tables will be automatically checked in the + present function, but we need an explicit check of SHOW VIEW: + */ + rc |= check_table_access(thd, SHOW_VIEW_ACL, t, false, 1, true); + } + t->security_ctx= backup_table_sctx; + t->grant= backup_grant_info; + if (rc) + { + trace->missing_privilege(); + break; + } + } + } + thd->set_security_context(backup_thd_sctx); + return; +} + +void opt_trace_disable_if_no_view_access(THD *thd, TABLE_LIST *view, + TABLE_LIST *underlying_tables) +{ + + if (likely(!(thd->variables.optimizer_trace & + Opt_trace_context::FLAG_ENABLED)) || + thd->system_thread || + !thd->trace_started()) + return; + + Opt_trace_context *const trace= &thd->opt_trace; + Security_context *const backup_table_sctx= view->security_ctx; + Security_context *const backup_thd_sctx= thd->security_context(); + const GRANT_INFO backup_grant_info= view->grant; + + view->security_ctx= NULL; // no SUID context for view + // no SUID context for THD + thd->set_security_context(&thd->main_security_ctx); + const int rc= check_table_access(thd, SHOW_VIEW_ACL, view, false, 1, true); + + view->security_ctx= backup_table_sctx; + thd->set_security_context(backup_thd_sctx); + view->grant= backup_grant_info; + + if (rc) + { + trace->missing_privilege(); + return; + } + /* + We needn't check SELECT privilege on this view. Some + opt_trace_disable_if_no_tables_access() call has or will check it. + + Now we check underlying tables/views of our view: + */ + opt_trace_disable_if_no_tables_access(thd, underlying_tables); + return; +} + + +/** + @class Opt_trace_stmt + + The trace of one statement. +*/ + +Opt_trace_stmt::Opt_trace_stmt(Opt_trace_context *ctx_arg) +{ + ctx= ctx_arg; + current_json= new Json_writer(); + missing_priv= false; + I_S_disabled= 0; +} + +Opt_trace_stmt::~Opt_trace_stmt() +{ + delete current_json; +} + +size_t Opt_trace_stmt::get_length() +{ + return current_json->output.length(); +} + +size_t Opt_trace_stmt::get_truncated_bytes() +{ + return current_json->get_truncated_bytes(); +} + +void Opt_trace_stmt::set_query(const char *query_ptr, size_t length, + const CHARSET_INFO *charset) +{ + query.append(query_ptr, length, charset); +} + +void Opt_trace_context::missing_privilege() +{ + if (current_trace) + current_trace->missing_privilege(); +} + +void Opt_trace_context::set_allowed_mem_size(size_t mem_size) +{ + current_trace->set_allowed_mem_size(mem_size); +} + +/* + TODO: In future when we would be saving multiple trace, + this function would return + max_mem_size - memory_occupied_by_the_saved_traces +*/ + +size_t Opt_trace_context::remaining_mem_size() +{ + return max_mem_size; +} + +/* + Disable tracing for children if the current trace is already present. + Currently only one trace is stored and there is no mechanism + to restore traces, so disabling tracing for children is the best option. +*/ + +bool Opt_trace_context::disable_tracing_if_required() +{ + if (current_trace) + { + current_trace->disable_tracing_for_children(); + return true; + } + return false; +} + +bool Opt_trace_context::enable_tracing_if_required() +{ + if (current_trace) + { + current_trace->enable_tracing_for_children(); + return true; + } + return false; +} + +bool Opt_trace_context::is_enabled() +{ + if (current_trace) + return current_trace->is_enabled(); + return false; +} + +Opt_trace_context::Opt_trace_context() : traces(PSI_INSTRUMENT_MEM) +{ + current_trace= NULL; + max_mem_size= 0; +} +Opt_trace_context::~Opt_trace_context() +{ + delete_traces(); +} + +void Opt_trace_context::set_query(const char *query, size_t length, const CHARSET_INFO *charset) +{ + current_trace->set_query(query, length, charset); +} + +void Opt_trace_context::start(THD *thd, TABLE_LIST *tbl, + enum enum_sql_command sql_command, + const char *query, + size_t query_length, + const CHARSET_INFO *query_charset, + ulong max_mem_size_arg) +{ + /* + This is done currently because we don't want to have multiple + traces open at the same time, so as soon as a new trace is created + we forcefully end the previous one, if it has not ended by itself. + This would mostly happen with stored functions or procedures. + + TODO: handle multiple traces + */ + DBUG_ASSERT(!current_trace); + current_trace= new Opt_trace_stmt(this); + max_mem_size= max_mem_size_arg; + set_allowed_mem_size(remaining_mem_size()); +} + +void Opt_trace_context::end() +{ + if (current_trace) + traces.push(current_trace); + + if (!traces.elements()) + return; + if (traces.elements() > 1) + { + Opt_trace_stmt *prev= traces.at(0); + delete prev; + traces.del(0); + } + current_trace= NULL; +} + +Opt_trace_start::Opt_trace_start(THD *thd, TABLE_LIST *tbl, + enum enum_sql_command sql_command, + List<set_var_base> *set_vars, + const char *query, + size_t query_length, + const CHARSET_INFO *query_charset):ctx(&thd->opt_trace) +{ + /* + if optimizer trace is enabled and the statment we have is traceable, + then we start the context. + */ + const ulonglong var= thd->variables.optimizer_trace; + traceable= FALSE; + if (unlikely(var & Opt_trace_context::FLAG_ENABLED) && + sql_command_can_be_traced(sql_command) && + !list_has_optimizer_trace_table(tbl) && + !sets_var_optimizer_trace(sql_command, set_vars) && + !thd->system_thread && + !ctx->disable_tracing_if_required()) + { + ctx->start(thd, tbl, sql_command, query, query_length, query_charset, + thd->variables.optimizer_trace_max_mem_size); + ctx->set_query(query, query_length, query_charset); + traceable= TRUE; + opt_trace_disable_if_no_tables_access(thd, tbl); + } +} + +Opt_trace_start::~Opt_trace_start() +{ + if (traceable) + { + ctx->end(); + traceable= FALSE; + } + else + { + ctx->enable_tracing_if_required(); + } +} + +void Opt_trace_stmt::fill_info(Opt_trace_info* info) +{ + if (unlikely(info->missing_priv= get_missing_priv())) + { + info->trace_ptr= info->query_ptr= ""; + info->trace_length= info->query_length= 0; + info->query_charset= &my_charset_bin; + info->missing_bytes= 0; + } + else + { + info->trace_ptr= current_json->output.get_string()->ptr(); + info->trace_length= get_length(); + info->query_ptr= query.ptr(); + info->query_length= query.length(); + info->query_charset= query.charset(); + info->missing_bytes= get_truncated_bytes(); + info->missing_priv= get_missing_priv(); + } +} + +void Opt_trace_stmt::missing_privilege() +{ + missing_priv= true; +} + +void Opt_trace_stmt::disable_tracing_for_children() +{ + ++I_S_disabled; +} + +void Opt_trace_stmt::enable_tracing_for_children() +{ + if (I_S_disabled) + --I_S_disabled; +} + +void Opt_trace_stmt::set_allowed_mem_size(size_t mem_size) +{ + current_json->set_size_limit(mem_size); +} + +/* + Prefer this when you are iterating over JOIN_TABs +*/ + +void Json_writer::add_table_name(const JOIN_TAB *tab) +{ + DBUG_ASSERT(tab->join->thd->trace_started()); + if (tab != NULL) + { + char table_name_buffer[SAFE_NAME_LEN]; + if (tab->table && tab->table->derived_select_number) + { + /* Derived table name generation */ + size_t len= my_snprintf(table_name_buffer, sizeof(table_name_buffer)-1, + "<derived%u>", + tab->table->derived_select_number); + add_str(table_name_buffer, len); + } + else if (tab->bush_children) + { + JOIN_TAB *ctab= tab->bush_children->start; + size_t len= my_snprintf(table_name_buffer, + sizeof(table_name_buffer)-1, + "<subquery%d>", + ctab->emb_sj_nest->sj_subq_pred->get_identifier()); + add_str(table_name_buffer, len); + } + else + { + TABLE_LIST *real_table= tab->table->pos_in_table_list; + add_str(real_table->alias.str, real_table->alias.length); + } + } + else + DBUG_ASSERT(0); +} + +void Json_writer::add_table_name(const TABLE *table) +{ + add_str(table->pos_in_table_list->alias.str); +} + + +void add_table_scan_values_to_trace(THD *thd, JOIN_TAB *tab) +{ + DBUG_ASSERT(thd->trace_started()); + Json_writer_object table_records(thd); + table_records.add_table_name(tab); + Json_writer_object table_rec(thd, "table_scan"); + table_rec.add("rows", tab->found_records) + .add("cost", tab->read_time); +} + + +/* + @brief + Add the tables inside a partial join to the optimizer trace + + @param join join handler + @param idx length of the partial QEP in 'join->positions' + @table_map map of all non-const tables of the join + + @note + This function is used during best_access_path to print the tables + inside the partial join that were considered doing the cost based + analysis of the various join orders. +*/ + +void trace_plan_prefix(JOIN *join, uint idx, table_map join_tables) +{ + THD *const thd= join->thd; + DBUG_ASSERT(thd->trace_started()); + + Json_writer_array plan_prefix(thd, "plan_prefix"); + for (uint i= 0; i < idx; i++) + { + TABLE_LIST *const tr= join->positions[i].table->tab_list; + if (!(tr->map & join_tables)) + plan_prefix.add_table_name(join->positions[i].table); + } +} + + +/* + Print the join order of all the tables for top level select. + + For example: + + select * from ot1 + where ot1.a IN (select it1.a from it1, it2 where it1.b=it2.a); + + So this function would print + ot1, <subquery2> ----> For select #1 +*/ + +void print_final_join_order(JOIN *join) +{ + DBUG_ASSERT(join->thd->trace_started()); + + Json_writer_object join_order(join->thd); + Json_writer_array best_order(join->thd, "best_join_order"); + JOIN_TAB *j; + uint i; + for (j= join->join_tab,i=0 ; i < join->top_join_tab_count; + i++, j++) + best_order.add_table_name(j); +} + + +void print_best_access_for_table(THD *thd, POSITION *pos, + enum join_type type) +{ + DBUG_ASSERT(thd->trace_started()); + + Json_writer_object obj(thd, "chosen_access_method"); + obj.add("type", type == JT_ALL ? "scan" : join_type_str[type]); + obj.add("records", pos->records_read); + obj.add("cost", pos->read_time); + obj.add("uses_join_buffering", pos->use_join_buffer); + if (pos->range_rowid_filter_info) + { + uint key_no= pos->range_rowid_filter_info->key_no; + obj.add("rowid_filter_key", + pos->table->table->key_info[key_no].name); + } +} + + +/* + Introduce enum_query_type flags parameter, maybe also allow + EXPLAIN also use this function. +*/ + +void Json_writer::add_str(Item *item) +{ + if (item) + { + THD *thd= current_thd; + StringBuffer<256> str(system_charset_info); + + ulonglong save_option_bits= thd->variables.option_bits; + thd->variables.option_bits &= ~OPTION_QUOTE_SHOW_CREATE; + item->print(&str, + enum_query_type(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER + | QT_ITEM_IDENT_SKIP_DB_NAMES)); + thd->variables.option_bits= save_option_bits; + add_str(str.c_ptr_safe()); + } + else + add_null(); +} + +void Opt_trace_context::delete_traces() +{ + if (traces.elements()) + { + while (traces.elements()) + { + Opt_trace_stmt *prev= traces.at(0); + delete prev; + traces.del(0); + } + } +} + + +int fill_optimizer_trace_info(THD *thd, TABLE_LIST *tables, Item *) +{ + TABLE *table= tables->table; + Opt_trace_info info; + + /* get_values of trace, query , missing bytes and missing_priv + + @todo: Need an iterator here to walk over all the traces + */ + Opt_trace_context* ctx= &thd->opt_trace; + + if (!thd->opt_trace.empty()) + { + Opt_trace_stmt *stmt= ctx->get_top_trace(); + stmt->fill_info(&info); + + table->field[0]->store(info.query_ptr, static_cast<uint>(info.query_length), + info.query_charset); + table->field[1]->store(info.trace_ptr, static_cast<uint>(info.trace_length), + system_charset_info); + table->field[2]->store(info.missing_bytes, true); + table->field[3]->store(info.missing_priv, true); + // Store in IS + if (schema_table_store_record(thd, table)) + return 1; + } + return 0; +} |