Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions mysql-test/main/table_elim.result
Original file line number Diff line number Diff line change
Expand Up @@ -1086,5 +1086,53 @@ id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 ALL NULL NULL NULL NULL 2
DROP TABLE t1, t2;
#
# MDEV-36610: Subquery wrongly marked as eliminated by table elimination
# when equality propagation shares a WHERE-clause subquery
# into an eliminated outer join's ON expression
#
CREATE TABLE t0 (k INT);
INSERT INTO t0 VALUES (20),(9);
CREATE TABLE t1 (a BIGINT);
INSERT INTO t1 VALUES (1),(2);
CREATE TABLE t2 (b INT PRIMARY KEY);
INSERT INTO t2 VALUES (3),(4);
INSERT INTO t2 VALUES (5),(6);
CREATE TABLE t3 (c INT);
INSERT INTO t3 VALUES (2);
CREATE TABLE t4 (a BIGINT, x INT);
INSERT INTO t4 VALUES (2,20),(3,30);
# t2 is eliminated, but the subquery must still be shown and executed:
explain SELECT t1.* FROM t1 LEFT JOIN t2 ON (t2.b = t1.a)
WHERE t1.a = (SELECT c FROM t3);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 ALL NULL NULL NULL NULL 2 Using where
2 SUBQUERY t3 system NULL NULL NULL NULL 1
SELECT t1.* FROM t1 LEFT JOIN t2 ON (t2.b = t1.a)
WHERE t1.a = (SELECT c FROM t3);
a
2
#
# Same problem, but the subquery is written in the ON expression of a
# surviving outer join and leaks into the ON of an eliminated inner one.
# Here only the surviving-ON sweep can clear the "eliminated" flag.
# The BIGINT vs INT mismatch keeps the subquery out of the (walk-skipped)
# multiple-equality constant, so it is reached as a plain equality arg.
#
# t2 is eliminated; the subquery must still be shown and executed:
explain SELECT t0.k, t4.x FROM t0
LEFT JOIN (t4 LEFT JOIN t2 ON t2.b = t4.a)
ON (t0.k = t4.x AND t4.a = (SELECT c FROM t3));
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t0 ALL NULL NULL NULL NULL 2
1 PRIMARY t4 ALL NULL NULL NULL NULL 2 Using where; Using join buffer (flat, BNL join)
2 SUBQUERY t3 system NULL NULL NULL NULL 1
SELECT t0.k, t4.x FROM t0
LEFT JOIN (t4 LEFT JOIN t2 ON t2.b = t4.a)
ON (t0.k = t4.x AND t4.a = (SELECT c FROM t3));
k x
20 20
9 NULL
DROP TABLE t0, t1, t2, t3, t4;
#
# End of 10.11 tests
#
43 changes: 43 additions & 0 deletions mysql-test/main/table_elim.test
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,49 @@ select t1.null_col from t1 left join t2 on (t2.unique_col<=>t1.notnull_col);

DROP TABLE t1, t2;

--echo #
--echo # MDEV-36610: Subquery wrongly marked as eliminated by table elimination
--echo # when equality propagation shares a WHERE-clause subquery
--echo # into an eliminated outer join's ON expression
--echo #
CREATE TABLE t0 (k INT);
INSERT INTO t0 VALUES (20),(9);
CREATE TABLE t1 (a BIGINT);
INSERT INTO t1 VALUES (1),(2);
CREATE TABLE t2 (b INT PRIMARY KEY);
INSERT INTO t2 VALUES (3),(4);
INSERT INTO t2 VALUES (5),(6);
CREATE TABLE t3 (c INT);
INSERT INTO t3 VALUES (2);
CREATE TABLE t4 (a BIGINT, x INT);
INSERT INTO t4 VALUES (2,20),(3,30);

--echo # t2 is eliminated, but the subquery must still be shown and executed:

let $q=
SELECT t1.* FROM t1 LEFT JOIN t2 ON (t2.b = t1.a)
WHERE t1.a = (SELECT c FROM t3);
eval explain $q;
eval $q;

--echo #
--echo # Same problem, but the subquery is written in the ON expression of a
--echo # surviving outer join and leaks into the ON of an eliminated inner one.
--echo # Here only the surviving-ON sweep can clear the "eliminated" flag.
--echo # The BIGINT vs INT mismatch keeps the subquery out of the (walk-skipped)
--echo # multiple-equality constant, so it is reached as a plain equality arg.
--echo #

--echo # t2 is eliminated; the subquery must still be shown and executed:
let $q=
SELECT t0.k, t4.x FROM t0
LEFT JOIN (t4 LEFT JOIN t2 ON t2.b = t4.a)
ON (t0.k = t4.x AND t4.a = (SELECT c FROM t3));
eval explain $q;
eval $q;

DROP TABLE t0, t1, t2, t3, t4;

--echo #
--echo # End of 10.11 tests
--echo #
1 change: 1 addition & 0 deletions sql/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,7 @@ class Item :public Value_source,

virtual bool enumerate_field_refs_processor(void *arg) { return 0; }
virtual bool mark_as_eliminated_processor(void *arg) { return 0; }
virtual bool unmark_as_eliminated_processor(void *arg) { return 0; }
virtual bool eliminate_subselect_processor(void *arg) { return 0; }
virtual bool view_used_tables_processor(void *arg) { return 0; }
virtual bool eval_not_null_tables(void *arg) { return 0; }
Expand Down
18 changes: 18 additions & 0 deletions sql/item_cmpfunc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7457,6 +7457,24 @@ bool Item_equal::walk(Item_processor processor, bool walk_subquery, void *arg)
}


/*
A multiple equality keeps its optional constant as the head of equal_items,
and the generic walk() (via Item_equal_fields_iterator) does not visit it.
So reach it explicitly here: otherwise a subquery that equality propagation
turned into the constant of a surviving multiple equality would stay wrongly
flagged as eliminated by table elimination. See
Item_subselect::unmark_as_eliminated_processor().
*/

bool Item_equal::unmark_as_eliminated_processor(void *arg)
{
Item *c= get_const();
if (c)
c->walk(&Item::unmark_as_eliminated_processor, FALSE, arg);
return FALSE;
}


Item *Item_equal::transform(THD *thd, Item_transformer transformer, uchar *arg)
{
DBUG_ASSERT(!thd->stmt_arena->is_stmt_prepare());
Expand Down
1 change: 1 addition & 0 deletions sql/item_cmpfunc.h
Original file line number Diff line number Diff line change
Expand Up @@ -3519,6 +3519,7 @@ class Item_equal: public Item_bool_func
SARGABLE_PARAM **sargables) override;
SEL_TREE *get_mm_tree(RANGE_OPT_PARAM *param, Item **cond_ptr) override;
bool walk(Item_processor processor, bool walk_subquery, void *arg) override;
bool unmark_as_eliminated_processor(void *arg) override;
Item *transform(THD *thd, Item_transformer transformer, uchar *arg) override;
void print(String *str, enum_query_type query_type) override;
const Type_handler *compare_type_handler() const { return m_compare_handler; }
Expand Down
20 changes: 20 additions & 0 deletions sql/item_subselect.cc
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,33 @@ bool Item_subselect::enumerate_field_refs_processor(void *arg)
return FALSE;
}


/*
Set/Clear the "eliminated" flag set by mark_as_eliminated_processor().

Table elimination marks every subquery reachable from an eliminated outer
join's ON expression as eliminated. However, equality propagation
(build_equal_items()) can inject a reference to a subquery that lives in
another part of the query (e.g. the WHERE clause) into that ON expression.
Such a subquery still has to be executed, so after elimination we walk the
surviving expressions and clear the flag on any subquery still referenced
from them.
*/

bool Item_subselect::mark_as_eliminated_processor(void *arg)
{
eliminated= TRUE;
return FALSE;
}


bool Item_subselect::unmark_as_eliminated_processor(void *arg)
{
eliminated= FALSE;
return FALSE;
}


/**
Remove a subselect item from its unit so that the unit no longer
represents a subquery.
Expand Down
1 change: 1 addition & 0 deletions sql/item_subselect.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class Item_subselect :public Item_result_field,
bool walk(Item_processor processor, bool walk_subquery, void *arg) override;
bool unknown_splocal_processor(void *arg) override;
bool mark_as_eliminated_processor(void *arg) override;
bool unmark_as_eliminated_processor(void *arg) override;
bool eliminate_subselect_processor(void *arg) override;
bool enumerate_field_refs_processor(void *arg) override;
bool check_vcol_func_processor(void *arg) override
Expand Down
63 changes: 63 additions & 0 deletions sql/opt_table_elimination.cc
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,44 @@ void add_module_expr(Dep_analysis_context *dac, Dep_module_expr **eq_mod,

/*****************************************************************************/

/*
Clear the "eliminated" flag on every subquery reachable from the given
expression (see Item_subselect::unmark_as_eliminated_processor).
*/

static inline void unmark_eliminated_subqueries(Item *expr)
{
if (expr)
expr->walk(&Item::unmark_as_eliminated_processor, FALSE, NULL);
}


/*
Clear the "eliminated" flag on subqueries reachable from ON expressions of
outer joins that were NOT eliminated. Such an ON expression is still
evaluated at runtime.
*/

static void unmark_live_on_expr_subqueries(JOIN *join,
List<TABLE_LIST> *join_list)
{
TABLE_LIST *tbl;
List_iterator<TABLE_LIST> it(*join_list);
while ((tbl= it++))
{
if (tbl->nested_join)
{
unmark_live_on_expr_subqueries(join, &tbl->nested_join->join_list);
/* Only sweep this nest's ON expr if some of its tables survived. */
if (tbl->nested_join->used_tables & ~join->eliminated_tables)
unmark_eliminated_subqueries(tbl->on_expr);
}
else if (tbl->table && !(tbl->table->map & join->eliminated_tables))
unmark_eliminated_subqueries(tbl->on_expr);
}
}


/*
Perform table elimination

Expand Down Expand Up @@ -769,6 +807,31 @@ void eliminate_tables(JOIN *join)
eliminate_tables_for_list(join, join->join_list, all_tables, NULL,
used_tables, &trace_eliminated_tables);
}

/*
Equality propagation (build_equal_items()) can inject into the ON
expression of an eliminated outer join, a reference to a subquery that
actually lives in another (surviving) part of the query.
mark_as_eliminated() then flags that subquery as eliminated even though
it still has to be executed.
Clear the flag on every subquery that is still reachable.
*/
if (join->eliminated_tables)
{
unmark_eliminated_subqueries(join->conds);
unmark_eliminated_subqueries(join->having);

List_iterator<Item> live_it(join->fields_list);
while ((item= live_it++))
unmark_eliminated_subqueries(item);

for (ORDER *ord= join->order; ord; ord= ord->next)
unmark_eliminated_subqueries(*(ord->item));
for (ORDER *ord= join->group_list; ord; ord= ord->next)
unmark_eliminated_subqueries(*(ord->item));

unmark_live_on_expr_subqueries(join, join->join_list);
}
DBUG_VOID_RETURN;
}

Expand Down