From 1a6f718a37d79be312f680c088020955696aa4ad Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Mon, 24 Nov 2025 14:09:19 -0500 Subject: [PATCH 1/2] MDEV-38177: FULL OUTER JOIN Allowed JOIN Orders This is a small cleanup commit ahead of the feature work for this MDEV: - Upgrade JOIN::get_allowed_nj_tables, JOIN::calc_allowed_top_level_tables to const methods as they have no side-effects and don't mutate object state - Document that JOIN_TAB::tab_list is the same as JOIN_TAB::table->pos_in_table_list - Direct access to nj_map replaced by functions to clarify expected usage - Remove a dead code path not executed by any regression tests - Invert and dedent a couple of loops to keep the happy path on the left - Cleanup restore_prev_nj_state and build_bitmap_for_nested_joins --- sql/item_subselect.cc | 4 +- sql/opt_hints.cc | 12 +-- sql/opt_trace.cc | 2 +- sql/sql_select.cc | 193 +++++++++++++++++++++--------------------- sql/sql_select.h | 23 ++++- sql/table.h | 9 ++ 6 files changed, 134 insertions(+), 109 deletions(-) diff --git a/sql/item_subselect.cc b/sql/item_subselect.cc index ed4797a34fe73..3360a62502e5f 100644 --- a/sql/item_subselect.cc +++ b/sql/item_subselect.cc @@ -4722,7 +4722,7 @@ void subselect_union_engine::print(String *str, enum_query_type query_type) void subselect_uniquesubquery_engine::print(String *str, enum_query_type query_type) { - TABLE *table= tab->tab_list ? tab->tab_list->table : tab->table; + TABLE *table= tab->get_tab_list() ? tab->get_tab_list()->table : tab->table; str->append(STRING_WITH_LEN("(")); tab->ref.items[0]->print(str, query_type); str->append(STRING_WITH_LEN(" in ")); @@ -4775,7 +4775,7 @@ void subselect_uniquesubquery_engine::print(String *str) void subselect_indexsubquery_engine::print(String *str, enum_query_type query_type) { - TABLE *table= tab->tab_list ? tab->tab_list->table : tab->table; + TABLE *table= tab->get_tab_list() ? tab->get_tab_list()->table : tab->table; str->append(STRING_WITH_LEN("(")); tab->ref.items[0]->print(str, query_type); str->append(STRING_WITH_LEN(" in ")); diff --git a/sql/opt_hints.cc b/sql/opt_hints.cc index 3d8f9fb919801..a73be3f991c97 100644 --- a/sql/opt_hints.cc +++ b/sql/opt_hints.cc @@ -1321,7 +1321,7 @@ bool Opt_hints_qb::set_join_hint_deps(JOIN *join, bool hint_table_found= false; for (uint i= 0; i < join->table_count; i++) { - TABLE_LIST *table= join->join_tab[i].tab_list; + TABLE_LIST *table= join->join_tab[i].get_tab_list(); if (!compare_table_name(&tbl_name_and_qb, table)) { hint_table_found= true; @@ -1337,7 +1337,7 @@ bool Opt_hints_qb::set_join_hint_deps(JOIN *join, // Hint tables are always dependent on preceding tables join_tab->dependent |= hint_tab_map; update_nested_join_deps(join, join_tab, hint_tab_map); - hint_tab_map |= join_tab->tab_list->get_map(); + hint_tab_map |= join_tab->get_tab_list()->get_map(); break; } } @@ -1356,7 +1356,7 @@ bool Opt_hints_qb::set_join_hint_deps(JOIN *join, JOIN_TAB *join_tab= &join->join_tab[i]; const table_map dependent_tables= get_other_dep(join, hint->hint_type, hint_tab_map, - join_tab->tab_list->get_map()); + join_tab->get_tab_list()->get_map()); update_nested_join_deps(join, join_tab, dependent_tables); join_tab->dependent |= dependent_tables; } @@ -1422,14 +1422,14 @@ bool Opt_hints_qb::set_join_hint_deps(JOIN *join, void Opt_hints_qb::update_nested_join_deps(JOIN *join, const JOIN_TAB *hint_tab, table_map hint_tab_map) { - const TABLE_LIST *table= hint_tab->tab_list; + const TABLE_LIST *table= hint_tab->get_tab_list(); if (table->embedding) { for (uint i= 0; i < join->table_count; i++) { JOIN_TAB *tab= &join->join_tab[i]; /* Walk up the nested joins that tab->table is a part of */ - for (TABLE_LIST *emb= tab->tab_list->embedding; emb; emb=emb->embedding) + for (TABLE_LIST *emb= tab->get_tab_list()->embedding; emb; emb=emb->embedding) { /* Apply the rule only for outer joins. Semi-joins do not impose such @@ -1439,7 +1439,7 @@ void Opt_hints_qb::update_nested_join_deps(JOIN *join, const JOIN_TAB *hint_tab, { const NESTED_JOIN *const nested_join= emb->nested_join; /* Is hint_tab somewhere inside this nested join, too? */ - if (hint_tab->embedding_map & nested_join->nj_map) + if (hint_tab->embedding_map & nested_join->get_nj_map()) { /* Yes, it is. Then, tab->table be also dependent on all outside diff --git a/sql/opt_trace.cc b/sql/opt_trace.cc index ed8b0137d84c2..410fb14dc2842 100644 --- a/sql/opt_trace.cc +++ b/sql/opt_trace.cc @@ -660,7 +660,7 @@ void trace_plan_prefix(Json_writer_object *jsobj, JOIN *join, uint idx, prefix_str.length(0); for (uint i= join->const_tables; i < idx; i++) { - TABLE_LIST *const tr= join->positions[i].table->tab_list; + TABLE_LIST *const tr= join->positions[i].table->get_tab_list(); if (!(tr->map & join_tables)) { String str; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 8133cc4074666..3898811cbbc71 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -584,7 +584,7 @@ static void trace_table_dependencies(THD *thd, for (uint i= 0; i < table_count; i++) { - TABLE_LIST *table_ref= join_tabs[i].tab_list; + TABLE_LIST *table_ref= join_tabs[i].get_tab_list(); Json_writer_object trace_one_table(thd); trace_one_table. add_table_name(&join_tabs[i]). @@ -2399,7 +2399,7 @@ JOIN::optimize_inner() } if (!allowed_top_level_tables) - calc_allowed_top_level_tables(select_lex); + allowed_top_level_tables= calc_allowed_top_level_tables(select_lex); if (optimize_constant_subqueries()) DBUG_RETURN(1); @@ -5799,7 +5799,7 @@ make_join_statistics(JOIN *join, List &tables_list, outer_join|= table->map; s->embedding_map= 0; for (;embedding; embedding= embedding->embedding) - s->embedding_map|= embedding->nested_join->nj_map; + s->embedding_map|= embedding->nested_join->get_nj_map(); continue; } if (embedding) @@ -5820,7 +5820,7 @@ make_join_statistics(JOIN *join, List &tables_list, } inside_an_outer_join= TRUE; NESTED_JOIN *nested_join= embedding->nested_join; - s->embedding_map|=nested_join->nj_map; + s->embedding_map|=nested_join->get_nj_map(); s->dependent|= embedding->dep_tables; embedding= embedding->embedding; outer_join|= nested_join->used_tables; @@ -5899,18 +5899,6 @@ make_join_statistics(JOIN *join, List &tables_list, join->const_tables= const_count; eliminate_tables(join); - /* - Temporary gate. As the FULL JOIN implementation matures, this keeps moving - deeper into the server until it's eventually eliminated. - */ - if (thd->lex->full_join_count && !thd->lex->describe) - { - my_error(ER_NOT_SUPPORTED_YET, MYF(0), - "FULL JOINs that cannot be converted to LEFT, RIGHT, or " - "INNER JOINs"); - goto error; - } - join->const_table_map &= ~no_rows_const_tables; const_count= join->const_tables; found_const_table_map= join->const_table_map; @@ -10407,6 +10395,18 @@ choose_plan(JOIN *join, table_map join_tables, TABLE_LIST *emb_sjm_nest) } } + /* + Temporary gate. As the FULL JOIN implementation matures, this keeps moving + deeper into the server until it's eventually eliminated. + */ + if (thd->lex->full_join_count && !thd->lex->describe) + { + my_error(ER_NOT_SUPPORTED_YET, MYF(0), + "FULL JOINs that cannot be converted to LEFT, RIGHT, or " + "INNER JOINs"); + DBUG_RETURN(TRUE); + } + join->emb_sjm_nest= 0; DBUG_RETURN(FALSE); } @@ -15910,13 +15910,13 @@ uint check_join_cache_usage(JOIN_TAB *tab, !(join->allowed_join_cache_types & JOIN_CACHE_INCREMENTAL_BIT); bool no_hashed_cache= !(join->allowed_join_cache_types & JOIN_CACHE_HASHED_BIT); - bool hint_disables_bnl= !hint_table_state(join->thd, tab->tab_list->table, + bool hint_disables_bnl= !hint_table_state(join->thd, tab->get_tab_list()->table, BNL_HINT_ENUM, true); - bool no_bka_cache= !hint_table_state(join->thd, tab->tab_list->table, + bool no_bka_cache= !hint_table_state(join->thd, tab->get_tab_list()->table, BKA_HINT_ENUM, join->allowed_join_cache_types & JOIN_CACHE_BKA_BIT); - bool hint_forces_bnl= hint_table_state(join->thd, tab->tab_list->table, + bool hint_forces_bnl= hint_table_state(join->thd, tab->get_tab_list()->table, BNL_HINT_ENUM, false); - bool hint_forces_bka= hint_table_state(join->thd, tab->tab_list->table, + bool hint_forces_bka= hint_table_state(join->thd, tab->get_tab_list()->table, BKA_HINT_ENUM, false); join->return_tab= 0; @@ -20552,7 +20552,7 @@ static COND *rewrite_full_outer_joins(JOIN *join, First unused bit in nested_join_map after the call. */ -static uint build_bitmap_for_nested_joins(List *join_list, +static uint build_bitmap_for_nested_joins(List *join_list, uint first_unused) { List_iterator li(*join_list); @@ -20561,26 +20561,26 @@ static uint build_bitmap_for_nested_joins(List *join_list, while ((table= li++)) { NESTED_JOIN *nested_join; - if ((nested_join= table->nested_join)) - { - /* - It is guaranteed by simplify_joins() function that a nested join - that has only one child represents a single table VIEW (and the child - is an underlying table). We don't assign bits to such nested join - structures because - 1. it is redundant (a "sequence" of one table cannot be interleaved - with anything) - 2. we could run out bits in nested_join_map otherwise. - */ - if (nested_join->n_tables != 1) - { - /* Don't assign bits to sj-nests */ - if (table->on_expr) - nested_join->nj_map= (nested_join_map) 1 << first_unused++; - first_unused= build_bitmap_for_nested_joins(&nested_join->join_list, - first_unused); - } - } + if (!(nested_join= table->nested_join)) + continue; + + /* + It is guaranteed by simplify_joins() function that a nested join + that has only one child represents a single table VIEW (and the child + is an underlying table). We don't assign bits to such nested join + structures because + 1. it is redundant (a "sequence" of one table cannot be interleaved + with anything) + 2. we could run out bits in nested_join_map otherwise. + */ + if (nested_join->n_tables == 1) + continue; + + /* Don't assign bits to sj-nests */ + if (table->on_expr) + nested_join->set_nj_map(first_unused++); + first_unused= build_bitmap_for_nested_joins(&nested_join->join_list, + first_unused); } DBUG_RETURN(first_unused); } @@ -20727,47 +20727,47 @@ static bool check_interleaving_with_nj(JOIN_TAB *next_tab) if (join->cur_embedding_map & ~next_tab->embedding_map) { - /* + /* next_tab is outside of the "pair of brackets" we're currently in. Cannot add it. */ - return TRUE; + DBUG_ASSERT(false); } - - TABLE_LIST *next_emb= next_tab->table->pos_in_table_list->embedding; + /* Do update counters for "pairs of brackets" that we've left (marked as X,Y,Z in the above picture) */ - for (;next_emb && next_emb != join->emb_sjm_nest; + for (TABLE_LIST *next_emb= next_tab->table->pos_in_table_list->embedding; + next_emb && next_emb != join->emb_sjm_nest; next_emb= next_emb->embedding) { - if (!next_emb->sj_on_expr) - { - next_emb->nested_join->counter++; - DBUG_ASSERT(next_emb->nested_join->counter <= next_emb->nested_join->n_tables); - if (next_emb->nested_join->counter == 1) - { - /* - next_emb is the first table inside a nested join we've "entered". In - the picture above, we're looking at the 'X' bracket. Don't exit yet - as X bracket might have Y pair bracket. - */ - join->cur_embedding_map |= next_emb->nested_join->nj_map; - } - - DBUG_ASSERT(next_emb->nested_join->n_tables >= - next_emb->nested_join->counter); + if (next_emb->sj_on_expr) + continue; - if (next_emb->nested_join->n_tables != - next_emb->nested_join->counter) - break; + next_emb->nested_join->counter++; + DBUG_ASSERT(next_emb->nested_join->counter <= next_emb->nested_join->n_tables); + if (next_emb->nested_join->counter == 1) + { /* - We're currently at Y or Z-bracket as depicted in the above picture. - Mark that we've left it and continue walking up the brackets hierarchy. + next_emb is the first table inside a nested join we've "entered". In + the picture above, we're looking at the 'X' bracket. Don't exit yet + as X bracket might have Y pair bracket. */ - join->cur_embedding_map &= ~next_emb->nested_join->nj_map; + join->cur_embedding_map |= next_emb->nested_join->get_nj_map(); } + + DBUG_ASSERT(next_emb->nested_join->n_tables >= + next_emb->nested_join->counter); + + if (next_emb->nested_join->n_tables != + next_emb->nested_join->counter) + break; + /* + We're currently at Y or Z-bracket as depicted in the above picture. + Mark that we've left it and continue walking up the brackets hierarchy. + */ + join->cur_embedding_map &= ~next_emb->nested_join->get_nj_map(); } return FALSE; } @@ -20829,24 +20829,24 @@ static void restore_prev_nj_state(JOIN_TAB *last) { TABLE_LIST *last_emb= last->table->pos_in_table_list->embedding; JOIN *join= last->join; - for (;last_emb != NULL && last_emb != join->emb_sjm_nest; + for (;last_emb != NULL && last_emb != join->emb_sjm_nest; last_emb= last_emb->embedding) { - if (!last_emb->sj_on_expr) - { - NESTED_JOIN *nest= last_emb->nested_join; - DBUG_ASSERT(nest->counter > 0); - - bool was_fully_covered= nest->is_fully_covered(); - - join->cur_embedding_map|= nest->nj_map; + if (last_emb->sj_on_expr) + continue; - if (--nest->counter == 0) - join->cur_embedding_map&= ~nest->nj_map; - - if (!was_fully_covered) - break; - } + NESTED_JOIN *nest= last_emb->nested_join; + DBUG_ASSERT(nest->counter > 0); + + bool was_fully_covered= nest->is_fully_covered(); + + join->cur_embedding_map|= nest->get_nj_map(); + + if (--nest->counter == 0) + join->cur_embedding_map&= ~nest->get_nj_map(); + + if (!was_fully_covered) + break; } } @@ -20865,12 +20865,13 @@ static void restore_prev_nj_state(JOIN_TAB *last) semi-join nest. */ -void JOIN::calc_allowed_top_level_tables(SELECT_LEX *lex) +table_map JOIN::calc_allowed_top_level_tables(SELECT_LEX *lex) const { TABLE_LIST *tl; List_iterator ti(lex->leaf_tables); DBUG_ENTER("JOIN::calc_allowed_top_level_tables"); DBUG_ASSERT(allowed_top_level_tables == 0); // Should only be called once + table_map allowed_top_level= 0; while ((tl= ti++)) { @@ -20887,7 +20888,7 @@ void JOIN::calc_allowed_top_level_tables(SELECT_LEX *lex) if (!(embedding= tl->embedding)) { - allowed_top_level_tables |= map; + allowed_top_level |= map; continue; } @@ -20902,7 +20903,7 @@ void JOIN::calc_allowed_top_level_tables(SELECT_LEX *lex) // Ok we are in the parent nested outer join nest. if (!embedding) { - allowed_top_level_tables |= map; + allowed_top_level |= map; continue; } embedding->nested_join->direct_children_map |= map; @@ -20924,9 +20925,9 @@ void JOIN::calc_allowed_top_level_tables(SELECT_LEX *lex) embedding->nested_join->direct_children_map |= map; } else - allowed_top_level_tables |= map; + allowed_top_level |= map; } - DBUG_VOID_RETURN; + DBUG_RETURN(allowed_top_level); } @@ -20935,7 +20936,7 @@ void JOIN::calc_allowed_top_level_tables(SELECT_LEX *lex) current plan */ -table_map JOIN::get_allowed_nj_tables(uint idx) +table_map JOIN::get_allowed_nj_tables(uint idx) const { TABLE_LIST *last_emb; if (idx > const_tables && @@ -20944,14 +20945,14 @@ table_map JOIN::get_allowed_nj_tables(uint idx) for (;last_emb && last_emb != emb_sjm_nest; last_emb= last_emb->embedding) { - if (!last_emb->sj_on_expr) + if (last_emb->sj_on_expr) + continue; + + NESTED_JOIN *nest= last_emb->nested_join; + if (!nest->is_fully_covered()) { - NESTED_JOIN *nest= last_emb->nested_join; - if (!nest->is_fully_covered()) - { - // Return tables that are direct members of this join nest - return nest->direct_children_map; - } + // Return tables that are direct members of this join nest + return nest->direct_children_map; } } } diff --git a/sql/sql_select.h b/sql/sql_select.h index 190302311dc44..f05d12f6cb558 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -251,8 +251,23 @@ struct SplM_plan_info; class SplM_opt_info; typedef struct st_join_table { - TABLE *table; - TABLE_LIST *tab_list; + TABLE *table; /**< pointer to table cursor */ + TABLE_LIST *tab_list; /**< pointer to query table, e.g. `t1` */ + + TABLE_LIST *get_tab_list() + { + DBUG_ASSERT(!tab_list || !table || tab_list == table->pos_in_table_list); + DBUG_ASSERT(!tab_list || !table || tab_list->table == table); + return tab_list; + } + + const TABLE_LIST *get_tab_list() const + { + DBUG_ASSERT(!tab_list || !table || tab_list == table->pos_in_table_list); + DBUG_ASSERT(!tab_list || !table || tab_list->table == table); + return tab_list; + } + KEYUSE *keyuse; /**< pointer to first used key */ KEY *hj_key; /**< descriptor of the used best hash join key not supported by any index */ @@ -1946,8 +1961,8 @@ class JOIN :public Sql_alloc bool transform_in_predicates_into_in_subq(THD *thd); bool optimize_upper_rownum_func(); - void calc_allowed_top_level_tables(SELECT_LEX *lex); - table_map get_allowed_nj_tables(uint idx); + table_map calc_allowed_top_level_tables(SELECT_LEX *lex) const; + table_map get_allowed_nj_tables(uint idx) const; bool propagate_dependencies(JOIN_TAB *stat); void update_key_dependencies(); table_map *export_table_dependencies() const; diff --git a/sql/table.h b/sql/table.h index bb4975863257d..ed029e4e04753 100644 --- a/sql/table.h +++ b/sql/table.h @@ -3462,6 +3462,15 @@ typedef struct st_nested_join */ uint n_tables; nested_join_map nj_map; /* Bit used to identify this nested join*/ + void set_nj_map(uint offset) + { + nj_map= static_cast(1 << offset); + } + nested_join_map get_nj_map() const + { + return nj_map; + } + /* (Valid only for semi-join nests) Bitmap of tables outside the semi-join that are used within the semi-join's ON condition. From 2d8ad76d37799161323fb25aac099917cceb055d Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Thu, 11 Dec 2025 16:36:21 -0500 Subject: [PATCH 2/2] MDEV-38177: FULL OUTER JOIN Allowed JOIN Orders Check that tables participating in a FULL OUTER JOIN are not separated from each other when trying new extenions to the query plan. Some tables t1, t2 participating in FULL OUTER JOIN should remain adjacent in the final JOIN order, otherwise wrong results will be computed. In this patch, tables in a FULL OUTER JOIN remember who their JOIN partner is. When JOINs are nested, these partners are preserved and checked when walking the nested JOIN tree during check_interleaving_in_nj. This code accounts for the case when tables in a FULL OUTER JOIN may themselves be nested joins, such as (t1, t2) FULL OUTER JOIN t3. --- mysql-test/main/join.result | 66 ++++++++++++++++++++++++++++++++++++- mysql-test/main/join.test | 20 ++++++++++- sql/sql_parse.cc | 2 ++ sql/sql_select.cc | 60 +++++++++++++++++++++++++++++++++ sql/sql_select.h | 11 +++++-- sql/sql_yacc.yy | 6 ++++ sql/table.h | 14 +++++++- 7 files changed, 174 insertions(+), 5 deletions(-) diff --git a/mysql-test/main/join.result b/mysql-test/main/join.result index e1ae218b4cc41..b210903f229c2 100644 --- a/mysql-test/main/join.result +++ b/mysql-test/main/join.result @@ -3969,6 +3969,8 @@ create table two (v int); insert into two (v) values (2); create table three (v int); insert into three (v) values (3); +create table four (v int); +insert into three (v) values (4); # (FULL)FULL to (INNER)INNER JOIN select * from one full join two on one.v = two.v full join three on two.v = three.v where one.v is not null and two.v is not null and three.v is not null; v v v @@ -4006,9 +4008,11 @@ NULL 2 NULL select * from one left join two on one.v = two.v full join three on two.v = three.v where three.v is not null; v v v NULL NULL 3 +NULL NULL 4 select * from one left join two on one.v = two.v right join three on two.v = three.v; v v v NULL NULL 3 +NULL NULL 4 # (LEFT)FULL to (LEFT)LEFT JOIN insert into one (v) values (2),(3); insert into two (v) values (1); @@ -4032,5 +4036,65 @@ v v v select * from three left join one on one.v = 1 left join two on two.v = 1; v v v 1 1 1 -drop table one, two, three; +# Interleaved JOIN checks +explain extended select * from one full outer join (two, three) on one.v=two.v; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE one UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE two UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three UNKNOWN NULL NULL NULL NULL 0 0.00 +Warnings: +Note 1003 select `test`.`one`.`v` AS `v`,`test`.`two`.`v` AS `v`,`test`.`three`.`v` AS `v` from `test`.`one` full join (`test`.`two` join `test`.`three`) on(multiple equal(`test`.`one`.`v`, `test`.`two`.`v`)) +# ^^^ join order must be one, two, three +explain extended select * from (one, two) full outer join three on one.v=two.v; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE one UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE two UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three UNKNOWN NULL NULL NULL NULL 0 0.00 +Warnings: +Note 1003 select `test`.`one`.`v` AS `v`,`test`.`two`.`v` AS `v`,`test`.`three`.`v` AS `v` from `test`.`one` join `test`.`two` full join `test`.`three` on(multiple equal(`test`.`one`.`v`, `test`.`two`.`v`)) +# ^^^ join order must be one, two, three +explain extended select * from one full outer join (three, two) on one.v=two.v; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE one UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE two UNKNOWN NULL NULL NULL NULL 0 0.00 +Warnings: +Note 1003 select `test`.`one`.`v` AS `v`,`test`.`three`.`v` AS `v`,`test`.`two`.`v` AS `v` from `test`.`one` full join (`test`.`three` join `test`.`two`) on(multiple equal(`test`.`one`.`v`, `test`.`two`.`v`)) +# ^^^ join order must be one, three, two +explain extended select * from (one, two t) full outer join (two v, three) on true; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE one UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE t UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE v UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three UNKNOWN NULL NULL NULL NULL 0 0.00 +Warnings: +Note 1003 select `test`.`one`.`v` AS `v`,`test`.`t`.`v` AS `v`,`test`.`v`.`v` AS `v`,`test`.`three`.`v` AS `v` from `test`.`one` join `test`.`two` `t` full join (`test`.`two` `v` join `test`.`three`) on(1) +# ^^^ join order must be one, t, v, three +explain extended select * from (one full outer join two on true) full outer join (three full outer join four on true) on true; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE one UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE two UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE four UNKNOWN NULL NULL NULL NULL 0 0.00 +Warnings: +Note 1003 select `test`.`one`.`v` AS `v`,`test`.`two`.`v` AS `v`,`test`.`three`.`v` AS `v`,`test`.`four`.`v` AS `v` from `test`.`one` full join `test`.`two` on(1) full join (`test`.`three` full join `test`.`four` on(1)) on(1) +# ^^^ join order must be one, two, three, four +explain extended select * from (one full outer join two on true) inner join (three full outer join four on true); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE one UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE two UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three system NULL NULL NULL NULL 1 100.00 +1 SIMPLE four system NULL NULL NULL NULL 0 0.00 Const row not found +Warnings: +Note 1003 select `test`.`one`.`v` AS `v`,`test`.`two`.`v` AS `v`,1 AS `v`,NULL AS `v` from `test`.`one` full join `test`.`two` on(1) +# ^^^ join order must be one, two, three, four +explain extended select * from four full outer join (two full outer join three on true) on true; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE four system NULL NULL NULL NULL 0 0.00 Const row not found +1 SIMPLE two UNKNOWN NULL NULL NULL NULL 0 0.00 +1 SIMPLE three UNKNOWN NULL NULL NULL NULL 0 0.00 +Warnings: +Note 1003 select NULL AS `v`,`test`.`two`.`v` AS `v`,`test`.`three`.`v` AS `v` from (`test`.`two` full join `test`.`three` on(1)) +# ^^^ join order must be four, two, three +drop table one, two, three, four; # End of 12.3 tests diff --git a/mysql-test/main/join.test b/mysql-test/main/join.test index e2a668d048a9f..60f61f6dbb9c7 100644 --- a/mysql-test/main/join.test +++ b/mysql-test/main/join.test @@ -2187,6 +2187,8 @@ create table two (v int); insert into two (v) values (2); create table three (v int); insert into three (v) values (3); +create table four (v int); +insert into three (v) values (4); --echo # (FULL)FULL to (INNER)INNER JOIN select * from one full join two on one.v = two.v full join three on two.v = three.v where one.v is not null and two.v is not null and three.v is not null; @@ -2223,7 +2225,23 @@ select * from three; select * from one left join two on one.v = two.v full join three on two.v = three.v where three.v = 1; select * from three left join one on one.v = 1 left join two on two.v = 1; -drop table one, two, three; +--echo # Interleaved JOIN checks +explain extended select * from one full outer join (two, three) on one.v=two.v; +--echo # ^^^ join order must be one, two, three +explain extended select * from (one, two) full outer join three on one.v=two.v; +--echo # ^^^ join order must be one, two, three +explain extended select * from one full outer join (three, two) on one.v=two.v; +--echo # ^^^ join order must be one, three, two +explain extended select * from (one, two t) full outer join (two v, three) on true; +--echo # ^^^ join order must be one, t, v, three +explain extended select * from (one full outer join two on true) full outer join (three full outer join four on true) on true; +--echo # ^^^ join order must be one, two, three, four +explain extended select * from (one full outer join two on true) inner join (three full outer join four on true); +--echo # ^^^ join order must be one, two, three, four +explain extended select * from four full outer join (two full outer join three on true) on true; +--echo # ^^^ join order must be four, two, three + +drop table one, two, three, four; # TODO fix PS protocol before end of FULL OUTER JOIN development --enable_ps_protocol diff --git a/sql/sql_parse.cc b/sql/sql_parse.cc index be98f1aee1f16..1ca4262aabaf4 100644 --- a/sql/sql_parse.cc +++ b/sql/sql_parse.cc @@ -8346,6 +8346,8 @@ TABLE_LIST *st_select_lex::nest_last_join(THD *thd) if (prev_join_using) ptr->join_using_fields= prev_join_using; } + if (table->outer_join & JOIN_TYPE_FULL) + nested_join->is_foj= true; } nested_join->used_tables= nested_join->not_null_tables= (table_map) 0; DBUG_RETURN(ptr); diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 3898811cbbc71..28b89c72e7aa5 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -10279,6 +10279,7 @@ choose_plan(JOIN *join, table_map join_tables, TABLE_LIST *emb_sjm_nest) DBUG_ENTER("choose_plan"); join->limit_optimization_mode= false; + join->foj_tables= 0; join->extra_heuristic_pruning= false; join->prune_level= join->thd->variables.optimizer_prune_level; @@ -20725,6 +20726,26 @@ static bool check_interleaving_with_nj(JOIN_TAB *next_tab) { JOIN *join= next_tab->join; + /* next_tab must be a single table */ + DBUG_ASSERT(!next_tab || !next_tab->tab_list || + !next_tab->tab_list->nested_join); + + /* + If we've entered a FULL JOIN then we expect whatever table + is represented by next_tab to be on the other side of the + FULL JOIN. + */ + if (join->foj_tables) // we're in a FULL JOIN + { + /* + If the candidate next table is not a nested join, then + it's a single table and we can inspect its table map + directly to be sure that it is expected. + */ + if (!(join->foj_tables & next_tab->tab_list->get_map())) + return TRUE; // Error: attempted to interleave in the FULL JOIN. + } + if (join->cur_embedding_map & ~next_tab->embedding_map) { /* @@ -20742,6 +20763,9 @@ static bool check_interleaving_with_nj(JOIN_TAB *next_tab) next_emb && next_emb != join->emb_sjm_nest; next_emb= next_emb->embedding) { + /* Tables with embeddings must have a nested_join instance */ + DBUG_ASSERT(next_emb->nested_join); + if (next_emb->sj_on_expr) continue; @@ -20755,6 +20779,20 @@ static bool check_interleaving_with_nj(JOIN_TAB *next_tab) as X bracket might have Y pair bracket. */ join->cur_embedding_map |= next_emb->nested_join->get_nj_map(); + + /* + Remember which tables participate in the FULL OUTER JOIN. This allows + us to handle the nested FULL OUTER JOIN case too. + */ + if (next_emb->nested_join->is_foj) + { + const table_map partner_tables= + next_emb->foj_partner->nested_join ? + next_emb->foj_partner->nested_join->used_tables : + next_emb->foj_partner->get_map(); + join->foj_tables|= (next_emb->nested_join->used_tables | + partner_tables); + } } DBUG_ASSERT(next_emb->nested_join->n_tables >= @@ -20768,6 +20806,16 @@ static bool check_interleaving_with_nj(JOIN_TAB *next_tab) Mark that we've left it and continue walking up the brackets hierarchy. */ join->cur_embedding_map &= ~next_emb->nested_join->get_nj_map(); + + /* Don't forget to mask-out partner tables, too. */ + if (next_emb->nested_join->is_foj) + { + const table_map partner_tables= + next_emb->foj_partner->nested_join ? + next_emb->foj_partner->nested_join->used_tables : + next_emb->foj_partner->get_map(); + join->foj_tables&= ~(next_emb->nested_join->used_tables | partner_tables); + } } return FALSE; } @@ -20843,8 +20891,20 @@ static void restore_prev_nj_state(JOIN_TAB *last) join->cur_embedding_map|= nest->get_nj_map(); if (--nest->counter == 0) + { join->cur_embedding_map&= ~nest->get_nj_map(); + /* Mask-out the partner tables */ + if (last_emb->nested_join->is_foj) + { + const table_map partner_tables= + last_emb->foj_partner->nested_join ? + last_emb->foj_partner->nested_join->used_tables : + last_emb->foj_partner->get_map(); + join->foj_tables&= ~(nest->used_tables | partner_tables); + } + } + if (!was_fully_covered) break; } diff --git a/sql/sql_select.h b/sql/sql_select.h index f05d12f6cb558..0c038006d2cfa 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -532,8 +532,8 @@ typedef struct st_join_table { */ int keep_current_rowid; - /* NestedOuterJoins: Bitmap of nested joins this table is part of */ - nested_join_map embedding_map; + /* NestedOuterJoins: All nested joins in which this join table appears */ + nested_join_map embedding_map; // is a bitmap /* Tmp table info */ TMP_TABLE_PARAM *tmp_table_param; @@ -1299,6 +1299,13 @@ class JOIN :public Sql_alloc void restore_query_plan(Join_plan_state *restore_from); public: + /** + Used during check_interleaving_with_nj and restore_prev_nj_state, tracks + expected tables participating in a FULL OUTER JOIN (if applicable) to + ensure that they appear together in the final join order. + */ + table_map foj_tables{0}; + JOIN_TAB *join_tab, **best_ref; /* List of fields that aren't under an aggregate function */ diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 587f4f5640862..68c0d0d373c41 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -12375,10 +12375,12 @@ join_table: Select->add_joined_table($1); $1->outer_join|= (JOIN_TYPE_LEFT | JOIN_TYPE_FULL); + $1->foj_partner= $5; Select->add_joined_table($5); $5->outer_join|= (JOIN_TYPE_RIGHT | JOIN_TYPE_FULL); + $5->foj_partner= $1; /* Change the current name resolution context to a local context. */ if (unlikely(push_new_name_resolution_context(thd, $1, $5))) @@ -12400,10 +12402,12 @@ join_table: Select->add_joined_table($1); $1->outer_join|= (JOIN_TYPE_LEFT | JOIN_TYPE_FULL); + $1->foj_partner= $5; Select->add_joined_table($5); $5->outer_join|= (JOIN_TYPE_RIGHT | JOIN_TYPE_FULL); + $5->foj_partner= $1; } USING '(' using_list ')' { @@ -12418,11 +12422,13 @@ join_table: $1->outer_join|= (JOIN_TYPE_LEFT | JOIN_TYPE_FULL | JOIN_TYPE_NATURAL); + $1->foj_partner= $6; Select->add_joined_table($6); $6->outer_join|= (JOIN_TYPE_RIGHT | JOIN_TYPE_FULL | JOIN_TYPE_NATURAL); + $6->foj_partner= $1; add_join_natural($6,$1,NULL,Select); ++Lex->full_join_count; diff --git a/sql/table.h b/sql/table.h index ed029e4e04753..b3080a4a39075 100644 --- a/sql/table.h +++ b/sql/table.h @@ -3270,6 +3270,13 @@ struct TABLE_LIST tabledef_version.str= (const uchar *) version->str; tabledef_version.length= version->length; } + + /* + If not nullptr, then foj_partner points to the other + table in a FULL OUTER JOIN. For example, + SELECT ... FROM *this FULL OUTER JOIN foj_partner ... + */ + TABLE_LIST *foj_partner{nullptr}; private: bool prep_check_option(THD *thd, uint8 check_opt_type); bool prep_where(THD *thd, Item **conds, bool no_where_clause); @@ -3464,7 +3471,7 @@ typedef struct st_nested_join nested_join_map nj_map; /* Bit used to identify this nested join*/ void set_nj_map(uint offset) { - nj_map= static_cast(1 << offset); + nj_map= static_cast(1ULL << offset); } nested_join_map get_nj_map() const { @@ -3493,6 +3500,11 @@ typedef struct st_nested_join 2. All child join nest nodes are fully covered. */ bool is_fully_covered() const { return n_tables == counter; } + + /** + True if this join nest represents a FULL OUTER JOIN. + */ + bool is_foj{false}; } NESTED_JOIN;