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 new file mode 100644 index 0000000000000..120e0eb1e7ac3 --- /dev/null +++ b/mysql-test/main/mdev_31342.result @@ -0,0 +1,795 @@ +# MDEV-31342: Test I_S fast path optimization +# +# 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 +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 +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 +WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' +) AS result; +result +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' + LIMIT 1 +) t; +COUNT(*) > 0 +1 +# +# 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 +LIMIT 3; +TABLE_NAME +columns_priv +column_stats +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; +TABLE_SCHEMA COUNT(*) +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 +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'; +TABLE_SCHEMA TABLE_NAME +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 COUNT(*) FROM ( +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +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) +# +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 COUNT(*) FROM ( +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +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) +# +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; +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 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; +TABLE_NAME +columns_priv +column_stats +db +SHOW SESSION STATUS LIKE 'Created_tmp_tables'; +Variable_name Value +Created_tmp_tables 2 +# +# 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, + "is_fast_path": true + } + } + ] + } +} +# +# 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'; +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 +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; +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; +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 +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, + "is_fast_path": true + } + } + ] + } +} diff --git a/mysql-test/main/mdev_31342.test b/mysql-test/main/mdev_31342.test new file mode 100644 index 0000000000000..51a190d606352 --- /dev/null +++ b/mysql-test/main/mdev_31342.test @@ -0,0 +1,276 @@ +--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 +--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 + WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' +) AS result; + +--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' + LIMIT 1 +) t; + +--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 +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; + +--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'; + +--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 COUNT(*) FROM ( +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 3 +) t; + +--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 COUNT(*) FROM ( +SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES +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 # +--disable_ps_protocol +EXPLAIN FORMAT=JSON SELECT SQL_CALC_FOUND_ROWS TABLE_NAME +FROM INFORMATION_SCHEMA.TABLES +WHERE TABLE_SCHEMA = 'mysql' LIMIT 1; +--disable_result_log +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 +--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 # +--disable_ps_protocol +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'; + +--echo # +--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'; +--enable_ps_protocol + +--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'; +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; + +--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; +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 +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_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 882db8f3c1353..b05d740910524 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -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; @@ -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; } @@ -9690,6 +9873,21 @@ 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 @@ -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); 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() {