From 26af7ea811b896ec25901f0c1dc780df631f60f2 Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Thu, 8 Oct 2026 15:07:54 -0400 Subject: [PATCH] MDEV-41146: Merge pseudo_bits_cond into outer_ref_cond A join of only const tables stored the WHERE conjuncts that refer to a column of an outer query or call rand() in pseudo_bits_cond. The executor read pseudo_bits_cond only when the query needed no temporary table, and read outer_ref_cond otherwise. A window function or DISTINCT with ROLLUP needs a temporary table, so those conjuncts were never checked, and the query returned rows that the WHERE clause rejects. pseudo_bits_cond came from the fix for LP bug 908269 in 2012. For a join of only const tables, that fix also collected conjuncts that call rand(), and it stored them in a new field without stating a reason. The code that filled the two fields and the code that read them chose between them by the same test. The tests have differed since MDEV-8646 in 2016, which added the temporary table condition to the executor's test. Later changes only display the field, in EXPLAIN FORMAT=JSON (MDEV-19714) and in the optimizer trace (MDEV-21095). In MDEV-19714, its author wrote that it was not clear why pseudo_bits_cond was needed in addition to outer_ref_cond. pseudo_bits_cond is removed, and outer_ref_cond holds these conjuncts for every join. The executor checks it on both paths, so where the conjuncts are stored no longer depends on the executor's test. Conjuncts that call rand() are still collected only for a join of only const tables. EXPLAIN FORMAT=JSON shows these conjuncts as outer_ref_condition instead of pseudo_bits_condition. Co-Authored-By: Claude Opus 5.5 (1M context) --- mysql-test/main/group_by.result | 4 +- mysql-test/main/olap.result | 15 ++++ mysql-test/main/olap.test | 16 +++++ mysql-test/main/subselect_no_semijoin.result | 2 +- mysql-test/main/win.result | 24 +++++++ mysql-test/main/win.test | 25 +++++++ .../encryption/r/tempfiles_encrypted.result | 24 +++++++ sql/sql_explain.cc | 5 -- sql/sql_explain.h | 1 - sql/sql_select.cc | 69 +++++++++---------- sql/sql_select.h | 11 ++- 11 files changed, 148 insertions(+), 48 deletions(-) diff --git a/mysql-test/main/group_by.result b/mysql-test/main/group_by.result index 26e1ba14e95..e221744fc47 100644 --- a/mysql-test/main/group_by.result +++ b/mysql-test/main/group_by.result @@ -3038,7 +3038,7 @@ EXPLAIN { "query_block": { "select_id": 1, - "pseudo_bits_condition": "if(uuid_short(),NULL,1)", + "outer_ref_condition": "if(uuid_short(),NULL,1)", "nested_loop": [ { "table": { @@ -3064,7 +3064,7 @@ EXPLAIN { "query_block": { "select_id": 1, - "pseudo_bits_condition": "10 = rand()", + "outer_ref_condition": "10 = rand()", "nested_loop": [ { "table": { diff --git a/mysql-test/main/olap.result b/mysql-test/main/olap.result index 22186ee085c..5d9060ec733 100644 --- a/mysql-test/main/olap.result +++ b/mysql-test/main/olap.result @@ -905,3 +905,18 @@ NULL NULL DROP TABLE t1; # End of 10.3 Tests +# +# MDEV-41146: WHERE is not checked when every table is const and +# the query needs a temporary table +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +# rand() < 0 is never true, so the result is empty. +select distinct a from t1 where a=1 and rand() < 0 group by a with rollup; +a +select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; +a +1 +NULL +drop table t1; +# End of 11.4 tests diff --git a/mysql-test/main/olap.test b/mysql-test/main/olap.test index 3b4852ebbdc..67018ecf8be 100644 --- a/mysql-test/main/olap.test +++ b/mysql-test/main/olap.test @@ -533,3 +533,19 @@ SELECT NULLIF( CAST( 'foo' AS DATE ), NULL & 'bar' ) AS f FROM t1 GROUP BY f WIT DROP TABLE t1; --echo # End of 10.3 Tests + +--echo # +--echo # MDEV-41146: WHERE is not checked when every table is const and +--echo # the query needs a temporary table +--echo # + +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); + +--echo # rand() < 0 is never true, so the result is empty. +select distinct a from t1 where a=1 and rand() < 0 group by a with rollup; +select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; + +drop table t1; + +--echo # End of 11.4 tests diff --git a/mysql-test/main/subselect_no_semijoin.result b/mysql-test/main/subselect_no_semijoin.result index 069bc7f9f9b..65eb2d81735 100644 --- a/mysql-test/main/subselect_no_semijoin.result +++ b/mysql-test/main/subselect_no_semijoin.result @@ -7668,7 +7668,7 @@ EXPLAIN { "query_block": { "select_id": 2, - "pseudo_bits_condition": "1 = t1.a or (1,(subquery#3))", + "outer_ref_condition": "1 = t1.a or (1,(subquery#3))", "nested_loop": [ { "table": { diff --git a/mysql-test/main/win.result b/mysql-test/main/win.result index 7af4a87d3eb..718e772a560 100644 --- a/mysql-test/main/win.result +++ b/mysql-test/main/win.result @@ -4866,3 +4866,27 @@ DROP TABLE t1; # # End of 10.11 tests # +# +# MDEV-41146: WHERE is not checked when every table is const and +# the query needs a temporary table +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int); +insert into t2 values (1),(10); +# rand() < 0 is never true, so the result is empty. +select a, row_number() over () from t1 where a=1 and rand() < 0; +a row_number() over () +select a, row_number() over () from t1 where a=1 and rand() >= 0; +a row_number() over () +1 1 +# t2.b > 5 is true only for the row where t2.b is 10. +select b, (select row_number() over () from t1 where t1.a=1 and t2.b > 5) x +from t2; +b x +1 NULL +10 1 +drop table t1, t2; +# +# End of 11.4 tests +# diff --git a/mysql-test/main/win.test b/mysql-test/main/win.test index dd40e61d312..1da3dfa3598 100644 --- a/mysql-test/main/win.test +++ b/mysql-test/main/win.test @@ -3150,3 +3150,28 @@ DROP TABLE t1; --echo # --echo # End of 10.11 tests --echo # + +--echo # +--echo # MDEV-41146: WHERE is not checked when every table is const and +--echo # the query needs a temporary table +--echo # + +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int); +insert into t2 values (1),(10); + +--echo # rand() < 0 is never true, so the result is empty. +select a, row_number() over () from t1 where a=1 and rand() < 0; +select a, row_number() over () from t1 where a=1 and rand() >= 0; + +--echo # t2.b > 5 is true only for the row where t2.b is 10. +--sorted_result +select b, (select row_number() over () from t1 where t1.a=1 and t2.b > 5) x +from t2; + +drop table t1, t2; + +--echo # +--echo # End of 11.4 tests +--echo # diff --git a/mysql-test/suite/encryption/r/tempfiles_encrypted.result b/mysql-test/suite/encryption/r/tempfiles_encrypted.result index f6180fcfa28..60bf65d3cad 100644 --- a/mysql-test/suite/encryption/r/tempfiles_encrypted.result +++ b/mysql-test/suite/encryption/r/tempfiles_encrypted.result @@ -4873,6 +4873,30 @@ DROP TABLE t1; # End of 10.11 tests # # +# MDEV-41146: WHERE is not checked when every table is const and +# the query needs a temporary table +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int); +insert into t2 values (1),(10); +# rand() < 0 is never true, so the result is empty. +select a, row_number() over () from t1 where a=1 and rand() < 0; +a row_number() over () +select a, row_number() over () from t1 where a=1 and rand() >= 0; +a row_number() over () +1 1 +# t2.b > 5 is true only for the row where t2.b is 10. +select b, (select row_number() over () from t1 where t1.a=1 and t2.b > 5) x +from t2; +b x +1 NULL +10 1 +drop table t1, t2; +# +# End of 11.4 tests +# +# # MDEV-23867: select crash in compute_window_func # set @save_sort_buffer_size=@@sort_buffer_size; diff --git a/sql/sql_explain.cc b/sql/sql_explain.cc index 4e67f1a2343..6b1eb02f2c9 100644 --- a/sql/sql_explain.cc +++ b/sql/sql_explain.cc @@ -1200,11 +1200,6 @@ void Explain_select::print_explain_json(Explain_query *query, writer->add_member("outer_ref_condition"); write_item(writer, outer_ref_cond); } - if (pseudo_bits_cond) - { - writer->add_member("pseudo_bits_condition"); - write_item(writer, pseudo_bits_cond); - } /* we do not print HAVING which always evaluates to TRUE */ if (having || (having_value == Item::COND_FALSE)) diff --git a/sql/sql_explain.h b/sql/sql_explain.h index 1aff4c05e60..18e2499acbd 100644 --- a/sql/sql_explain.h +++ b/sql/sql_explain.h @@ -248,7 +248,6 @@ public: /* Expensive constant condition */ Item *exec_const_cond; Item *outer_ref_cond; - Item *pseudo_bits_cond; /* HAVING condition */ Item *having; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 97ed619a584..6a07e34be0c 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -570,7 +570,7 @@ void JOIN::init(THD *thd_arg, List &fields_arg, first_select= sub_select; group_sent= 0; - outer_ref_cond= pseudo_bits_cond= NULL; + outer_ref_cond= NULL; in_to_exists_where= NULL; in_to_exists_having= NULL; emb_sjm_nest= NULL; @@ -14588,40 +14588,37 @@ make_join_select(JOIN *join,SQL_SELECT *select,COND *cond) join->exec_const_cond= const_cond; } - if (join->table_count != join->const_tables) { - COND *outer_ref_cond= make_cond_for_table(thd, cond, - join->const_table_map | - OUTER_REF_TABLE_BIT, - OUTER_REF_TABLE_BIT, - -1, FALSE, FALSE); - if (outer_ref_cond) + /* + Collect the conjuncts that are checked once for each execution + of the query, before the join reads a table that is not const. + Each refers to at least one pseudo table in pseudo_bits and to + no table that is not const. + + A conjunct that refers to a column of an outer query is always + collected, because that column has one value for each + execution. rand() takes a new value on each call, and a + conjunct that calls it is checked for each row of the last top + level table that is neither const nor a materialized semijoin + nest. A join of only const tables has no such table and + returns at most one row, and its rand() conjuncts are collected + here. + */ + table_map pseudo_bits= OUTER_REF_TABLE_BIT; + if (join->only_const_tables()) + pseudo_bits= PSEUDO_TABLE_BITS; + COND *pseudo_cond= make_cond_for_table(thd, cond, + join->const_table_map | + pseudo_bits, + pseudo_bits, + -1, FALSE, FALSE); + if (pseudo_cond) { - add_cond_and_fix(thd, &outer_ref_cond, join->outer_ref_cond); - join->outer_ref_cond= outer_ref_cond; + add_cond_and_fix(thd, &pseudo_cond, join->outer_ref_cond); + join->outer_ref_cond= pseudo_cond; Json_writer_object trace(thd); - trace.add("outer_ref_cond", outer_ref_cond); - } - else if (thd->is_error()) - DBUG_RETURN(1); - } - else - { - COND *pseudo_bits_cond= - make_cond_for_table(thd, cond, - join->const_table_map | - PSEUDO_TABLE_BITS, - PSEUDO_TABLE_BITS, - -1, FALSE, FALSE); - if (pseudo_bits_cond) - { - add_cond_and_fix(thd, &pseudo_bits_cond, - join->pseudo_bits_cond); - join->pseudo_bits_cond= pseudo_bits_cond; - - Json_writer_object trace(thd); - trace.add("pseudo_bits_cond", pseudo_bits_cond); + trace.add("outer_ref_cond", pseudo_cond); } else if (thd->is_error()) DBUG_RETURN(1); @@ -24056,12 +24053,11 @@ do_select(JOIN *join, Procedure *procedure) HAVING will be checked after processing aggregate functions, But WHERE should checked here (we alredy have read tables). Notice that make_join_select() splits all conditions in this case - into two groups exec_const_cond and outer_ref_cond. - If join->table_count == join->const_tables then it is - sufficient to check only the condition pseudo_bits_cond. + into two groups exec_const_cond and outer_ref_cond. exec_const_cond + is checked before the join starts, and it is sufficient to check + only the condition outer_ref_cond. */ - DBUG_ASSERT(join->outer_ref_cond == NULL); - if (!join->pseudo_bits_cond || join->pseudo_bits_cond->val_bool()) + if (!join->outer_ref_cond || join->outer_ref_cond->val_bool()) { // HAVING will be checked by end_select error= (*end_select)(join, 0, 0); @@ -31667,7 +31663,6 @@ int JOIN::save_explain_data_intern(Explain_query *output, xpl_sel->exec_const_cond= exec_const_cond; xpl_sel->outer_ref_cond= outer_ref_cond; - xpl_sel->pseudo_bits_cond= pseudo_bits_cond; if (tmp_having) xpl_sel->having= tmp_having; else diff --git a/sql/sql_select.h b/sql/sql_select.h index 2711b1138b1..f7563577f0f 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -1648,8 +1648,15 @@ public: ORDER *order, *group_list, *proc_param; //hold parameters of mysql_select COND *conds; // ---"--- Item *conds_history; // store WHERE for explain - COND *outer_ref_cond; /// *join_list; ///< list of joined tables in reverse order COND_EQUAL *cond_equal;