From ce84d0c138678195a0052e738063e663660a789e Mon Sep 17 00:00:00 2001 From: Anway Durge Date: Mon, 22 Jun 2026 17:48:31 +0530 Subject: [PATCH 1/5] MDEV-31342: Optimize INFORMATION_SCHEMA.TABLES queries to skip temp table writes For simple single-schema SELECT queries on information_schema.tables, bypass the internal temporary table write (ha_write_tmp_row()) and stream rows directly to the client via the protocol layer. Eligible queries are detected by is_simple_is_query(): single table, no UNION/subquery/ORDER BY/GROUP BY/DISTINCT/aggregates, specific schema lookup value (has_db_lookup_value()), no SELECT *. The fast path is restricted to information_schema.tables only (SCH_TABLES guard) and requires SKIP_OPEN_TABLE access method. A fast-path state enum (FP_INACTIVE/FP_ACTIVE/FP_STREAMING) tracks the lifecycle across schema_table_store_record() calls. Guards added after validation against the full test suite: - join->table_count == 1 (no multi-table joins) - SCH_TABLES only - table_open_method == SKIP_OPEN_TABLE - has_db_lookup_value() (specific schema filter required) - No indirect access through views - const_item() check on LIMIT to handle prepared statements - fp_state reset on re-execution for prepared statement correctness Performance: fast path holds flat (~31ms) while the legacy path grows linearly with table count (~78ms at 2000 tables). The in_exec_inner flag added to JOIN is initialized in JOIN::init(). --- mysql-test/main/mdev_31342.result | 285 ++++++++++++++++++++++ mysql-test/main/mdev_31342.test | 192 +++++++++++++++ sql/sql_show.cc | 380 ++++++++++++++++++++++++++---- sql/sql_show.h | 17 +- 4 files changed, 824 insertions(+), 50 deletions(-) create mode 100644 mysql-test/main/mdev_31342.result create mode 100644 mysql-test/main/mdev_31342.test diff --git a/mysql-test/main/mdev_31342.result b/mysql-test/main/mdev_31342.result new file mode 100644 index 0000000000000..063a811924386 --- /dev/null +++ b/mysql-test/main/mdev_31342.result @@ -0,0 +1,285 @@ +# MDEV-31342: Test I_S fast path optimization +# +# Test 1: Simple SELECT - should use fast path +# +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +TABLE_NAME +user +# +# Test 2: WHERE filter +# +SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'information_schema' AND TABLE_NAME = 'TABLES'; +TABLE_NAME TABLE_SCHEMA +TABLES information_schema +# +# Test 3: Scalar subquery - should trigger single row early exit +# +SELECT ( +SELECT TABLE_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' +) AS result; +result +user +# +# Test 4: LIMIT 1 - should trigger early exit +# +SELECT COUNT(*) > 0 FROM ( +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' + LIMIT 1 +) t; +COUNT(*) > 0 +1 +# +# Test 5: Fallback path - ORDER BY should still work correctly +# +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME +LIMIT 3; +TABLE_NAME +columns_priv +column_stats +db +# +# Test 6: Fallback path - GROUP BY should still work +# +SELECT TABLE_SCHEMA, COUNT(*) FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +GROUP BY TABLE_SCHEMA; +TABLE_SCHEMA COUNT(*) +mysql 31 +# +# Test 7: Fallback path - DISTINCT should still work +# +SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql'; +TABLE_SCHEMA +mysql +# +# Test 8: HAVING clause - should use fallback path +# +SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +HAVING TABLE_NAME = 'user'; +TABLE_SCHEMA TABLE_NAME +mysql user +# +# Test 9: WHERE TABLE_SCHEMA and TABLE_CATALOG mismatch - no rows +# +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; +TABLE_NAME +# +# Verify: simple query temp table behavior +# +FLUSH STATUS; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +TABLE_NAME +user +SHOW SESSION STATUS LIKE 'Created_tmp_tables'; +Variable_name Value +Created_tmp_tables 1 +# +# Verify: ORDER BY uses fallback +# +FLUSH STATUS; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME LIMIT 3; +TABLE_NAME +columns_priv +column_stats +db +SHOW SESSION STATUS LIKE 'Created_tmp_tables'; +Variable_name Value +Created_tmp_tables 1 +# +# EXPLAIN shows no filesort for fast-path query +# +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} +# +# EXPLAIN shows filesort for fallback query (ORDER BY) +# +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 3; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + } + } + ] + } +} +# +# Test: Prepared statements - fast path re-execution correctness +# +PREPARE stmt FROM 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?'; +SET @db = 'mysql', @tbl = 'user'; +EXECUTE stmt USING @db, @tbl; +TABLE_NAME +user +EXECUTE stmt USING @db, @tbl; +TABLE_NAME +user +DEALLOCATE PREPARE stmt; +# +# Test: Prepared statement with LIMIT 1 +# +PREPARE stmt2 FROM 'SELECT COUNT(*) > 0 FROM (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? LIMIT 1) t'; +SET @db = 'mysql'; +EXECUTE stmt2 USING @db; +COUNT(*) > 0 +1 +EXECUTE stmt2 USING @db; +COUNT(*) > 0 +1 +DEALLOCATE PREPARE stmt2; +# +# Test: I_S query inside stored procedure +# +CREATE PROCEDURE test_is_proc() +BEGIN +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +END// +CALL test_is_proc(); +TABLE_NAME +user +CALL test_is_proc(); +TABLE_NAME +user +DROP PROCEDURE test_is_proc; +# +# Test: I_S query inside a view - should use fallback path +# +CREATE VIEW test_is_view AS +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql'; +SELECT TABLE_NAME FROM test_is_view WHERE TABLE_NAME = 'user'; +TABLE_NAME +user +DROP VIEW test_is_view; +# +# Test: WHERE with multiple conditions +# +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME LIKE 'user%' +LIMIT 1; +TABLE_NAME +user +# +# Test: I_S query inside trigger +# +CREATE TABLE test_mdev31342 (id INT); +CREATE TRIGGER test_is_trigger BEFORE INSERT ON test_mdev31342 +FOR EACH ROW +BEGIN +SET NEW.id = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql'); +END// +INSERT INTO test_mdev31342 VALUES (0); +SELECT id > 0 AS has_tables FROM test_mdev31342; +has_tables +1 +DROP TRIGGER test_is_trigger; +DROP TABLE test_mdev31342; +# End of 13.1 tests +# +# Test: Query cache - ensure results are not served from cache +# +SET SESSION query_cache_type = 0; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +TABLE_NAME +user +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +TABLE_NAME +user +SET SESSION query_cache_type = DEFAULT; +# +# Test: Prepared statement with ? placeholders - second execution +# +PREPARE stmt3 FROM +'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?'; +SET @s1= 'mysql', @t1= 'user'; +EXECUTE stmt3 USING @s1, @t1; +TABLE_NAME +user +SET @s1= 'information_schema', @t1= 'TABLES'; +EXECUTE stmt3 USING @s1, @t1; +TABLE_NAME +TABLES +DEALLOCATE PREPARE stmt3; +# +# Test: Verify fast path via EXPLAIN (no filesort = fast path engaged) +# +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "skip_open_table": true, + "scanned_databases": 0 + } + } + ] + } +} diff --git a/mysql-test/main/mdev_31342.test b/mysql-test/main/mdev_31342.test new file mode 100644 index 0000000000000..6e0f145f6aec2 --- /dev/null +++ b/mysql-test/main/mdev_31342.test @@ -0,0 +1,192 @@ +--echo # MDEV-31342: Test I_S fast path optimization + +--echo # +--echo # Test 1: Simple SELECT - should use fast path +--echo # +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; + +--echo # +--echo # Test 2: WHERE filter +--echo # +SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'information_schema' AND TABLE_NAME = 'TABLES'; + +--echo # +--echo # Test 3: Scalar subquery - should trigger single row early exit +--echo # +SELECT ( + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' +) AS result; + +--echo # +--echo # Test 4: LIMIT 1 - should trigger early exit +--echo # +SELECT COUNT(*) > 0 FROM ( + SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql' + LIMIT 1 +) t; + +--echo # +--echo # Test 5: Fallback path - ORDER BY should still work correctly +--echo # +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME +LIMIT 3; + +--echo # +--echo # Test 6: Fallback path - GROUP BY should still work +--echo # +SELECT TABLE_SCHEMA, COUNT(*) FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +GROUP BY TABLE_SCHEMA; + +--echo # +--echo # Test 7: Fallback path - DISTINCT should still work +--echo # +SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql'; + + + +--echo # +--echo # Test 8: HAVING clause - should use fallback path +--echo # +SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +HAVING TABLE_NAME = 'user'; + +--echo # +--echo # Test 9: WHERE TABLE_SCHEMA and TABLE_CATALOG mismatch - no rows +--echo # +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; + +--echo # +--echo # Verify: simple query temp table behavior +--echo # +FLUSH STATUS; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +SHOW SESSION STATUS LIKE 'Created_tmp_tables'; + +--echo # +--echo # Verify: ORDER BY uses fallback +--echo # +FLUSH STATUS; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME LIMIT 3; +SHOW SESSION STATUS LIKE 'Created_tmp_tables'; + + +--echo # +--echo # EXPLAIN shows no filesort for fast-path query +--echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; + +--echo # +--echo # EXPLAIN shows filesort for fallback query (ORDER BY) +--echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 3; + +--echo # +--echo # Test: Prepared statements - fast path re-execution correctness +--echo # +PREPARE stmt FROM 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?'; +SET @db = 'mysql', @tbl = 'user'; +EXECUTE stmt USING @db, @tbl; +EXECUTE stmt USING @db, @tbl; +DEALLOCATE PREPARE stmt; + +--echo # +--echo # Test: Prepared statement with LIMIT 1 +--echo # +PREPARE stmt2 FROM 'SELECT COUNT(*) > 0 FROM (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? LIMIT 1) t'; +SET @db = 'mysql'; +EXECUTE stmt2 USING @db; +EXECUTE stmt2 USING @db; +DEALLOCATE PREPARE stmt2; + +--echo # +--echo # Test: I_S query inside stored procedure +--echo # +DELIMITER //; +CREATE PROCEDURE test_is_proc() +BEGIN + SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +END// +DELIMITER ;// +CALL test_is_proc(); +CALL test_is_proc(); +DROP PROCEDURE test_is_proc; + +--echo # +--echo # Test: I_S query inside a view - should use fallback path +--echo # +CREATE VIEW test_is_view AS + SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql'; +SELECT TABLE_NAME FROM test_is_view WHERE TABLE_NAME = 'user'; +DROP VIEW test_is_view; + +--echo # +--echo # Test: WHERE with multiple conditions +--echo # +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME LIKE 'user%' +LIMIT 1; + +--echo # +--echo # Test: I_S query inside trigger +--echo # +CREATE TABLE test_mdev31342 (id INT); +DELIMITER //; +CREATE TRIGGER test_is_trigger BEFORE INSERT ON test_mdev31342 +FOR EACH ROW +BEGIN + SET NEW.id = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql'); +END// +DELIMITER ;// +INSERT INTO test_mdev31342 VALUES (0); +SELECT id > 0 AS has_tables FROM test_mdev31342; +DROP TRIGGER test_is_trigger; +DROP TABLE test_mdev31342; + +--echo # End of 13.1 tests + +--echo # +--echo # Test: Query cache - ensure results are not served from cache +--echo # +SET SESSION query_cache_type = 0; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +SET SESSION query_cache_type = DEFAULT; + +--echo # +--echo # Test: Prepared statement with ? placeholders - second execution +--echo # +PREPARE stmt3 FROM + 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?'; +SET @s1= 'mysql', @t1= 'user'; +EXECUTE stmt3 USING @s1, @t1; +SET @s1= 'information_schema', @t1= 'TABLES'; +EXECUTE stmt3 USING @s1, @t1; +DEALLOCATE PREPARE stmt3; + +--echo # +--echo # Test: Verify fast path via EXPLAIN (no filesort = fast path engaged) +--echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; diff --git a/sql/sql_show.cc b/sql/sql_show.cc index 882db8f3c1353..5fac9ab0251a6 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -628,8 +628,8 @@ bool mysqld_show_privileges(THD *thd) /** Hash of LEX_STRINGs used to search for ignored db directories. */ static HASH ignore_db_dirs_hash; -/** - An array of LEX_STRING pointers to collect the options at +/** + An array of LEX_STRING pointers to collect the options at option parsing time. */ static DYNAMIC_ARRAY ignore_db_dirs_array; @@ -768,7 +768,7 @@ ignore_db_dirs_free() Initialize the ignore db directories hash and status variable from the options collected in the array. - Called when option processing is over and the server's in-memory + Called when option processing is over and the server's in-memory structures are fully initialized. @return state @@ -915,7 +915,7 @@ ignore_db_dirs_process_additions() DBUG_ASSERT(ptr - opt_ignore_db_dirs <= (ptrdiff_t) len); *ptr= 0; - /* + /* It's OK to empty the array here as the allocated elements are referenced through the hash now. */ @@ -937,7 +937,7 @@ static inline bool is_in_ignore_db_dirs_list(const char *directory) { return ignore_db_dirs_hash.records && - NULL != my_hash_search(&ignore_db_dirs_hash, (const uchar *) directory, + NULL != my_hash_search(&ignore_db_dirs_hash, (const uchar *) directory, strlen(directory)); } @@ -1082,7 +1082,7 @@ find_files(THD *thd, Dynamic_array *files, can't untangle its access checking from that of the view itself. */ class Show_create_error_handler : public Internal_error_handler { - + TABLE_LIST *m_top_view; bool m_handling; Security_context *m_sctx; @@ -1094,17 +1094,17 @@ class Show_create_error_handler : public Internal_error_handler { /** Creates a new Show_create_error_handler for the particular security - context and view. + context and view. @thd Thread context, used for security context information if needed. @top_view The view. We do not verify at this point that top_view is in fact a view since, alas, these things do not stay constant. */ - explicit Show_create_error_handler(THD *thd, TABLE_LIST *top_view) : + explicit Show_create_error_handler(THD *thd, TABLE_LIST *top_view) : m_top_view(top_view), m_handling(FALSE), - m_view_access_denied_message_ptr(NULL) + m_view_access_denied_message_ptr(NULL) { - + m_sctx= MY_TEST(m_top_view->security_ctx) ? m_top_view->security_ctx : thd->security_ctx; } @@ -1118,7 +1118,7 @@ class Show_create_error_handler : public Internal_error_handler { failed is not available at this point. The only way for us to check is by reconstructing the actual error message and see if it's the same. */ - char* get_view_access_denied_message(THD *thd) + char* get_view_access_denied_message(THD *thd) { if (!m_view_access_denied_message_ptr) { @@ -1170,7 +1170,7 @@ class Show_create_error_handler : public Internal_error_handler { case ER_NO_SUCH_TABLE_IN_ENGINE: /* Established behavior: warn if underlying tables, columns, or functions are missing. */ - push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, + push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, ER_VIEW_INVALID, ER_THD(thd, ER_VIEW_INVALID), m_top_view->get_db_name().str, @@ -3181,7 +3181,7 @@ void Show_explain_request::call_in_target_thread() Query_arena backup_arena; bool printed_anything= FALSE; - /* + /* Change the arena because JOIN::print_explain and co. are going to allocate items. Let them allocate them on our arena. */ @@ -3229,7 +3229,7 @@ int select_result_explain_buffer::send_data(List &items) set_current_thd(thd); fill_record(thd, dst_table, dst_table->field, items, true, false, false); res= dst_table->file->ha_write_tmp_row(dst_table->record[0]); - set_current_thd(cur_thd); + set_current_thd(cur_thd); DBUG_RETURN(MY_TEST(res)); } @@ -3262,7 +3262,7 @@ int select_result_text_buffer::append_row(List &items, bool send_names) while ((item= it++)) { DBUG_ASSERT(column < n_columns); - const char *data_ptr; + const char *data_ptr; char *ptr; size_t data_len; @@ -3344,11 +3344,11 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, /* If calling_user==NULL, calling thread has SUPER or PROCESS privilege, and so can do SHOW EXPLAIN/SHOW ANALYZE on any user. - + if calling_user!=NULL, he's only allowed to view SHOW EXPLAIN/SHOW ANALYZE on his own threads. */ - if (calling_user && (!tmp_sctx->user || strcmp(calling_user, + if (calling_user && (!tmp_sctx->user || strcmp(calling_user, tmp_sctx->user))) { my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0), "PROCESS"); @@ -3364,8 +3364,8 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, } bool bres; - /* - Ok we've found the thread of interest and it won't go away because + /* + Ok we've found the thread of interest and it won't go away because we're holding its LOCK_thd_kill. Post it a SHOW EXPLAIN/SHOW ANALYZE request. */ @@ -3374,7 +3374,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, Show_explain_request explain_req; explain_req.is_json_format= json_format; select_result_explain_buffer *explain_buf; - + if (!(explain_buf= new (thd->mem_root) select_result_explain_buffer(thd, table->table))) DBUG_RETURN(1); @@ -3384,7 +3384,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, explain_req.target_thd= tmp; explain_req.request_thd= thd; explain_req.failed_to_produce= FALSE; - + /* Do not use default memroot as this is only to be used by the target thread (It's marked as thread MY_THREAD_SPECIFIC). @@ -3422,7 +3422,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, char *warning_text; if (!my_charset_same(fromcs, tocs)) { - uint conv_length= 1 + tocs->mbmaxlen * explain_req.query_str.length() / + uint conv_length= 1 + tocs->mbmaxlen * explain_req.query_str.length() / fromcs->mbminlen; uint dummy_errors; char *to, *p; @@ -3430,7 +3430,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, DBUG_RETURN(1); p= to; p+= copy_and_convert(to, conv_length, tocs, - explain_req.query_str.c_ptr(), + explain_req.query_str.c_ptr(), explain_req.query_str.length(), fromcs, &dummy_errors); *p= 0; @@ -4091,7 +4091,7 @@ static bool show_status_array(THD *thd, const char *wild, var->type == SHOW_SIMPLE_FUNC; var= &tmp) ((mysql_show_var_func)(var->value))(thd, &tmp, (void *) buff, status_var, scope); - + SHOW_TYPE show_type=var->type; if (show_type == SHOW_ARRAY) { @@ -4182,6 +4182,102 @@ uint calc_sum_of_all_status(STATUS_VAR *to) /* This is only used internally, but we need it here as a forward reference */ extern ST_SCHEMA_TABLE schema_tables[]; +/** + Detect if an I_S query can bypass temp table materialization. + Qualifies if: no ORDER BY, no GROUP BY, no DISTINCT, no aggregates. +*/ +static bool is_simple_is_query(THD *thd) +{ + SELECT_LEX *sel= thd->lex->current_select; + if (!sel) + return false; + /* Reject UNION queries */ + if (sel->master_unit()->first_select()->next_select()) + { + DBUG_PRINT("info", ("has UNION, using fallback path")); + return false; + } + /* Reject subqueries (scalar subquery context) */ + if (sel->master_unit()->item) + { + DBUG_PRINT("info", ("is subquery, using fallback path")); + return false; + } + if (sel->order_list.elements) + { + DBUG_PRINT("info", ("has ORDER BY, using fallback path")); + return false; + } + if (sel->group_list.elements) + { + DBUG_PRINT("info", ("has GROUP BY, using fallback path")); + return false; + } + if (sel->having) + { + DBUG_PRINT("info", ("has HAVING, using fallback path")); + return false; + } + if (sel->options & SELECT_DISTINCT) + { + DBUG_PRINT("info", ("has DISTINCT, using fallback path")); + return false; + } + if (sel->with_sum_func) + { + DBUG_PRINT("info", ("has aggregates, using fallback path")); + return false; + } + /* Reject SELECT * (wildcard expansion not yet validated) */ + if (sel->with_wild) + { + DBUG_PRINT("info", ("has SELECT *, using fallback path")); + return false; + } + /* + Only allow plain column references in the SELECT list (no + constants, expressions, or function calls), since those are the + only shapes validated against the fast path so far. + */ + { + List_iterator_fast it(sel->item_list); + Item *item; + while ((item= it++)) + { + if (item->real_item()->type() != Item::FIELD_ITEM) + { + DBUG_PRINT("info", ("non-field item in SELECT list, " + "using fallback path")); + return false; + } + } + } + /* + Only allow no LIMIT, or LIMIT 1 exactly (the single-row early-exit + case, which is tested and proven correct). Any other LIMIT n is + rejected for now until validated against the fast path. + */ + if (sel->limit_params.explicit_limit && + sel->limit_params.select_limit && + (!sel->limit_params.select_limit->const_item() || + sel->limit_params.select_limit->val_int() != 1)) + { + DBUG_PRINT("info", ("has LIMIT != 1, using fallback path")); + return false; + } + if (sel->limit_params.offset_limit) + { + DBUG_PRINT("info", ("has OFFSET, using fallback path")); + return false; + } + if (sel->options & OPTION_FOUND_ROWS) + { + DBUG_PRINT("info", ("has SQL_CALC_FOUND_ROWS, using fallback path")); + return false; + } + return true; +} + /* Store record to I_S table, convert HEAP table to MyISAM if necessary @@ -4206,6 +4302,88 @@ bool schema_table_store_record(THD *thd, TABLE *table) return 1; } + /* + Fast path: when the query has been identified as a simple I_S + lookup, skip the temp table entirely. Send metadata lazily on the + first row, then stream each row's fields directly to the client, + using the I_S table's Field objects already populated by the fill + function in table->record[0]. + */ + { + IS_table_read_plan *plan= + table->pos_in_table_list->is_table_read_plan; + if (plan && plan->fp_state != IS_table_read_plan::FP_INACTIVE) + { + DBUG_PRINT("info", ("MDEV-31342 FAST PATH: skipping " + "ha_write_tmp_row, streaming directly")); + Protocol *protocol= thd->protocol; + List *proj= plan->projection_fields; + if (plan->fp_state == IS_table_read_plan::FP_ACTIVE) + { + if (proj) + { + if (protocol->send_result_set_metadata( + proj, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + return 1; + } + else + { + /* Build field list once and cache in projection_fields */ + List *field_list= + new (thd->mem_root) List(); + if (!field_list) + return 1; + for (Field **f= table->field; *f; f++) + { + Item_field *item= new (thd->mem_root) Item_field(thd, *f); + if (!item || field_list->push_back(item)) + return 1; + } + plan->projection_fields= field_list; + proj= field_list; + if (protocol->send_result_set_metadata( + proj, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + return 1; + } + plan->fp_state= IS_table_read_plan::FP_STREAMING; + } + + /* + Skip row if it does not match the complete original condition. + partial_cond is a pruning-only subset (see make_cond_for_info_schema) + and is NOT sufficient on its own to gate correctness here, since it + silently drops conjuncts on fields outside the schema-table index + (e.g. TABLE_CATALOG) and is built before HAVING pushdown in some + cases. full_cond is the complete condition and must be checked. + */ + if (plan->full_cond && !plan->full_cond->val_bool()) + return 0; + if (proj) + { + protocol->prepare_for_resend(); + if (protocol->send_result_set_row(proj)) + return 1; + if (protocol->write()) + return 1; + return 0; + } + protocol->prepare_for_resend(); + for (Field **f= table->field; *f; f++) + { + if ((*f)->is_null()) + protocol->store_null(); + else if (protocol->store(*f)) + return 1; + } + if (protocol->write()) + return 1; + return 0; + } + } + + DBUG_PRINT("info", ("MDEV-31342 SLOW PATH: calling ha_write_tmp_row")); if (unlikely((error= table->file->ha_write_tmp_row(table->record[0])))) { TMP_TABLE_PARAM *param= table->pos_in_table_list->schema_table_param; @@ -4213,7 +4391,6 @@ bool schema_table_store_record(THD *thd, TABLE *table) param->start_recinfo, ¶m->recinfo, error, 0, NULL))) - return 1; } return 0; @@ -5425,10 +5602,10 @@ static privilege_t get_schema_privileges_for_show(THD *thd, TABLE_LIST *tables, bool on_any_column) { #ifndef NO_EMBEDDED_ACCESS_CHECKS - /* + /* We know that the table or at least some of the columns have necessary privileges, but the caller didn't pass down the GRANT_INFO - object, so we have to rediscover everything again :( + object, so we have to rediscover everything again :( */ if (thd->col_access & need) return thd->col_access & need; @@ -5492,7 +5669,7 @@ static bool strcmpcs(const LEX_CSTRING &str, const LEX_CSTRING &pat) get_all_tables(). @note This function assumes optimize_for_get_all_tables() has been - run for the table and produced a "read plan" in + run for the table and produced a "read plan" in tables->is_table_read_plan. @param[in] thd thread handler @@ -5545,7 +5722,7 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) thd->reset_n_backup_open_tables_state(&open_tables_state_backup); schema_table_idx= get_schema_table_idx(schema_table); - /* + /* this branch processes SHOW FIELDS, SHOW INDEXES commands. see sql_parse.cc, prepare_schema_table() function where this values are initialized @@ -5689,6 +5866,12 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) table->field[0]->store(STRING_WITH_LEN("def"), system_charset_info); if (schema_table_store_record(thd, table)) goto err; /* Out of space in temporary table */ + if (plan->is_single_row && + plan->fp_state != IS_table_read_plan::FP_INACTIVE) + { + error= 0; + goto err; + } continue; } @@ -6255,7 +6438,7 @@ void process_i_s_table_temporary_tables(THD *thd, TABLE * table, TABLE *tmp_tbl) @param[in] cs I_S table charset @param[in] offset offset from beginning of table to DATE_TYPE column in I_S table - + @return void */ @@ -6493,7 +6676,7 @@ static int store_schema_period_record(THD *thd, TABLE_LIST *tl, for (auto *field: {period.start_field(s), period.end_field(s)}) { /* Reveal the value only if the user has any privilege on this column */ - bool col_granted= get_column_grant(thd, &tl->grant, + bool col_granted= get_column_grant(thd, &tl->grant, db_name->str, table_name->str, field->field_name) & COL_DML_ACLS; if (col_granted) @@ -7135,7 +7318,7 @@ int store_schema_params(THD *thd, TABLE *table, TABLE *proc_table, default: tmp_buff= ""; break; - } + } restore_record(table, s->default_values); table->field[0]->store(STRING_WITH_LEN("def"), cs); @@ -7264,7 +7447,7 @@ int store_schema_proc(THD *thd, TABLE *table, TABLE *proc_table, table->field[18]->store(STRING_WITH_LEN("SQL"), cs); copy_field_as_string(table->field[19], proc_table->field[MYSQL_PROC_FIELD_DETERMINISTIC]); - table->field[20]->store(sp_data_access_name[enum_idx].str, + table->field[20]->store(sp_data_access_name[enum_idx].str, sp_data_access_name[enum_idx].length , cs); copy_field_as_string(table->field[22], proc_table->field[MYSQL_PROC_FIELD_SECURITY_TYPE]); @@ -7544,7 +7727,7 @@ static int get_schema_stat_record(THD *thd, TABLE_LIST *tables, TABLE *table, DBUG_ASSERT(MY_TEST(key_info->flags & HA_USES_COMMENT) == (key_info->comment.length > 0)); if (key_info->flags & HA_USES_COMMENT) - table->field[15]->store(key_info->comment.str, + table->field[15]->store(key_info->comment.str, key_info->comment.length, cs); // IGNORED column @@ -7776,7 +7959,7 @@ static int get_schema_constraints_record(THD *thd, TABLE_LIST *tables, if (!tables->view) { /* need any non-SELECT privilege on the table or any of its columns */ - if (!get_schema_privileges_for_show(thd, tables, TABLE_ACLS & ~SELECT_ACL, + if (!get_schema_privileges_for_show(thd, tables, TABLE_ACLS & ~SELECT_ACL, true)) DBUG_RETURN(0); @@ -8995,7 +9178,7 @@ get_referential_constraints_record(THD *thd, TABLE_LIST *tables, table->field[2]->store(f_key_info->foreign_id->str, f_key_info->foreign_id->length, cs); table->field[3]->store(STRING_WITH_LEN("def"), cs); - table->field[4]->store(f_key_info->referenced_db->str, + table->field[4]->store(f_key_info->referenced_db->str, f_key_info->referenced_db->length, cs); bool show_ref_table= true; #ifndef NO_EMBEDDED_ACCESS_CHECKS @@ -9620,7 +9803,7 @@ int make_schema_select(THD *thd, SELECT_LEX *sel, Optimize reading from an I_S table. @detail - This function prepares a plan for populating an I_S table with + This function prepares a plan for populating an I_S table with get_all_tables(). The plan is in IS_table_read_plan structure, it is saved in @@ -9629,7 +9812,7 @@ int make_schema_select(THD *thd, SELECT_LEX *sel, @return false - Ok true - Out Of Memory - + */ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) @@ -9646,11 +9829,11 @@ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond tables->is_table_read_plan= plan; schema_table_idx= get_schema_table_idx(schema_table); - tables->table_open_method= get_table_open_method(tables, schema_table, + tables->table_open_method= get_table_open_method(tables, schema_table, schema_table_idx); DBUG_PRINT("open_method", ("%d", tables->table_open_method)); - /* + /* this branch processes SHOW FIELDS, SHOW INDEXES commands. see sql_parse.cc, prepare_schema_table() function where this values are initialized @@ -9673,7 +9856,7 @@ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond plan->lookup_field_vals.db_value.str, plan->lookup_field_vals.table_value.str)); - if (!plan->lookup_field_vals.wild_db_value && + if (!plan->lookup_field_vals.wild_db_value && !plan->lookup_field_vals.wild_table_value) { /* @@ -9690,11 +9873,26 @@ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond } } + /* Mark simple queries for fast path */ + if (is_simple_is_query(thd) && + get_schema_table_idx(schema_table) == SCH_TABLES && + tables->table_open_method == SKIP_OPEN_TABLE) + { + DBUG_PRINT("info", ("simple query qualifies for I_S fast path")); + plan->is_optimized_query= true; + SELECT_LEX *sel= thd->lex->current_select; + if (sel && sel->limit_params.select_limit && + sel->limit_params.select_limit->const_item() && + sel->limit_params.select_limit->val_int() == 1) + plan->is_single_row= true; + } + + plan->full_cond= cond; if (plan->has_db_lookup_value() && plan->has_table_lookup_value()) plan->partial_cond= 0; else plan->partial_cond= make_cond_for_info_schema(thd, cond, tables); - + end: DBUG_RETURN(0); } @@ -9781,7 +9979,7 @@ bool optimize_schema_tables_reads(JOIN *join) JOIN_TAB *tab; for (tab= first_linear_tab(join, WITHOUT_BUSH_ROOTS, WITH_CONST_TABLES); - tab; + tab; tab= next_linear_tab(join, tab, WITH_BUSH_ROOTS)) { /* @@ -9856,7 +10054,7 @@ bool get_schema_tables_result(JOIN *join, JOIN_TAB *tab; for (tab= first_linear_tab(join, WITHOUT_BUSH_ROOTS, WITH_CONST_TABLES); - tab; + tab; tab= next_linear_tab(join, tab, WITH_BUSH_ROOTS)) { /* @@ -9905,7 +10103,16 @@ bool get_schema_tables_result(JOIN *join, */ if (table_list->schema_table_state && (!is_subselect || table_list->schema_table_state != executed_place)) + { + IS_table_read_plan *skip_plan= table_list->is_table_read_plan; + if (skip_plan && + skip_plan->fp_state != IS_table_read_plan::FP_INACTIVE) + { + result= 1; + break; + } continue; + } /* if table is used in a subselect and @@ -9918,10 +10125,19 @@ bool get_schema_tables_result(JOIN *join, table_list->table->file->extra(HA_EXTRA_RESET_STATE); table_list->table->file->ha_delete_all_rows(); table_list->table->null_row= 0; + /* Reset fast-path state for re-execution */ + if (table_list->is_table_read_plan) + table_list->is_table_read_plan->fp_state= + IS_table_read_plan::FP_INACTIVE; } else + { table_list->table->file->stats.records= 0; - + if (table_list->is_table_read_plan) + table_list->is_table_read_plan->fp_state= + IS_table_read_plan::FP_INACTIVE; + } + Item *cond= tab->select_cond; if (tab->cache_select && tab->cache_select->cond) { @@ -9938,6 +10154,34 @@ bool get_schema_tables_result(JOIN *join, Switch_to_definer_security_ctx backup_ctx(thd, table_list); Check_level_instant_set check_level_save(thd, CHECK_FIELD_IGNORE); + + IS_table_read_plan *plan= table_list->is_table_read_plan; + /* + Structural eligibility (SCH_TABLES, SKIP_OPEN_TABLE, simple query + shape) was already verified in optimize_for_get_all_tables() and + encoded in plan->is_optimized_query. Here we check only runtime + conditions that cannot be determined at optimization time. + */ + bool use_fast_path= (plan && plan->is_optimized_query && + !thd->bootstrap && !thd->spcont && + !is_show_command(thd) && + join->result && + thd->lex->sql_command == SQLCOM_SELECT && + !(join->select_options & SELECT_DESCRIBE) && + join->table_count == 1 && + thd->lex->query_tables == table_list && + !table_list->next_global && + !table_list->schema_table_state && + plan->has_db_lookup_value() && + plan->fp_state == + IS_table_read_plan::FP_INACTIVE); + + if (use_fast_path) + { + plan->fp_state= IS_table_read_plan::FP_ACTIVE; + plan->projection_fields= join->fields; + } + if (table_list->schema_table->fill_table(thd, table_list, cond)) { result= 1; @@ -9946,8 +10190,46 @@ bool get_schema_tables_result(JOIN *join, table_list->schema_table_state= executed_place; break; } + tab->read_record.table->file= table_list->table->file; table_list->schema_table_state= executed_place; + if (use_fast_path) + { + /* + fill_table() completed using the fast path. If at least one + row matched, schema_table_store_record() already streamed + metadata + rows directly to the client. If zero rows + matched, metadata was never sent, so send it now (empty + result set still needs a column definition packet). + Either way, finish with EOF and signal exec_inner to skip + its own send_result_set_metadata + do_select. + */ + if (plan->fp_state == IS_table_read_plan::FP_ACTIVE) + { + List *meta= plan->projection_fields; + List field_list; + if (!meta) + { + TABLE *fb_table= table_list->table; + for (Field **f= fb_table->field; *f; f++) + { + Item_field *item= new (thd->mem_root) Item_field(thd, *f); + field_list.push_back(item); + } + meta= &field_list; + } + if (thd->protocol->send_result_set_metadata( + meta, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + { + result= 1; + break; + } + plan->fp_state= IS_table_read_plan::FP_STREAMING; + } + my_eof(thd); + result= 1; + } } } thd->pop_internal_handler(); @@ -9969,7 +10251,7 @@ bool get_schema_tables_result(JOIN *join, Sql_condition::WARN_LEVEL_ERROR, thd->get_stmt_da()->message()); } - else if (result) + else if (result && !thd->get_stmt_da()->is_eof()) my_error(ER_UNKNOWN_ERROR, MYF(0)); THD_STAGE_INFO(thd, org_stage); DBUG_RETURN(result); @@ -10008,7 +10290,7 @@ int store_key_cache_table_record(THD *thd, TABLE *table, table->field[1]->set_null(); else { - table->field[1]->set_notnull(); + table->field[1]->set_notnull(); table->field[1]->store((long) partitions, TRUE); } @@ -10042,7 +10324,7 @@ int run_fill_key_cache_tables(const char *name, KEY_CACHE *key_cache, void *p) TABLE *table= (TABLE *)p; THD *thd= table->in_use; - uint partitions= key_cache->partitions; + uint partitions= key_cache->partitions; size_t namelen= strlen(name); DBUG_ASSERT(partitions <= MAX_KEY_CACHE_PARTITIONS); @@ -11307,7 +11589,7 @@ static bool show_create_trigger_impl(THD *thd, Trigger *trigger) mem_root); static const Datetime zero_datetime(Datetime::zero()); - Item_datetime_literal *tmp= (new (mem_root) + Item_datetime_literal *tmp= (new (mem_root) Item_datetime_literal(thd, &zero_datetime, 2)); tmp->set_name(thd, Lex_cstring(STRING_WITH_LEN("Created"))); fields.push_back(tmp, mem_root); diff --git a/sql/sql_show.h b/sql/sql_show.h index 892aadfa6202f..825a31e930a59 100644 --- a/sql/sql_show.h +++ b/sql/sql_show.h @@ -234,7 +234,11 @@ typedef struct st_lookup_field_values class IS_table_read_plan : public Sql_alloc { public: - IS_table_read_plan() : no_rows(false), trivial_show_command(FALSE) {} + IS_table_read_plan() : no_rows(false), trivial_show_command(FALSE), + is_optimized_query(false), is_single_row(false), + abort_scan(false), fp_state(FP_INACTIVE), + projection_fields(NULL), partial_cond(NULL), + full_cond(NULL) {} bool no_rows; /* @@ -245,9 +249,20 @@ class IS_table_read_plan : public Sql_alloc data, we set trivial_show_command=true. */ bool trivial_show_command; + /* Flags for I_S fast-path optimization */ + bool is_optimized_query; /* Can this query bypass temp table? */ + bool is_single_row; /* Does it only need 1 row? */ + bool abort_scan; /* Signal to stop I_S scan early */ + enum fp_state_t { + FP_INACTIVE=0, /* fast path not engaged */ + FP_ACTIVE, /* fast path on, metadata pending */ + FP_STREAMING /* fast path on, metadata sent */ + } fp_state; + List *projection_fields; /* join->fields for fast-path send */ LOOKUP_FIELD_VALUES lookup_field_vals; Item *partial_cond; + Item *full_cond; /* complete original WHERE/HAVING, for fast-path row filtering */ bool has_db_lookup_value() { From 049973a429cb6f4c5d402cd927f477a074b0f8ca Mon Sep 17 00:00:00 2001 From: Anway Durge Date: Fri, 10 Jul 2026 16:18:22 +0530 Subject: [PATCH 2/5] MDEV-31342: revert incidental whitespace, add EXPLAIN coverage and is_fast_path visibility --- mysql-test/main/mdev_31342.result | 520 +++++++++++++++++++++++++++++- mysql-test/main/mdev_31342.test | 80 ++++- sql/sql_explain.cc | 4 + sql/sql_explain.h | 1 + sql/sql_select.cc | 2 + sql/sql_show.cc | 92 +++--- 6 files changed, 646 insertions(+), 53 deletions(-) diff --git a/mysql-test/main/mdev_31342.result b/mysql-test/main/mdev_31342.result index 063a811924386..7f70ddc7913a4 100644 --- a/mysql-test/main/mdev_31342.result +++ b/mysql-test/main/mdev_31342.result @@ -2,6 +2,30 @@ # # Test 1: Simple SELECT - should use fast path # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "skip_open_table": true, + "scanned_databases": 0, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; TABLE_NAME @@ -9,6 +33,30 @@ user # # Test 2: WHERE filter # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'information_schema' AND TABLE_NAME = 'TABLES'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'information_schema' and information_schema.`TABLES`.`TABLE_NAME` = 'TABLES'", + "skip_open_table": true, + "scanned_databases": 0, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'information_schema' AND TABLE_NAME = 'TABLES'; TABLE_NAME TABLE_SCHEMA @@ -16,6 +64,42 @@ TABLES information_schema # # Test 3: Scalar subquery - should trigger single row early exit # +EXPLAIN FORMAT=JSON SELECT ( +SELECT TABLE_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' +) AS result; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "table": { + "message": "No tables used" + }, + "subqueries": [ + { + "query_block": { + "select_id": 2, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "skip_open_table": true, + "scanned_databases": 0 + } + } + ] + } + } + ] + } +} SELECT ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES @@ -26,6 +110,51 @@ user # # Test 4: LIMIT 1 - should trigger early exit # +EXPLAIN FORMAT=JSON SELECT COUNT(*) > 0 FROM ( +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' + LIMIT 1 +) t; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.010089369, + "nested_loop": [ + { + "table": { + "table_name": "", + "access_type": "ALL", + "loops": 1, + "rows": 2, + "cost": 0.010089369, + "filtered": 100, + "materialized": { + "query_block": { + "select_id": 2, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1, + "is_fast_path": true + } + } + ] + } + } + } + } + ] + } +} SELECT COUNT(*) > 0 FROM ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' @@ -36,6 +165,36 @@ COUNT(*) > 0 # # Test 5: Fallback path - ORDER BY should still work correctly # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME +LIMIT 3; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME @@ -47,6 +206,30 @@ db # # Test 6: Fallback path - GROUP BY should still work # +EXPLAIN FORMAT=JSON SELECT TABLE_SCHEMA, COUNT(*) FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +GROUP BY TABLE_SCHEMA; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} SELECT TABLE_SCHEMA, COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' GROUP BY TABLE_SCHEMA; @@ -55,6 +238,29 @@ mysql 31 # # Test 7: Fallback path - DISTINCT should still work # +EXPLAIN FORMAT=JSON SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql'; TABLE_SCHEMA @@ -62,6 +268,31 @@ mysql # # Test 8: HAVING clause - should use fallback path # +EXPLAIN FORMAT=JSON SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +HAVING TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "having_condition": "information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' HAVING TABLE_NAME = 'user'; @@ -70,24 +301,231 @@ mysql user # # Test 9: WHERE TABLE_SCHEMA and TABLE_CATALOG mismatch - no rows # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.TABLE_CATALOG = 'nope'", + "skip_open_table": true, + "scanned_databases": 1, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; TABLE_NAME # +# Test 10: LIMIT n > 1 without ORDER BY - not the single-row early-exit +# case, so is_simple_is_query() rejects it -> fallback path +# +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; +TABLE_NAME +user +transaction_registry +time_zone_transition_type +# +# Test 11: LIMIT 1 OFFSET 1 - offset_limit set -> fallback path +# (regression coverage: fast path must not skip the offset row) +# +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +TABLE_NAME +transaction_registry +# +# Test 12: SQL_CALC_FOUND_ROWS - OPTION_FOUND_ROWS set -> fallback path +# (regression coverage: fast path must not short-circuit the full scan +# needed for an accurate FOUND_ROWS() count) +# +EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + ] + } +} +SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +TABLE_NAME +user +SELECT FOUND_ROWS(); +FOUND_ROWS() +31 +# +# Test 13: SELECT * - non-field item shape rejected -> fallback path +# +EXPLAIN FORMAT=JSON SELECT * FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "open_full_table": true, + "scanned_databases": 0 + } + } + ] + } +} +SELECT * FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +TABLE_CATALOG TABLE_SCHEMA TABLE_NAME TABLE_TYPE ENGINE VERSION ROW_FORMAT TABLE_ROWS AVG_ROW_LENGTH DATA_LENGTH MAX_DATA_LENGTH INDEX_LENGTH DATA_FREE AUTO_INCREMENT CREATE_TIME UPDATE_TIME CHECK_TIME TABLE_COLLATION CHECKSUM CREATE_OPTIONS TABLE_COMMENT MAX_INDEX_LENGTH TEMPORARY +def mysql user VIEW NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL VIEW NULL NULL +# # Verify: simple query temp table behavior # FLUSH STATUS; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "skip_open_table": true, + "scanned_databases": 0, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; TABLE_NAME user SHOW SESSION STATUS LIKE 'Created_tmp_tables'; Variable_name Value -Created_tmp_tables 1 +Created_tmp_tables 2 # # Verify: ORDER BY uses fallback # FLUSH STATUS; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME LIMIT 3; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "read_sorted_file": { + "filesort": { + "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 3; @@ -97,7 +535,7 @@ column_stats db SHOW SESSION STATUS LIKE 'Created_tmp_tables'; Variable_name Value -Created_tmp_tables 1 +Created_tmp_tables 2 # # EXPLAIN shows no filesort for fast-path query # @@ -118,7 +556,8 @@ EXPLAIN "cost": 0.01423506, "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", "skip_open_table": true, - "scanned_databases": 1 + "scanned_databases": 1, + "is_fast_path": true } } ] @@ -200,6 +639,29 @@ DROP PROCEDURE test_is_proc; CREATE VIEW test_is_view AS SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql'; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM test_is_view WHERE TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.`TABLE_NAME` = 'user' and information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 0, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME FROM test_is_view WHERE TABLE_NAME = 'user'; TABLE_NAME user @@ -207,6 +669,31 @@ DROP VIEW test_is_view; # # Test: WHERE with multiple conditions # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME LIKE 'user%' +LIMIT 1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` like 'user%'", + "skip_open_table": true, + "scanned_databases": 1, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME LIKE 'user%' LIMIT 1; @@ -233,6 +720,30 @@ DROP TABLE test_mdev31342; # Test: Query cache - ensure results are not served from cache # SET SESSION query_cache_type = 0; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.01423506, + "nested_loop": [ + { + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA,TABLE_NAME", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", + "skip_open_table": true, + "scanned_databases": 0, + "is_fast_path": true + } + } + ] + } +} SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; TABLE_NAME @@ -277,7 +788,8 @@ EXPLAIN "cost": 0.01423506, "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql' and information_schema.`TABLES`.`TABLE_NAME` = 'user'", "skip_open_table": true, - "scanned_databases": 0 + "scanned_databases": 0, + "is_fast_path": true } } ] diff --git a/mysql-test/main/mdev_31342.test b/mysql-test/main/mdev_31342.test index 6e0f145f6aec2..c7899e08876bf 100644 --- a/mysql-test/main/mdev_31342.test +++ b/mysql-test/main/mdev_31342.test @@ -3,18 +3,27 @@ --echo # --echo # Test 1: Simple SELECT - should use fast path --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; --echo # --echo # Test 2: WHERE filter --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'information_schema' AND TABLE_NAME = 'TABLES'; SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'information_schema' AND TABLE_NAME = 'TABLES'; --echo # --echo # Test 3: Scalar subquery - should trigger single row early exit --echo # +EXPLAIN FORMAT=JSON SELECT ( + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' +) AS result; SELECT ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES @@ -24,6 +33,11 @@ SELECT ( --echo # --echo # Test 4: LIMIT 1 - should trigger early exit --echo # +EXPLAIN FORMAT=JSON SELECT COUNT(*) > 0 FROM ( + SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'mysql' + LIMIT 1 +) t; SELECT COUNT(*) > 0 FROM ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' @@ -33,6 +47,10 @@ SELECT COUNT(*) > 0 FROM ( --echo # --echo # Test 5: Fallback path - ORDER BY should still work correctly --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME +LIMIT 3; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME @@ -41,6 +59,9 @@ LIMIT 3; --echo # --echo # Test 6: Fallback path - GROUP BY should still work --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_SCHEMA, COUNT(*) FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +GROUP BY TABLE_SCHEMA; SELECT TABLE_SCHEMA, COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' GROUP BY TABLE_SCHEMA; @@ -48,14 +69,17 @@ GROUP BY TABLE_SCHEMA; --echo # --echo # Test 7: Fallback path - DISTINCT should still work --echo # +EXPLAIN FORMAT=JSON SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql'; SELECT DISTINCT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql'; - - --echo # --echo # Test 8: HAVING clause - should use fallback path --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +HAVING TABLE_NAME = 'user'; SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' HAVING TABLE_NAME = 'user'; @@ -63,13 +87,55 @@ HAVING TABLE_NAME = 'user'; --echo # --echo # Test 9: WHERE TABLE_SCHEMA and TABLE_CATALOG mismatch - no rows --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; +--echo # +--echo # Test 10: LIMIT n > 1 without ORDER BY - not the single-row early-exit +--echo # case, so is_simple_is_query() rejects it -> fallback path +--echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; + +--echo # +--echo # Test 11: LIMIT 1 OFFSET 1 - offset_limit set -> fallback path +--echo # (regression coverage: fast path must not skip the offset row) +--echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; + +--echo # +--echo # Test 12: SQL_CALC_FOUND_ROWS - OPTION_FOUND_ROWS set -> fallback path +--echo # (regression coverage: fast path must not short-circuit the full scan +--echo # needed for an accurate FOUND_ROWS() count) +--echo # +EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +SELECT FOUND_ROWS(); + +--echo # +--echo # Test 13: SELECT * - non-field item shape rejected -> fallback path +--echo # +EXPLAIN FORMAT=JSON SELECT * FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; +SELECT * FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; + --echo # --echo # Verify: simple query temp table behavior --echo # FLUSH STATUS; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; SHOW SESSION STATUS LIKE 'Created_tmp_tables'; @@ -78,12 +144,14 @@ SHOW SESSION STATUS LIKE 'Created_tmp_tables'; --echo # Verify: ORDER BY uses fallback --echo # FLUSH STATUS; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' +ORDER BY TABLE_NAME LIMIT 3; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 3; SHOW SESSION STATUS LIKE 'Created_tmp_tables'; - --echo # --echo # EXPLAIN shows no filesort for fast-path query --echo # @@ -134,12 +202,16 @@ DROP PROCEDURE test_is_proc; CREATE VIEW test_is_view AS SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql'; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM test_is_view WHERE TABLE_NAME = 'user'; SELECT TABLE_NAME FROM test_is_view WHERE TABLE_NAME = 'user'; DROP VIEW test_is_view; --echo # --echo # Test: WHERE with multiple conditions --echo # +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME LIKE 'user%' +LIMIT 1; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME LIKE 'user%' LIMIT 1; @@ -167,6 +239,8 @@ DROP TABLE test_mdev31342; --echo # Test: Query cache - ensure results are not served from cache --echo # SET SESSION query_cache_type = 0; +EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES diff --git a/sql/sql_explain.cc b/sql/sql_explain.cc index 77aab40092f65..970d213ee7712 100644 --- a/sql/sql_explain.cc +++ b/sql/sql_explain.cc @@ -1818,6 +1818,9 @@ void Explain_table_access::tag_to_json(Json_writer *writer, case ET_SKIP_OPEN_TABLE: writer->add_member("skip_open_table").add_bool(true); break; + case ET_IS_FAST_PATH: + writer->add_member("is_fast_path").add_bool(true); + break; case ET_OPEN_FRM_ONLY: writer->add_member("open_frm_only").add_bool(true); break; @@ -2416,6 +2419,7 @@ const LEX_CSTRING extra_tag_text[]= { STRING_WITH_LEN("Scanned 0 databases") }, { STRING_WITH_LEN("Scanned 1 database") }, { STRING_WITH_LEN("Scanned all databases") }, + { STRING_WITH_LEN("I_S fast path") }, { STRING_WITH_LEN("Using index for group-by") }, // special handling { STRING_WITH_LEN("USING MRR: DONT PRINT ME") }, // special handling diff --git a/sql/sql_explain.h b/sql/sql_explain.h index fd442e3d8e8e0..a5d825e15efb0 100644 --- a/sql/sql_explain.h +++ b/sql/sql_explain.h @@ -584,6 +584,7 @@ enum explain_extra_tag ET_SCANNED_0_DATABASES, ET_SCANNED_1_DATABASE, ET_SCANNED_ALL_DATABASES, + ET_IS_FAST_PATH, ET_USING_INDEX_FOR_GROUP_BY, diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 8502fd93b40b7..42211485b7ac9 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -31460,6 +31460,8 @@ bool JOIN_TAB::save_explain_data(Explain_table_access *eta, eta->push_extra(ET_SCANNED_1_DATABASE); else eta->push_extra(ET_SCANNED_ALL_DATABASES); + if (table_list->is_table_read_plan->is_optimized_query) + eta->push_extra(ET_IS_FAST_PATH); } if (quick_type == QUICK_SELECT_I::QS_TYPE_GROUP_MIN_MAX) diff --git a/sql/sql_show.cc b/sql/sql_show.cc index 5fac9ab0251a6..b05d740910524 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -628,8 +628,8 @@ bool mysqld_show_privileges(THD *thd) /** Hash of LEX_STRINGs used to search for ignored db directories. */ static HASH ignore_db_dirs_hash; -/** - An array of LEX_STRING pointers to collect the options at +/** + An array of LEX_STRING pointers to collect the options at option parsing time. */ static DYNAMIC_ARRAY ignore_db_dirs_array; @@ -768,7 +768,7 @@ ignore_db_dirs_free() Initialize the ignore db directories hash and status variable from the options collected in the array. - Called when option processing is over and the server's in-memory + Called when option processing is over and the server's in-memory structures are fully initialized. @return state @@ -915,7 +915,7 @@ ignore_db_dirs_process_additions() DBUG_ASSERT(ptr - opt_ignore_db_dirs <= (ptrdiff_t) len); *ptr= 0; - /* + /* It's OK to empty the array here as the allocated elements are referenced through the hash now. */ @@ -937,7 +937,7 @@ static inline bool is_in_ignore_db_dirs_list(const char *directory) { return ignore_db_dirs_hash.records && - NULL != my_hash_search(&ignore_db_dirs_hash, (const uchar *) directory, + NULL != my_hash_search(&ignore_db_dirs_hash, (const uchar *) directory, strlen(directory)); } @@ -1082,7 +1082,7 @@ find_files(THD *thd, Dynamic_array *files, can't untangle its access checking from that of the view itself. */ class Show_create_error_handler : public Internal_error_handler { - + TABLE_LIST *m_top_view; bool m_handling; Security_context *m_sctx; @@ -1094,17 +1094,17 @@ class Show_create_error_handler : public Internal_error_handler { /** Creates a new Show_create_error_handler for the particular security - context and view. + context and view. @thd Thread context, used for security context information if needed. @top_view The view. We do not verify at this point that top_view is in fact a view since, alas, these things do not stay constant. */ - explicit Show_create_error_handler(THD *thd, TABLE_LIST *top_view) : + explicit Show_create_error_handler(THD *thd, TABLE_LIST *top_view) : m_top_view(top_view), m_handling(FALSE), - m_view_access_denied_message_ptr(NULL) + m_view_access_denied_message_ptr(NULL) { - + m_sctx= MY_TEST(m_top_view->security_ctx) ? m_top_view->security_ctx : thd->security_ctx; } @@ -1118,7 +1118,7 @@ class Show_create_error_handler : public Internal_error_handler { failed is not available at this point. The only way for us to check is by reconstructing the actual error message and see if it's the same. */ - char* get_view_access_denied_message(THD *thd) + char* get_view_access_denied_message(THD *thd) { if (!m_view_access_denied_message_ptr) { @@ -1170,7 +1170,7 @@ class Show_create_error_handler : public Internal_error_handler { case ER_NO_SUCH_TABLE_IN_ENGINE: /* Established behavior: warn if underlying tables, columns, or functions are missing. */ - push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, + push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, ER_VIEW_INVALID, ER_THD(thd, ER_VIEW_INVALID), m_top_view->get_db_name().str, @@ -3181,7 +3181,7 @@ void Show_explain_request::call_in_target_thread() Query_arena backup_arena; bool printed_anything= FALSE; - /* + /* Change the arena because JOIN::print_explain and co. are going to allocate items. Let them allocate them on our arena. */ @@ -3229,7 +3229,7 @@ int select_result_explain_buffer::send_data(List &items) set_current_thd(thd); fill_record(thd, dst_table, dst_table->field, items, true, false, false); res= dst_table->file->ha_write_tmp_row(dst_table->record[0]); - set_current_thd(cur_thd); + set_current_thd(cur_thd); DBUG_RETURN(MY_TEST(res)); } @@ -3262,7 +3262,7 @@ int select_result_text_buffer::append_row(List &items, bool send_names) while ((item= it++)) { DBUG_ASSERT(column < n_columns); - const char *data_ptr; + const char *data_ptr; char *ptr; size_t data_len; @@ -3344,11 +3344,11 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, /* If calling_user==NULL, calling thread has SUPER or PROCESS privilege, and so can do SHOW EXPLAIN/SHOW ANALYZE on any user. - + if calling_user!=NULL, he's only allowed to view SHOW EXPLAIN/SHOW ANALYZE on his own threads. */ - if (calling_user && (!tmp_sctx->user || strcmp(calling_user, + if (calling_user && (!tmp_sctx->user || strcmp(calling_user, tmp_sctx->user))) { my_error(ER_SPECIFIC_ACCESS_DENIED_ERROR, MYF(0), "PROCESS"); @@ -3364,8 +3364,8 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, } bool bres; - /* - Ok we've found the thread of interest and it won't go away because + /* + Ok we've found the thread of interest and it won't go away because we're holding its LOCK_thd_kill. Post it a SHOW EXPLAIN/SHOW ANALYZE request. */ @@ -3374,7 +3374,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, Show_explain_request explain_req; explain_req.is_json_format= json_format; select_result_explain_buffer *explain_buf; - + if (!(explain_buf= new (thd->mem_root) select_result_explain_buffer(thd, table->table))) DBUG_RETURN(1); @@ -3384,7 +3384,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, explain_req.target_thd= tmp; explain_req.request_thd= thd; explain_req.failed_to_produce= FALSE; - + /* Do not use default memroot as this is only to be used by the target thread (It's marked as thread MY_THREAD_SPECIFIC). @@ -3422,7 +3422,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, char *warning_text; if (!my_charset_same(fromcs, tocs)) { - uint conv_length= 1 + tocs->mbmaxlen * explain_req.query_str.length() / + uint conv_length= 1 + tocs->mbmaxlen * explain_req.query_str.length() / fromcs->mbminlen; uint dummy_errors; char *to, *p; @@ -3430,7 +3430,7 @@ int fill_show_explain_or_analyze(THD *thd, TABLE_LIST *table, COND *cond, DBUG_RETURN(1); p= to; p+= copy_and_convert(to, conv_length, tocs, - explain_req.query_str.c_ptr(), + explain_req.query_str.c_ptr(), explain_req.query_str.length(), fromcs, &dummy_errors); *p= 0; @@ -4091,7 +4091,7 @@ static bool show_status_array(THD *thd, const char *wild, var->type == SHOW_SIMPLE_FUNC; var= &tmp) ((mysql_show_var_func)(var->value))(thd, &tmp, (void *) buff, status_var, scope); - + SHOW_TYPE show_type=var->type; if (show_type == SHOW_ARRAY) { @@ -5602,10 +5602,10 @@ static privilege_t get_schema_privileges_for_show(THD *thd, TABLE_LIST *tables, bool on_any_column) { #ifndef NO_EMBEDDED_ACCESS_CHECKS - /* + /* We know that the table or at least some of the columns have necessary privileges, but the caller didn't pass down the GRANT_INFO - object, so we have to rediscover everything again :( + object, so we have to rediscover everything again :( */ if (thd->col_access & need) return thd->col_access & need; @@ -5669,7 +5669,7 @@ static bool strcmpcs(const LEX_CSTRING &str, const LEX_CSTRING &pat) get_all_tables(). @note This function assumes optimize_for_get_all_tables() has been - run for the table and produced a "read plan" in + run for the table and produced a "read plan" in tables->is_table_read_plan. @param[in] thd thread handler @@ -5722,7 +5722,7 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) thd->reset_n_backup_open_tables_state(&open_tables_state_backup); schema_table_idx= get_schema_table_idx(schema_table); - /* + /* this branch processes SHOW FIELDS, SHOW INDEXES commands. see sql_parse.cc, prepare_schema_table() function where this values are initialized @@ -6438,7 +6438,7 @@ void process_i_s_table_temporary_tables(THD *thd, TABLE * table, TABLE *tmp_tbl) @param[in] cs I_S table charset @param[in] offset offset from beginning of table to DATE_TYPE column in I_S table - + @return void */ @@ -6676,7 +6676,7 @@ static int store_schema_period_record(THD *thd, TABLE_LIST *tl, for (auto *field: {period.start_field(s), period.end_field(s)}) { /* Reveal the value only if the user has any privilege on this column */ - bool col_granted= get_column_grant(thd, &tl->grant, + bool col_granted= get_column_grant(thd, &tl->grant, db_name->str, table_name->str, field->field_name) & COL_DML_ACLS; if (col_granted) @@ -7318,7 +7318,7 @@ int store_schema_params(THD *thd, TABLE *table, TABLE *proc_table, default: tmp_buff= ""; break; - } + } restore_record(table, s->default_values); table->field[0]->store(STRING_WITH_LEN("def"), cs); @@ -7447,7 +7447,7 @@ int store_schema_proc(THD *thd, TABLE *table, TABLE *proc_table, table->field[18]->store(STRING_WITH_LEN("SQL"), cs); copy_field_as_string(table->field[19], proc_table->field[MYSQL_PROC_FIELD_DETERMINISTIC]); - table->field[20]->store(sp_data_access_name[enum_idx].str, + table->field[20]->store(sp_data_access_name[enum_idx].str, sp_data_access_name[enum_idx].length , cs); copy_field_as_string(table->field[22], proc_table->field[MYSQL_PROC_FIELD_SECURITY_TYPE]); @@ -7727,7 +7727,7 @@ static int get_schema_stat_record(THD *thd, TABLE_LIST *tables, TABLE *table, DBUG_ASSERT(MY_TEST(key_info->flags & HA_USES_COMMENT) == (key_info->comment.length > 0)); if (key_info->flags & HA_USES_COMMENT) - table->field[15]->store(key_info->comment.str, + table->field[15]->store(key_info->comment.str, key_info->comment.length, cs); // IGNORED column @@ -7959,7 +7959,7 @@ static int get_schema_constraints_record(THD *thd, TABLE_LIST *tables, if (!tables->view) { /* need any non-SELECT privilege on the table or any of its columns */ - if (!get_schema_privileges_for_show(thd, tables, TABLE_ACLS & ~SELECT_ACL, + if (!get_schema_privileges_for_show(thd, tables, TABLE_ACLS & ~SELECT_ACL, true)) DBUG_RETURN(0); @@ -9178,7 +9178,7 @@ get_referential_constraints_record(THD *thd, TABLE_LIST *tables, table->field[2]->store(f_key_info->foreign_id->str, f_key_info->foreign_id->length, cs); table->field[3]->store(STRING_WITH_LEN("def"), cs); - table->field[4]->store(f_key_info->referenced_db->str, + table->field[4]->store(f_key_info->referenced_db->str, f_key_info->referenced_db->length, cs); bool show_ref_table= true; #ifndef NO_EMBEDDED_ACCESS_CHECKS @@ -9803,7 +9803,7 @@ int make_schema_select(THD *thd, SELECT_LEX *sel, Optimize reading from an I_S table. @detail - This function prepares a plan for populating an I_S table with + This function prepares a plan for populating an I_S table with get_all_tables(). The plan is in IS_table_read_plan structure, it is saved in @@ -9812,7 +9812,7 @@ int make_schema_select(THD *thd, SELECT_LEX *sel, @return false - Ok true - Out Of Memory - + */ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) @@ -9829,11 +9829,11 @@ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond tables->is_table_read_plan= plan; schema_table_idx= get_schema_table_idx(schema_table); - tables->table_open_method= get_table_open_method(tables, schema_table, + tables->table_open_method= get_table_open_method(tables, schema_table, schema_table_idx); DBUG_PRINT("open_method", ("%d", tables->table_open_method)); - /* + /* this branch processes SHOW FIELDS, SHOW INDEXES commands. see sql_parse.cc, prepare_schema_table() function where this values are initialized @@ -9856,7 +9856,7 @@ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond plan->lookup_field_vals.db_value.str, plan->lookup_field_vals.table_value.str)); - if (!plan->lookup_field_vals.wild_db_value && + if (!plan->lookup_field_vals.wild_db_value && !plan->lookup_field_vals.wild_table_value) { /* @@ -9892,7 +9892,7 @@ static bool optimize_for_get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond plan->partial_cond= 0; else plan->partial_cond= make_cond_for_info_schema(thd, cond, tables); - + end: DBUG_RETURN(0); } @@ -9979,7 +9979,7 @@ bool optimize_schema_tables_reads(JOIN *join) JOIN_TAB *tab; for (tab= first_linear_tab(join, WITHOUT_BUSH_ROOTS, WITH_CONST_TABLES); - tab; + tab; tab= next_linear_tab(join, tab, WITH_BUSH_ROOTS)) { /* @@ -10054,7 +10054,7 @@ bool get_schema_tables_result(JOIN *join, JOIN_TAB *tab; for (tab= first_linear_tab(join, WITHOUT_BUSH_ROOTS, WITH_CONST_TABLES); - tab; + tab; tab= next_linear_tab(join, tab, WITH_BUSH_ROOTS)) { /* @@ -10290,7 +10290,7 @@ int store_key_cache_table_record(THD *thd, TABLE *table, table->field[1]->set_null(); else { - table->field[1]->set_notnull(); + table->field[1]->set_notnull(); table->field[1]->store((long) partitions, TRUE); } @@ -10324,7 +10324,7 @@ int run_fill_key_cache_tables(const char *name, KEY_CACHE *key_cache, void *p) TABLE *table= (TABLE *)p; THD *thd= table->in_use; - uint partitions= key_cache->partitions; + uint partitions= key_cache->partitions; size_t namelen= strlen(name); DBUG_ASSERT(partitions <= MAX_KEY_CACHE_PARTITIONS); @@ -11589,7 +11589,7 @@ static bool show_create_trigger_impl(THD *thd, Trigger *trigger) mem_root); static const Datetime zero_datetime(Datetime::zero()); - Item_datetime_literal *tmp= (new (mem_root) + Item_datetime_literal *tmp= (new (mem_root) Item_datetime_literal(thd, &zero_datetime, 2)); tmp->set_name(thd, Lex_cstring(STRING_WITH_LEN("Created"))); fields.push_back(tmp, mem_root); From a3938462eaae80d462831de2cc5ed660443d9176 Mon Sep 17 00:00:00 2001 From: Anway Durge Date: Sun, 12 Jul 2026 15:12:11 +0530 Subject: [PATCH 3/5] MDEV-31342: Fix CI test failures for I_S fast path EXPLAIN and result ordering --- mysql-test/main/information_schema.result | 4 +- mysql-test/main/mdev_31342.result | 72 +++++++++++++---------- mysql-test/main/mdev_31342.test | 16 ++--- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/mysql-test/main/information_schema.result b/mysql-test/main/information_schema.result index 5febd5baea044..af03a27575e33 100644 --- a/mysql-test/main/information_schema.result +++ b/mysql-test/main/information_schema.result @@ -1465,7 +1465,7 @@ id select_type table type possible_keys key key_len ref rows Extra explain select * from (select table_name from information_schema.tables) as a; id select_type table type possible_keys key key_len ref rows Extra 1 PRIMARY ALL NULL NULL NULL NULL 100 -2 DERIVED tables ALL NULL NULL NULL NULL NULL Skip_open_table; Scanned all databases +2 DERIVED tables ALL NULL NULL NULL NULL NULL Skip_open_table; Scanned all databases; I_S fast path set optimizer_switch=@tmp_optimizer_switch; drop view v1; create table t1 (f1 int(11)); @@ -1739,7 +1739,7 @@ explain select b.table_name from information_schema.tables a, information_schema.columns b where a.table_name='t1' and a.table_schema='test' and b.table_name=a.table_name; id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE a ALL NULL TABLE_SCHEMA,TABLE_NAME NULL NULL NULL Using where; Skip_open_table; Scanned 0 databases +1 SIMPLE a ALL NULL TABLE_SCHEMA,TABLE_NAME NULL NULL NULL Using where; Skip_open_table; Scanned 0 databases; I_S fast path 1 SIMPLE b ALL NULL NULL NULL NULL NULL Using where; Open_frm_only; Scanned all databases; Using join buffer (flat, BNL join) SELECT * FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'mysqltest'; diff --git a/mysql-test/main/mdev_31342.result b/mysql-test/main/mdev_31342.result index 7f70ddc7913a4..dbc68e6539479 100644 --- a/mysql-test/main/mdev_31342.result +++ b/mysql-test/main/mdev_31342.result @@ -355,18 +355,18 @@ EXPLAIN ] } } +SELECT COUNT(*) FROM ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; -TABLE_NAME -user -transaction_registry -time_zone_transition_type -# +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3 +) t; +COUNT(*) +3 # Test 11: LIMIT 1 OFFSET 1 - offset_limit set -> fallback path # (regression coverage: fast path must not skip the offset row) +# ORDER BY added for deterministic result across platforms # EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; EXPLAIN { "query_block": { @@ -374,32 +374,37 @@ EXPLAIN "cost": 0.01423506, "nested_loop": [ { - "table": { - "table_name": "TABLES", - "access_type": "ALL", - "key": "TABLE_SCHEMA", - "loops": 1, - "cost": 0.01423506, - "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", - "skip_open_table": true, - "scanned_databases": 1 + "read_sorted_file": { + "filesort": { + "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } } } ] } } SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; TABLE_NAME -transaction_registry -# +column_stats # Test 12: SQL_CALC_FOUND_ROWS - OPTION_FOUND_ROWS set -> fallback path # (regression coverage: fast path must not short-circuit the full scan # needed for an accurate FOUND_ROWS() count) +# ORDER BY added for deterministic result across platforms # EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; EXPLAIN { "query_block": { @@ -407,24 +412,29 @@ EXPLAIN "cost": 0.01423506, "nested_loop": [ { - "table": { - "table_name": "TABLES", - "access_type": "ALL", - "key": "TABLE_SCHEMA", - "loops": 1, - "cost": 0.01423506, - "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", - "skip_open_table": true, - "scanned_databases": 1 + "read_sorted_file": { + "filesort": { + "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 + } + } } } ] } } SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; TABLE_NAME -user +columns_priv SELECT FOUND_ROWS(); FOUND_ROWS() 31 diff --git a/mysql-test/main/mdev_31342.test b/mysql-test/main/mdev_31342.test index c7899e08876bf..3434ee152030f 100644 --- a/mysql-test/main/mdev_31342.test +++ b/mysql-test/main/mdev_31342.test @@ -98,28 +98,30 @@ WHERE TABLE_SCHEMA = 'mysql' AND TABLE_CATALOG = 'nope'; --echo # EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; +SELECT COUNT(*) FROM ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 3; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3 +) t; ---echo # --echo # Test 11: LIMIT 1 OFFSET 1 - offset_limit set -> fallback path --echo # (regression coverage: fast path must not skip the offset row) +--echo # ORDER BY added for deterministic result across platforms --echo # EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; ---echo # --echo # Test 12: SQL_CALC_FOUND_ROWS - OPTION_FOUND_ROWS set -> fallback path --echo # (regression coverage: fast path must not short-circuit the full scan --echo # needed for an accurate FOUND_ROWS() count) +--echo # ORDER BY added for deterministic result across platforms --echo # EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; SELECT FOUND_ROWS(); --echo # From f40735b077dfa6772a0471179831f635b003bb34 Mon Sep 17 00:00:00 2001 From: Anway Durge Date: Sun, 12 Jul 2026 15:59:10 +0530 Subject: [PATCH 4/5] MDEV-31342: Fix Tests 11/12 to test OFFSET and SQL_CALC_FOUND_ROWS correctly --- mysql-test/main/mdev_31342.result | 64 +++++++++++++------------------ mysql-test/main/mdev_31342.test | 14 ++++--- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/mysql-test/main/mdev_31342.result b/mysql-test/main/mdev_31342.result index dbc68e6539479..120e0eb1e7ac3 100644 --- a/mysql-test/main/mdev_31342.result +++ b/mysql-test/main/mdev_31342.result @@ -363,10 +363,9 @@ COUNT(*) 3 # Test 11: LIMIT 1 OFFSET 1 - offset_limit set -> fallback path # (regression coverage: fast path must not skip the offset row) -# ORDER BY added for deterministic result across platforms # EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; EXPLAIN { "query_block": { @@ -374,37 +373,33 @@ EXPLAIN "cost": 0.01423506, "nested_loop": [ { - "read_sorted_file": { - "filesort": { - "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", - "table": { - "table_name": "TABLES", - "access_type": "ALL", - "key": "TABLE_SCHEMA", - "loops": 1, - "cost": 0.01423506, - "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", - "skip_open_table": true, - "scanned_databases": 1 - } - } + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 } } ] } } +SELECT COUNT(*) FROM ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; -TABLE_NAME -column_stats +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1 +) t; +COUNT(*) +1 # Test 12: SQL_CALC_FOUND_ROWS - OPTION_FOUND_ROWS set -> fallback path # (regression coverage: fast path must not short-circuit the full scan # needed for an accurate FOUND_ROWS() count) -# ORDER BY added for deterministic result across platforms # EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; EXPLAIN { "query_block": { @@ -412,29 +407,22 @@ EXPLAIN "cost": 0.01423506, "nested_loop": [ { - "read_sorted_file": { - "filesort": { - "sort_key": "information_schema.`TABLES`.`TABLE_NAME`", - "table": { - "table_name": "TABLES", - "access_type": "ALL", - "key": "TABLE_SCHEMA", - "loops": 1, - "cost": 0.01423506, - "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", - "skip_open_table": true, - "scanned_databases": 1 - } - } + "table": { + "table_name": "TABLES", + "access_type": "ALL", + "key": "TABLE_SCHEMA", + "loops": 1, + "cost": 0.01423506, + "attached_condition": "information_schema.`TABLES`.TABLE_SCHEMA = 'mysql'", + "skip_open_table": true, + "scanned_databases": 1 } } ] } } SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; -TABLE_NAME -columns_priv +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; SELECT FOUND_ROWS(); FOUND_ROWS() 31 diff --git a/mysql-test/main/mdev_31342.test b/mysql-test/main/mdev_31342.test index 3434ee152030f..7c6269cff0e70 100644 --- a/mysql-test/main/mdev_31342.test +++ b/mysql-test/main/mdev_31342.test @@ -105,23 +105,25 @@ WHERE TABLE_SCHEMA = 'mysql' LIMIT 3 --echo # Test 11: LIMIT 1 OFFSET 1 - offset_limit set -> fallback path --echo # (regression coverage: fast path must not skip the offset row) ---echo # ORDER BY added for deterministic result across platforms --echo # EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1; +SELECT COUNT(*) FROM ( SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1 OFFSET 1; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1 +) t; --echo # Test 12: SQL_CALC_FOUND_ROWS - OPTION_FOUND_ROWS set -> fallback path --echo # (regression coverage: fast path must not short-circuit the full scan --echo # needed for an accurate FOUND_ROWS() count) ---echo # ORDER BY added for deterministic result across platforms --echo # EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +--disable_result_log SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES -WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 1; +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +--enable_result_log SELECT FOUND_ROWS(); --echo # From c68991b17e22bcec65033c541783d88c4e543a21 Mon Sep 17 00:00:00 2001 From: Anway Durge Date: Sun, 12 Jul 2026 16:52:02 +0530 Subject: [PATCH 5/5] MDEV-31342: Fix mdev_31342 for Windows casing and PS protocol builders --- mysql-test/main/mdev_31342.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mysql-test/main/mdev_31342.test b/mysql-test/main/mdev_31342.test index 7c6269cff0e70..51a190d606352 100644 --- a/mysql-test/main/mdev_31342.test +++ b/mysql-test/main/mdev_31342.test @@ -1,4 +1,6 @@ --echo # MDEV-31342: Test I_S fast path optimization +--replace_regex /information_schema\.`tables`/information_schema.`TABLES`/ +--replace_regex /"table_name": "tables"/"table_name": "TABLES"/ --echo # --echo # Test 1: Simple SELECT - should use fast path @@ -117,6 +119,7 @@ WHERE TABLE_SCHEMA = 'mysql' LIMIT 1 OFFSET 1 --echo # (regression coverage: fast path must not short-circuit the full scan --echo # needed for an accurate FOUND_ROWS() count) --echo # +--disable_ps_protocol EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; @@ -125,6 +128,7 @@ SELECT SQL_CALC_FOUND_ROWS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; --enable_result_log SELECT FOUND_ROWS(); +--enable_ps_protocol --echo # --echo # Test 13: SELECT * - non-field item shape rejected -> fallback path @@ -137,6 +141,7 @@ WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; --echo # --echo # Verify: simple query temp table behavior --echo # +--disable_ps_protocol FLUSH STATUS; EXPLAIN FORMAT=JSON SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user'; @@ -155,6 +160,7 @@ SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mysql' ORDER BY TABLE_NAME LIMIT 3; SHOW SESSION STATUS LIKE 'Created_tmp_tables'; +--enable_ps_protocol --echo # --echo # EXPLAIN shows no filesort for fast-path query