From 05e0127478c39437be53668f0db1d674071e2485 Mon Sep 17 00:00:00 2001 From: Sergey Petrunya Date: Wed, 14 Dec 2011 04:39:29 +0400 Subject: [PATCH] BUG#901506: Crash in TABLE_LIST::print on EXPLAIN EXTENDED - Let JTBM optimization code handle the case where the subquery is degenerate and doesn't have a join query plan. Regular materialization would fall back to IN->EXISTS for such cases. Semi-Join materialization does not have such option, instead we introduce and use "constant JTBM join tabs". --- mysql-test/r/subselect_mat.result | 14 ++ mysql-test/r/subselect_sj_mat.result | 15 ++ mysql-test/t/subselect_sj_mat.test | 12 ++ sql/item_subselect.h | 44 ++++- sql/opt_subselect.cc | 240 ++++++++++++++++++++++++++- sql/opt_subselect.h | 2 + sql/sql_select.cc | 140 +++------------- 7 files changed, 343 insertions(+), 124 deletions(-) diff --git a/mysql-test/r/subselect_mat.result b/mysql-test/r/subselect_mat.result index 81c8b2e1e84..fa8ce49941c 100644 --- a/mysql-test/r/subselect_mat.result +++ b/mysql-test/r/subselect_mat.result @@ -1764,6 +1764,20 @@ SELECT MIN(a) FROM t1, t2 WHERE b IN (SELECT c FROM t3 GROUP BY c); MIN(a) 1 DROP TABLE t1,t2,t3; +# +# BUG#901506: Crash in TABLE_LIST::print on EXPLAIN EXTENDED +# +CREATE TABLE t1 ( a INT, KEY(a) ); +INSERT INTO t1 VALUES (8); +EXPLAIN EXTENDED +SELECT * FROM t1 +WHERE a IN ( SELECT MIN(a) FROM t1 ); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 PRIMARY t1 system NULL NULL NULL NULL 1 100.00 +2 DEPENDENT SUBQUERY NULL NULL NULL NULL NULL NULL NULL NULL Select tables optimized away +Warnings: +Note 1003 select 8 AS `a` from `test`.`t1` where <8>((8,(select min(`test`.`t1`.`a`) from `test`.`t1` having ((8) = (min(`test`.`t1`.`a`)))))) +DROP TABLE t1; # This must be at the end: set optimizer_switch=@subselect_sj_mat_tmp; set @subselect_mat_test_optimizer_switch_value=null; diff --git a/mysql-test/r/subselect_sj_mat.result b/mysql-test/r/subselect_sj_mat.result index 4845616018c..7bbf5aaf66d 100644 --- a/mysql-test/r/subselect_sj_mat.result +++ b/mysql-test/r/subselect_sj_mat.result @@ -1800,5 +1800,20 @@ SELECT MIN(a) FROM t1, t2 WHERE b IN (SELECT c FROM t3 GROUP BY c); MIN(a) 1 DROP TABLE t1,t2,t3; +# +# BUG#901506: Crash in TABLE_LIST::print on EXPLAIN EXTENDED +# +CREATE TABLE t1 ( a INT, KEY(a) ); +INSERT INTO t1 VALUES (8); +EXPLAIN EXTENDED +SELECT * FROM t1 +WHERE a IN ( SELECT MIN(a) FROM t1 ); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 PRIMARY t1 system a NULL NULL NULL 1 100.00 +1 PRIMARY system NULL NULL NULL NULL 1 100.00 +2 MATERIALIZED NULL NULL NULL NULL NULL NULL NULL NULL Select tables optimized away +Warnings: +Note 1003 select 8 AS `a` from (select min(`test`.`t1`.`a`) from `test`.`t1`) join `test`.`t1` where 1 +DROP TABLE t1; # This must be at the end: set optimizer_switch=@subselect_sj_mat_tmp; diff --git a/mysql-test/t/subselect_sj_mat.test b/mysql-test/t/subselect_sj_mat.test index a8dbeded84a..12d04097a51 100644 --- a/mysql-test/t/subselect_sj_mat.test +++ b/mysql-test/t/subselect_sj_mat.test @@ -1465,6 +1465,18 @@ SELECT MIN(a) FROM t1, t2 WHERE b IN (SELECT c FROM t3 GROUP BY c); DROP TABLE t1,t2,t3; +--echo # +--echo # BUG#901506: Crash in TABLE_LIST::print on EXPLAIN EXTENDED +--echo # +CREATE TABLE t1 ( a INT, KEY(a) ); +INSERT INTO t1 VALUES (8); + +EXPLAIN EXTENDED + SELECT * FROM t1 + WHERE a IN ( SELECT MIN(a) FROM t1 ); + +DROP TABLE t1; + --echo # This must be at the end: set optimizer_switch=@subselect_sj_mat_tmp; diff --git a/sql/item_subselect.h b/sql/item_subselect.h index 880b505a8d1..fad951dd80f 100644 --- a/sql/item_subselect.h +++ b/sql/item_subselect.h @@ -446,11 +446,44 @@ public: Same as above, but they also allow to scan the materialized table. */ bool sjm_scan_allowed; - double jtbm_read_time; - double jtbm_record_count; - bool is_jtbm_merged; - bool is_jtbm_const_tab; + /* + JoinTaB Materialization (JTBM) members + */ + + /* + TRUE <=> This subselect has been converted into non-mergeable semi-join + table. + */ + bool is_jtbm_merged; + + /* (Applicable if is_jtbm_merged==TRUE) Time required to run the materialized join */ + double jtbm_read_time; + + /* (Applicable if is_jtbm_merged==TRUE) Number of output rows in materialized join */ + double jtbm_record_count; + + /* + (Applicable if is_jtbm_merged==TRUE) TRUE <=> The materialized subselect is + a degenerate subselect which produces 0 or 1 rows, which we know at + optimization phase. + Examples: + 1. subquery has "Impossible WHERE": + + SELECT * FROM ot WHERE ot.column IN (SELECT it.col FROM it WHERE 2 > 3) + + 2. Subquery produces one row which opt_sum.cc is able to get with one lookup: + + SELECT * FROM ot WHERE ot.column IN (SELECT MAX(it.key_col) FROM it) + */ + bool is_jtbm_const_tab; + + /* + (Applicable if is_jtbm_const_tab==TRUE) Whether the subquery has produced + the row (or not) + */ + bool jtbm_const_row_found; + /* TRUE<=>this is a flattenable semi-join, false overwise. */ @@ -741,6 +774,9 @@ public: friend class subselect_hash_sj_engine; friend class Item_in_subselect; + friend bool setup_jtbm_semi_joins(JOIN *join, List *join_list, + Item **join_where); + }; diff --git a/sql/opt_subselect.cc b/sql/opt_subselect.cc index 46ecb5ff65a..794bbf3e5dc 100644 --- a/sql/opt_subselect.cc +++ b/sql/opt_subselect.cc @@ -4611,6 +4611,234 @@ enum_nested_loop_state join_tab_execution_startup(JOIN_TAB *tab) } +/* + Create a dummy temporary table, useful only for the sake of having a + TABLE* object with map,tablenr and maybe_null properties. + + This is used by non-mergeable semi-join materilization code to handle + degenerate cases where materialized subquery produced "Impossible WHERE" + and thus wasn't materialized. +*/ + +TABLE *create_dummy_tmp_table(THD *thd) +{ + DBUG_ENTER("create_dummy_tmp_table"); + TABLE *table; + TMP_TABLE_PARAM sjm_table_param; + sjm_table_param.init(); + sjm_table_param.field_count= 1; + List sjm_table_cols; + Item *column_item= new Item_int(1); + sjm_table_cols.push_back(column_item); + if (!(table= create_tmp_table(thd, &sjm_table_param, + sjm_table_cols, (ORDER*) 0, + TRUE /* distinct */, + 1, /*save_sum_fields*/ + thd->options | TMP_TABLE_ALL_COLUMNS, + HA_POS_ERROR /*rows_limit */, + (char*)"dummy", TRUE /* Do not open */))) + { + DBUG_RETURN(NULL); + } + DBUG_RETURN(table); +} + + +/* + A class that is used to catch one single tuple that is sent to the join + output, and save it in Item_cache element(s). + + It is very similar to select_singlerow_subselect but doesn't require a + Item_singlerow_subselect item. +*/ + +class select_value_catcher :public select_subselect +{ +public: + select_value_catcher(Item_subselect *item_arg) + :select_subselect(item_arg) + {} + int send_data(List &items); + int setup(List *items); + bool assigned; /* TRUE <=> we've caught a value */ + uint n_elements; /* How many elements we get */ + Item_cache **row; /* Array of cache elements */ +}; + + +int select_value_catcher::setup(List *items) +{ + assigned= FALSE; + n_elements= items->elements; + + if (!(row= (Item_cache**) sql_alloc(sizeof(Item_cache*)*n_elements))) + return TRUE; + + Item *sel_item; + List_iterator li(*items); + for (uint i= 0; (sel_item= li++); i++) + { + if (!(row[i]= Item_cache::get_cache(sel_item))) + return TRUE; + row[i]->setup(sel_item); + } + return FALSE; +} + + +int select_value_catcher::send_data(List &items) +{ + DBUG_ENTER("select_value_catcher::send_data"); + DBUG_ASSERT(!assigned); + DBUG_ASSERT(items.elements == n_elements); + + if (unit->offset_limit_cnt) + { // Using limit offset,count + unit->offset_limit_cnt--; + DBUG_RETURN(0); + } + + Item *val_item; + List_iterator_fast li(items); + for (uint i= 0; (val_item= li++); i++) + { + row[i]->store(val_item); + row[i]->cache_value(); + } + assigned= TRUE; + DBUG_RETURN(0); +} + + +/* + Setup JTBM join tabs for execution +*/ + +bool setup_jtbm_semi_joins(JOIN *join, List *join_list, + Item **join_where) +{ + TABLE_LIST *table; + NESTED_JOIN *nested_join; + List_iterator li(*join_list); + DBUG_ENTER("setup_jtbm_semi_joins"); + + while ((table= li++)) + { + Item_in_subselect *item; + + if ((item= table->jtbm_subselect)) + { + Item_in_subselect *subq_pred= item; + double rows; + double read_time; + + /* + Perform optimization of the subquery, so that we know estmated + - cost of materialization process + - how many records will be in the materialized temp.table + */ + if (subq_pred->optimize(&rows, &read_time)) + DBUG_RETURN(TRUE); + + subq_pred->jtbm_read_time= read_time; + subq_pred->jtbm_record_count=rows; + JOIN *subq_join= subq_pred->unit->first_select()->join; + + if (!subq_join->tables_list || !subq_join->table_count) + { + /* + A special case; subquery's join is degenerate, and it either produces + 0 or 1 record. Examples of both cases: + + select * from ot where col in (select ... from it where 2>3) + select * from ot where col in (select min(it.key) from it) + + in this case, the subquery predicate has not been setup for + materialization. In particular, there is no materialized temp.table. + We'll now need to + 1. Check whether 1 or 0 records are produced, setup this as a + constant join tab. + 2. Create a dummy temporary table, because all of the join + optimization code relies on TABLE object being present (here we + follow a bad tradition started by derived tables) + */ + DBUG_ASSERT(subq_pred->engine->engine_type() == + subselect_engine::SINGLE_SELECT_ENGINE); + subselect_single_select_engine *engine= + (subselect_single_select_engine*)subq_pred->engine; + select_value_catcher *new_sink; + if (!(new_sink= new select_value_catcher(subq_pred))) + DBUG_RETURN(TRUE); + if (new_sink->setup(&engine->select_lex->join->fields_list) || + engine->select_lex->join->change_result(new_sink) || + engine->exec()) + { + DBUG_RETURN(TRUE); + } + subq_pred->is_jtbm_const_tab= TRUE; + + if (new_sink->assigned) + { + subq_pred->jtbm_const_row_found= TRUE; + /* + Subselect produced one row, which is saved in new_sink->row. + Inject "left_expr[i] == row[i] equalities into parent's WHERE. + */ + Item *eq_cond; + for (uint i= 0; i < subq_pred->left_expr->cols(); i++) + { + eq_cond= new Item_func_eq(subq_pred->left_expr->element_index(i), + new_sink->row[i]); + if (!eq_cond || eq_cond->fix_fields(join->thd, &eq_cond)) + DBUG_RETURN(1); + + (*join_where)= and_items(*join_where, eq_cond); + } + } + else + { + /* Subselect produced no rows. Just set the flag, */ + subq_pred->jtbm_const_row_found= FALSE; + } + + /* Set up a dummy TABLE*, optimizer code needs JOIN_TABs to have TABLE */ + TABLE *dummy_table; + if (!(dummy_table= create_dummy_tmp_table(join->thd))) + DBUG_RETURN(1); + table->table= dummy_table; + table->table->pos_in_table_list= table; + setup_table_map(table->table, table, table->jtbm_table_no); + } + else + { + DBUG_ASSERT(subq_pred->test_set_strategy(SUBS_MATERIALIZATION)); + subq_pred->is_jtbm_const_tab= FALSE; + subselect_hash_sj_engine *hash_sj_engine= + ((subselect_hash_sj_engine*)item->engine); + + table->table= hash_sj_engine->tmp_table; + table->table->pos_in_table_list= table; + + setup_table_map(table->table, table, table->jtbm_table_no); + + Item *sj_conds= hash_sj_engine->semi_join_conds; + + (*join_where)= and_items(*join_where, sj_conds); + if (!(*join_where)->fixed) + (*join_where)->fix_fields(join->thd, join_where); + } + } + + if ((nested_join= table->nested_join)) + { + if (setup_jtbm_semi_joins(join, &nested_join->join_list, join_where)) + DBUG_RETURN(TRUE); + } + } + DBUG_RETURN(FALSE); +} + + /** Choose an optimal strategy to execute an IN/ALL/ANY subquery predicate based on cost. @@ -4919,8 +5147,16 @@ bool JOIN::choose_tableless_subquery_plan() NULL. */ } - - if (subs_predicate->is_in_predicate()) + + /* + For IN subqueries, use IN->EXISTS transfomation, unless the subquery + has been converted to a JTBM semi-join. In that case, just leave + everything as-is, setup_jtbm_semi_joins() has special handling for cases + like this. + */ + if (subs_predicate->is_in_predicate() && + !(subs_predicate->substype() == Item_subselect::IN_SUBS && + ((Item_in_subselect*)subs_predicate)->is_jtbm_merged)) { Item_in_subselect *in_subs; in_subs= (Item_in_subselect*) subs_predicate; diff --git a/sql/opt_subselect.h b/sql/opt_subselect.h index 94c6153f0c1..07f1fc77a20 100644 --- a/sql/opt_subselect.h +++ b/sql/opt_subselect.h @@ -10,6 +10,8 @@ int check_and_do_in_subquery_rewrites(JOIN *join); bool convert_join_subqueries_to_semijoins(JOIN *join); int pull_out_semijoin_tables(JOIN *join); bool optimize_semijoin_nests(JOIN *join, table_map all_table_map); +bool setup_jtbm_semi_joins(JOIN *join, List *join_list, + Item **join_where); // used by Loose_scan_opt ulonglong get_bound_sj_equalities(TABLE_LIST *sj_nest, diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 7a86557901f..9c1f53ce0d9 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -798,117 +798,6 @@ err: } -/* - Create a dummy temporary table, useful only for the sake of having a - TABLE* object with map,tablenr and maybe_null properties. - - This is used by non-mergeable semi-join materilization code to handle - degenerate cases where materialized subquery produced "Impossible WHERE" - and thus wasn't materialized. -*/ - -TABLE *create_dummy_tmp_table(THD *thd) -{ - DBUG_ENTER("create_dummy_tmp_table"); - TABLE *table; - TMP_TABLE_PARAM sjm_table_param; - sjm_table_param.init(); - sjm_table_param.field_count= 1; - List sjm_table_cols; - Item *column_item= new Item_int(1); - sjm_table_cols.push_back(column_item); - if (!(table= create_tmp_table(thd, &sjm_table_param, - sjm_table_cols, (ORDER*) 0, - TRUE /* distinct */, - 1, /*save_sum_fields*/ - thd->options | TMP_TABLE_ALL_COLUMNS, - HA_POS_ERROR /*rows_limit */, - (char*)"dummy", TRUE /* Do not open */))) - { - DBUG_RETURN(NULL); - } - DBUG_RETURN(table); -} - - -void -setup_jtbm_semi_joins(JOIN *join, List *join_list, Item **join_where) -{ - TABLE_LIST *table; - NESTED_JOIN *nested_join; - List_iterator li(*join_list); - DBUG_ENTER("setup_jtbm_semi_joins"); - - - while ((table= li++)) - { - Item_in_subselect *item; - - if ((item= table->jtbm_subselect)) - { - Item_in_subselect *subq_pred= item; - double rows; - double read_time; - - subq_pred->optimize(&rows, &read_time); - subq_pred->jtbm_read_time= read_time; - subq_pred->jtbm_record_count=rows; - subq_pred->is_jtbm_merged= TRUE; - JOIN *subq_join= subq_pred->unit->first_select()->join; - if (!subq_join->tables_list || !subq_join->table_count) - { - /* - This is an empty and constant table. - - TODO: what if this is not empty but still constant? - - We'll need to check the equality but there's no materializatnion - table? - - A: create an IN-equality from - - left_expr - - right_expr. Q: how can right-expr exist in the context of - parent select? We don't have refs from outside to inside! - A: create/check in the context of the child select? - - for injection, check how in->exists is performed. - */ - subq_pred->is_jtbm_const_tab= TRUE; - - TABLE *dummy_table= create_dummy_tmp_table(join->thd); - table->table= dummy_table; - table->table->pos_in_table_list= table; - - setup_table_map(table->table, table, table->jtbm_table_no); - } - else - { - DBUG_ASSERT(subq_pred->test_set_strategy(SUBS_MATERIALIZATION)); - subq_pred->is_jtbm_const_tab= FALSE; - subselect_hash_sj_engine *hash_sj_engine= - ((subselect_hash_sj_engine*)item->engine); - - table->table= hash_sj_engine->tmp_table; - table->table->pos_in_table_list= table; - - setup_table_map(table->table, table, table->jtbm_table_no); - - Item *sj_conds= hash_sj_engine->semi_join_conds; - - (*join_where)= and_items(*join_where, sj_conds); - if (!(*join_where)->fixed) - (*join_where)->fix_fields(join->thd, join_where); - } - } - - if ((nested_join= table->nested_join)) - { - setup_jtbm_semi_joins(join, &nested_join->join_list, join_where); - } - } - DBUG_VOID_RETURN; -} - /** global select optimisation. @@ -1033,7 +922,8 @@ JOIN::optimize() thd->restore_active_arena(arena, &backup); } - setup_jtbm_semi_joins(this, join_list, &conds); + if (setup_jtbm_semi_joins(this, join_list, &conds)) + DBUG_RETURN(1); conds= optimize_cond(this, conds, join_list, &cond_value, &cond_equal); @@ -15656,7 +15546,12 @@ join_read_const_table(JOIN_TAB *tab, POSITION *pos) tab->table->pos_in_table_list->jtbm_subselect->is_jtbm_const_tab) { /* Row will not be found */ - DBUG_RETURN(-1); + int res; + if (tab->table->pos_in_table_list->jtbm_subselect->jtbm_const_row_found) + res= 0; + else + res= -1; + DBUG_RETURN(res); } else if (tab->type == JT_SYSTEM) { @@ -21452,11 +21347,20 @@ void TABLE_LIST::print(THD *thd, table_map eliminated_tables, String *str, } else if (jtbm_subselect) { - str->append(STRING_WITH_LEN(" (")); - subselect_hash_sj_engine *hash_engine; - hash_engine= (subselect_hash_sj_engine*)jtbm_subselect->engine; - hash_engine->materialize_engine->print(str, query_type); - str->append(')'); + if (jtbm_subselect->is_jtbm_const_tab) + { + str->append(STRING_WITH_LEN(" (")); + jtbm_subselect->engine->print(str, query_type); + str->append(')'); + } + else + { + str->append(STRING_WITH_LEN(" (")); + subselect_hash_sj_engine *hash_engine; + hash_engine= (subselect_hash_sj_engine*)jtbm_subselect->engine; + hash_engine->materialize_engine->print(str, query_type); + str->append(')'); + } } else {