From a79ce0c550bc3cdb5b319bf34170b9021f7cc3ed Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Wed, 6 May 2026 10:31:26 -0400 Subject: [PATCH] MDEV-37020: Derived table merge optimization does not work for delete and update A derived table in a multitable DELETE or UPDATE was materialized while a derived table in an equivalent query using a VIEW was merged. The cause was a blanket guard in TABLE_LIST::init_derived added by commit fe89df42686f. That commit fixed a ROWNUM crash on VIEWs with ORDER BY, but its derived table guard was wider than it needed to be. Narrow that guard to the case when the derived table lives inside a VIEW's body (the case when belong_to_view is set). A derived table at the top level of a multitable query will be merged, while a derived nested table within a VIEW will be materialized. Narrowing that guard exposes a separate latent bug. The access check in multi_update_check_table_access has a branch for VIEWs and another for 'not VIEWs' which dereferences table->table->map. A merged derived table that is not a VIEW fits neither condition. As is the case in main.lock_multi_bug38499, when concurrent ALTER on the target forces the prepared statement to be prepared again, table->table on the merged derived table might be NULL and this leads to a crash. Privileges for the underlying base tables are already checked by multi_update_precheck, so multi_update_check_table_access now returns early in its else branch when the input is a merged derived table. --- mysql-test/main/derived_view.result | 94 +++++++++++++++++++ mysql-test/main/derived_view.test | 51 ++++++++++ mysql-test/main/multi_update.result | 3 - .../main/myisam_explain_non_select_all.result | 50 +++++----- sql/sql_update.cc | 13 +++ sql/table.cc | 8 +- 6 files changed, 186 insertions(+), 33 deletions(-) diff --git a/mysql-test/main/derived_view.result b/mysql-test/main/derived_view.result index 3f3f68154882c..747cfbaf55632 100644 --- a/mysql-test/main/derived_view.result +++ b/mysql-test/main/derived_view.result @@ -4430,3 +4430,97 @@ deallocate prepare stmt; drop view v1,v2; drop table t1,t2; # End of 10.6 tests +# +# Beginning of 10.11 tests +# +# +# MDEV-37020: derived table not merged in multi-table DELETE/UPDATE +# while equivalent SELECT and view-based form do merge +# +create table t1 (a int, b int); +insert into t1 select seq, seq from seq_1_to_10; +create table t2 (a int, b int); +insert into t2 select seq, seq from seq_1_to_10; +create table t3 (a int, b int); +insert into t3 select seq, seq from seq_1_to_10; +# Baseline: equivalent SELECT merges the derived table. +explain select * from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a<5) dt +where t1.b+1=1+dt.b1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 10 +1 SIMPLE t2 ALL NULL NULL NULL NULL 10 Using where; Using join buffer (flat, BNL join) +1 SIMPLE t3 ALL NULL NULL NULL NULL 10 Using where; Using join buffer (incremental, BNL join) +# Multitable DELETE must merge the derived table. +explain delete t1.* from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a) dt +where t1.b+1=1+dt.b1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 10 +1 SIMPLE t2 ALL NULL NULL NULL NULL 10 Using where +1 SIMPLE t3 ALL NULL NULL NULL NULL 10 Using where +# Multitable UPDATE must merge the derived table. +explain update t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a) dt +set t1.a=t1.a+1 where t1.b+1=1+dt.b1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 10 +1 SIMPLE t2 ALL NULL NULL NULL NULL 10 Using where +1 SIMPLE t3 ALL NULL NULL NULL NULL 10 Using where +# VIEW form already merged the derived table before this fix, verify. +create view v1 as select t2.b as b1 from t2, t3 where t3.a=t2.a; +select t1.* from t1, v1 where t1.b+1=1+v1.b1; +a b +1 1 +2 2 +3 3 +4 4 +5 5 +6 6 +7 7 +8 8 +9 9 +10 10 +explain select t1.* from t1, v1 where t1.b+1=1+v1.b1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 10 +1 SIMPLE t2 ALL NULL NULL NULL NULL 10 Using where; Using join buffer (flat, BNL join) +1 SIMPLE t3 ALL NULL NULL NULL NULL 10 Using where; Using join buffer (incremental, BNL join) +explain delete t1.* from t1, v1 where t1.b+1=1+v1.b1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 10 +1 SIMPLE t2 ALL NULL NULL NULL NULL 10 Using where +1 SIMPLE t3 ALL NULL NULL NULL NULL 10 Using where +explain update t1, v1 set t1.a=t1.a+1 where t1.b+1=1+v1.b1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 10 +1 SIMPLE t2 ALL NULL NULL NULL NULL 10 Using where +1 SIMPLE t3 ALL NULL NULL NULL NULL 10 Using where +# Confirm queries still produce the correct results. +select t1.* from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a<5) dt +where t1.b+1=1+dt.b1; +a b +1 1 +2 2 +3 3 +4 4 +delete t1.* from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a<5) dt2 +where t1.b=dt2.b1; +select * from t1 order by a; +a b +5 5 +6 6 +7 7 +8 8 +9 9 +10 10 +update t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a>7) dt2 +set t1.a=t1.a*100 where t1.b=dt2.b1; +select * from t1 order by b; +a b +5 5 +6 6 +7 7 +800 8 +900 9 +1000 10 +drop view v1; +drop table t1, t2, t3; +# End of 10.11 tests diff --git a/mysql-test/main/derived_view.test b/mysql-test/main/derived_view.test index 4c02d0fa906b4..5d4a3681ee229 100644 --- a/mysql-test/main/derived_view.test +++ b/mysql-test/main/derived_view.test @@ -2940,3 +2940,54 @@ drop view v1,v2; drop table t1,t2; --echo # End of 10.6 tests + + +--echo # +--echo # Beginning of 10.11 tests +--echo # + +--echo # +--echo # MDEV-37020: derived table not merged in multi-table DELETE/UPDATE +--echo # while equivalent SELECT and view-based form do merge +--echo # + +create table t1 (a int, b int); +insert into t1 select seq, seq from seq_1_to_10; +create table t2 (a int, b int); +insert into t2 select seq, seq from seq_1_to_10; +create table t3 (a int, b int); +insert into t3 select seq, seq from seq_1_to_10; + +--echo # Baseline: equivalent SELECT merges the derived table. +explain select * from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a<5) dt + where t1.b+1=1+dt.b1; + +--echo # Multitable DELETE must merge the derived table. +explain delete t1.* from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a) dt + where t1.b+1=1+dt.b1; + +--echo # Multitable UPDATE must merge the derived table. +explain update t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a) dt + set t1.a=t1.a+1 where t1.b+1=1+dt.b1; + +--echo # VIEW form already merged the derived table before this fix, verify. +create view v1 as select t2.b as b1 from t2, t3 where t3.a=t2.a; +select t1.* from t1, v1 where t1.b+1=1+v1.b1; +explain select t1.* from t1, v1 where t1.b+1=1+v1.b1; +explain delete t1.* from t1, v1 where t1.b+1=1+v1.b1; +explain update t1, v1 set t1.a=t1.a+1 where t1.b+1=1+v1.b1; + +--echo # Confirm queries still produce the correct results. +select t1.* from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a<5) dt + where t1.b+1=1+dt.b1; +delete t1.* from t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a<5) dt2 + where t1.b=dt2.b1; +select * from t1 order by a; +update t1, (select t2.b as b1 from t2, t3 where t3.a=t2.a and t2.a>7) dt2 + set t1.a=t1.a*100 where t1.b=dt2.b1; +select * from t1 order by b; + +drop view v1; +drop table t1, t2, t3; + +--echo # End of 10.11 tests diff --git a/mysql-test/main/multi_update.result b/mysql-test/main/multi_update.result index d94adfca24290..7cd17580ac44e 100644 --- a/mysql-test/main/multi_update.result +++ b/mysql-test/main/multi_update.result @@ -585,9 +585,6 @@ INSERT INTO t3 VALUES (1), (2); UPDATE IGNORE ( SELECT ( SELECT COUNT(*) FROM t1 GROUP BY a, @v ) a FROM t2 ) x, t3 SET t3.a = 0; -Warnings: -Warning 1242 Subquery returns more than 1 row -Warning 1242 Subquery returns more than 1 row DROP TABLE t1, t2, t3; SET SESSION sql_safe_updates = DEFAULT; # diff --git a/mysql-test/main/myisam_explain_non_select_all.result b/mysql-test/main/myisam_explain_non_select_all.result index bc6152b8f0aa0..cd8395b6ce3c3 100644 --- a/mysql-test/main/myisam_explain_non_select_all.result +++ b/mysql-test/main/myisam_explain_non_select_all.result @@ -198,18 +198,16 @@ Warnings: Warning 1287 ' INTO FROM...' instead EXPLAIN UPDATE t1 t11, (SELECT * FROM t2) t12 SET t11.a = 10 WHERE t11.a = 1; id select_type table type possible_keys key key_len ref rows Extra -1 PRIMARY t11 ALL NULL NULL NULL NULL 3 Using where -1 PRIMARY ALL NULL NULL NULL NULL 3 -2 DERIVED t2 ALL NULL NULL NULL NULL 3 +1 SIMPLE t11 ALL NULL NULL NULL NULL 3 Using where +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 FLUSH STATUS; FLUSH TABLES; EXPLAIN EXTENDED UPDATE t1 t11, (SELECT * FROM t2) t12 SET t11.a = 10 WHERE t11.a = 1; id select_type table type possible_keys key key_len ref rows filtered Extra -1 PRIMARY t11 ALL NULL NULL NULL NULL 3 100.00 Using where -1 PRIMARY ALL NULL NULL NULL NULL 3 100.00 -2 DERIVED t2 ALL NULL NULL NULL NULL 3 100.00 +1 SIMPLE t11 ALL NULL NULL NULL NULL 3 100.00 Using where +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 100.00 Warnings: -Note 1003 /* select#1 */ update `test`.`t1` `t11` join (/* select#2 */ select `test`.`t2`.`b` AS `b` from `test`.`t2`) `t12` set `test`.`t11`.`a` = 10 where `test`.`t11`.`a` = 1 +Note 1003 update `test`.`t1` `t11` join `test`.`t2` set `test`.`t11`.`a` = 10 where `test`.`t11`.`a` = 1 # Status of EXPLAIN EXTENDED query Variable_name Value Handler_read_key 4 @@ -233,7 +231,7 @@ Handler_read_rnd_next 8 # Status of testing query execution: Variable_name Value Handler_read_key 4 -Handler_read_rnd_next 12 +Handler_read_rnd_next 8 Handler_update 1 DROP TABLE t1, t2; @@ -410,18 +408,16 @@ Warnings: Warning 1287 ' INTO FROM...' instead EXPLAIN UPDATE t1 t11, (SELECT * FROM t2) t12 SET t11.a = t11.a + 10; id select_type table type possible_keys key key_len ref rows Extra -1 PRIMARY t11 ALL NULL NULL NULL NULL 3 -1 PRIMARY ALL NULL NULL NULL NULL 3 -2 DERIVED t2 ALL NULL NULL NULL NULL 3 +1 SIMPLE t11 ALL NULL NULL NULL NULL 3 +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 FLUSH STATUS; FLUSH TABLES; EXPLAIN EXTENDED UPDATE t1 t11, (SELECT * FROM t2) t12 SET t11.a = t11.a + 10; id select_type table type possible_keys key key_len ref rows filtered Extra -1 PRIMARY t11 ALL NULL NULL NULL NULL 3 100.00 -1 PRIMARY ALL NULL NULL NULL NULL 3 100.00 -2 DERIVED t2 ALL NULL NULL NULL NULL 3 100.00 +1 SIMPLE t11 ALL NULL NULL NULL NULL 3 100.00 +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 100.00 Warnings: -Note 1003 /* select#1 */ update `test`.`t1` `t11` join (/* select#2 */ select `test`.`t2`.`b` AS `b` from `test`.`t2`) `t12` set `test`.`t11`.`a` = `test`.`t11`.`a` + 10 +Note 1003 update `test`.`t1` `t11` join `test`.`t2` set `test`.`t11`.`a` = `test`.`t11`.`a` + 10 # Status of EXPLAIN EXTENDED query Variable_name Value Handler_read_key 4 @@ -447,7 +443,7 @@ Variable_name Value Handler_read_key 4 Handler_read_rnd 3 Handler_read_rnd_deleted 1 -Handler_read_rnd_next 24 +Handler_read_rnd_next 20 Handler_update 3 DROP TABLE t1, t2; @@ -520,18 +516,16 @@ Warnings: Warning 1287 ' INTO FROM...' instead EXPLAIN UPDATE t1 t11, (SELECT * FROM t2) t12 SET t11.a = 10 WHERE t11.a > 1; id select_type table type possible_keys key key_len ref rows Extra -1 PRIMARY t11 ALL NULL NULL NULL NULL 3 Using where -1 PRIMARY ALL NULL NULL NULL NULL 3 -2 DERIVED t2 ALL NULL NULL NULL NULL 3 +1 SIMPLE t11 ALL NULL NULL NULL NULL 3 Using where +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 FLUSH STATUS; FLUSH TABLES; EXPLAIN EXTENDED UPDATE t1 t11, (SELECT * FROM t2) t12 SET t11.a = 10 WHERE t11.a > 1; id select_type table type possible_keys key key_len ref rows filtered Extra -1 PRIMARY t11 ALL NULL NULL NULL NULL 3 100.00 Using where -1 PRIMARY ALL NULL NULL NULL NULL 3 100.00 -2 DERIVED t2 ALL NULL NULL NULL NULL 3 100.00 +1 SIMPLE t11 ALL NULL NULL NULL NULL 3 100.00 Using where +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 100.00 Warnings: -Note 1003 /* select#1 */ update `test`.`t1` `t11` join (/* select#2 */ select `test`.`t2`.`b` AS `b` from `test`.`t2`) `t12` set `test`.`t11`.`a` = 10 where `test`.`t11`.`a` > 1 +Note 1003 update `test`.`t1` `t11` join `test`.`t2` set `test`.`t11`.`a` = 10 where `test`.`t11`.`a` > 1 # Status of EXPLAIN EXTENDED query Variable_name Value Handler_read_key 4 @@ -555,7 +549,7 @@ Handler_read_rnd_next 8 # Status of testing query execution: Variable_name Value Handler_read_key 4 -Handler_read_rnd_next 16 +Handler_read_rnd_next 12 Handler_update 2 DROP TABLE t1, t2; @@ -3409,20 +3403,18 @@ EXPLAIN UPDATE t1, (SELECT * FROM t2) y SET a = 10 WHERE a IN (SELECT * F id select_type table type possible_keys key key_len ref rows Extra 1 PRIMARY t1 ALL NULL NULL NULL NULL 3 Using where 1 PRIMARY ref key0 key0 5 test.t1.a 2 FirstMatch(t1) -1 PRIMARY ALL NULL NULL NULL NULL 3 +1 PRIMARY t2 ALL NULL NULL NULL NULL 3 4 DERIVED t2 ALL NULL NULL NULL NULL 3 Using filesort -2 DERIVED t2 ALL NULL NULL NULL NULL 3 FLUSH STATUS; FLUSH TABLES; EXPLAIN EXTENDED UPDATE t1, (SELECT * FROM t2) y SET a = 10 WHERE a IN (SELECT * FROM (SELECT b FROM t2 ORDER BY b LIMIT 2,2) x); id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY t1 ALL NULL NULL NULL NULL 3 100.00 Using where 1 PRIMARY ref key0 key0 5 test.t1.a 2 100.00 FirstMatch(t1) -1 PRIMARY ALL NULL NULL NULL NULL 3 100.00 +1 PRIMARY t2 ALL NULL NULL NULL NULL 3 100.00 4 DERIVED t2 ALL NULL NULL NULL NULL 3 100.00 Using filesort -2 DERIVED t2 ALL NULL NULL NULL NULL 3 100.00 Warnings: -Note 1003 /* select#1 */ update `test`.`t1` semi join ((/* select#4 */ select `test`.`t2`.`b` AS `b` from `test`.`t2` order by `test`.`t2`.`b` limit 2,2) `x`) join (/* select#2 */ select `test`.`t2`.`b` AS `b` from `test`.`t2`) `y` set `test`.`t1`.`a` = 10 where `x`.`b` = `test`.`t1`.`a` +Note 1003 /* select#1 */ update `test`.`t1` semi join ((/* select#4 */ select `test`.`t2`.`b` AS `b` from `test`.`t2` order by `test`.`t2`.`b` limit 2,2) `x`) join `test`.`t2` set `test`.`t1`.`a` = 10 where `x`.`b` = `test`.`t1`.`a` # Status of EXPLAIN EXTENDED query Variable_name Value Handler_read_key 4 diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 7a18cc640ee5c..cf2c383aa72e1 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -1733,6 +1733,19 @@ static bool multi_update_check_table_access(THD *thd, TABLE_LIST *table, } else { + /* + A merged derived table that is not a VIEW has no TABLE* so we + can't check privileges on it here. Besides, the privileges for + the underlying base tables are already checked by + multi_update_precheck. + + Without this guard, the main.lock_multi_bug38499 test will crash + just below when running a prepared statement that is made from an + UPDATE. + */ + if (table->is_merged_derived()) + return false; + /* Must be a base or derived table. */ const bool updated= table->table->map & tables_for_update; if (check_table_access(thd, updated ? UPDATE_ACL : SELECT_ACL, table, diff --git a/sql/table.cc b/sql/table.cc index 2b283fd93e1b8..9a6a0659df03c 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -9903,8 +9903,14 @@ bool TABLE_LIST::init_derived(THD *thd, bool init_view) (is_view() || optimizer_flag(thd, OPTIMIZER_SWITCH_DERIVED_MERGE)) && !thd->lex->can_not_use_merged() && + /* + Allow merged derived optimization for multitable DELETE and + UPDATE, but with one exception: if this derived table is + itself within a VIEW, then don't allow it to be merged. + */ !((thd->lex->sql_command == SQLCOM_UPDATE_MULTI || - thd->lex->sql_command == SQLCOM_DELETE_MULTI) && !is_view()) && + thd->lex->sql_command == SQLCOM_DELETE_MULTI) && + !is_view() && belong_to_view) && !is_recursive_with_table()) set_merged_derived(); else