From 4b6922a315fa5411665ac99c0b40fd7238093403 Mon Sep 17 00:00:00 2001 From: Yuchen Pei Date: Thu, 29 Aug 2024 11:10:59 +1000 Subject: [PATCH] MDEV-25008: UPDATE/DELETE: Cost-based choice IN->EXISTS vs Materialization Single-table UPDATE/DELETE didn't provide outer_lookup_keys value for subqueries. This didn't allow to make a meaningful choice between IN->EXISTS and Materialization strategies for subqueries. Fix this: * Make UPDATE/DELETE save Sql_cmd_dml::scanned_rows, * Then, subquery's JOIN::choose_subquery_plan() can fetch it from there for outer_lookup_keys Details: UPDATE/DELETE now calls select_lex->optimize_unflattened_subqueries() twice, like SELECT does (first call optimize_constant_subquries() in JOIN::optimize_inner(), then call optimize_unflattened_subqueries() in JOIN::optimize_stage2()): 1. Call with const_only=true before any optimizations. This allows range optimizer and others to use the values of cheap const subqueries. 2. Call it with const_only=false after range optimizer, partition pruning, etc. outer_lookup_keys value is provided, so it's possible to pick a good subquery strategy. Note: PROTECT_STATEMENT_MEMROOT requires that first SP execution performs subquery optimization for all subqueries, even for degenerate query plans like "Impossible WHERE". Due to that, we ensure that the call to optimize_unflattened_subqueries (with const_only=false) even for degenerate query plans still happens, as was the case before this change. --- mysql-test/main/delete.result | 33 ++++++++++++++++++++ mysql-test/main/delete.test | 37 ++++++++++++++++++++++ mysql-test/main/ps.result | 21 +++++++++++++ mysql-test/main/ps.test | 26 ++++++++++++++++ mysql-test/main/ps_mem_leaks.test | 34 ++++++++++++++++----- mysql-test/main/update.result | 33 ++++++++++++++++++++ mysql-test/main/update.test | 36 ++++++++++++++++++++++ sql/opt_range.cc | 4 +-- sql/opt_subselect.cc | 15 ++++----- sql/sql_base.cc | 2 +- sql/sql_cmd.h | 7 ++++- sql/sql_delete.cc | 50 ++++++++++++++++++++++++++++-- sql/sql_lex.h | 1 + sql/sql_select.cc | 1 + sql/sql_select.h | 9 ++++++ sql/sql_update.cc | 51 ++++++++++++++++++++++++++++--- 16 files changed, 334 insertions(+), 26 deletions(-) diff --git a/mysql-test/main/delete.result b/mysql-test/main/delete.result index 061c1961f13..192c390596c 100644 --- a/mysql-test/main/delete.result +++ b/mysql-test/main/delete.result @@ -647,3 +647,36 @@ c2 1 DROP TABLE t1, t2; End of 11.6 tests +# +# MDEV-25008: Delete query gets stuck on mariadb, same query works +# on MySQL 8.0.21 +# +CREATE TABLE t1 ( +id int NOT NULL PRIMARY KEY, +item_id varchar(100), +seller_name varchar(400), +variant varchar(400), +FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant) +)engine=innodb; +insert into t1 select seq,seq,seq,seq from seq_1_to_10000; +explain +DELETE FROM t1 WHERE id NOT IN +(SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 ALL NULL NULL NULL NULL # Using where +2 MATERIALIZED ALL NULL NULL NULL NULL # +3 DERIVED t1 ALL NULL NULL NULL NULL # Using temporary; Using filesort +drop table t1; +create table t1 (a int primary key, b int, c int, key(b)); +insert into t1 select seq, seq, seq from seq_1_to_20000; +create table t2 as select * from t1; +explain delete from t1 where b <= 2 and a not in (select b from t2); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 range b b 5 NULL 2 Using where +2 DEPENDENT SUBQUERY t2 ALL NULL NULL NULL NULL 20000 Using where +explain delete from t1 where b <= 3 and a not in (select b from t2); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 range b b 5 NULL 3 Using where +2 MATERIALIZED t2 ALL NULL NULL NULL NULL 20000 +drop table t1, t2; +# End of 11.7 tests diff --git a/mysql-test/main/delete.test b/mysql-test/main/delete.test index af55fe46ecb..530f041cac8 100644 --- a/mysql-test/main/delete.test +++ b/mysql-test/main/delete.test @@ -705,3 +705,40 @@ SELECT * FROM t2; DROP TABLE t1, t2; --echo End of 11.6 tests + +--echo # +--echo # MDEV-25008: Delete query gets stuck on mariadb, same query works +--echo # on MySQL 8.0.21 +--echo # + +--source include/have_sequence.inc +--source include/have_innodb.inc + +# original case +CREATE TABLE t1 ( + id int NOT NULL PRIMARY KEY, + item_id varchar(100), + seller_name varchar(400), + variant varchar(400), + FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant) +)engine=innodb; + +insert into t1 select seq,seq,seq,seq from seq_1_to_10000; + +# Masking the `rows` column as the value might vary a bit +--replace_column 9 # +explain +DELETE FROM t1 WHERE id NOT IN + (SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable); + +drop table t1; + +# example of not full table +create table t1 (a int primary key, b int, c int, key(b)); +insert into t1 select seq, seq, seq from seq_1_to_20000; +create table t2 as select * from t1; +explain delete from t1 where b <= 2 and a not in (select b from t2); +explain delete from t1 where b <= 3 and a not in (select b from t2); +drop table t1, t2; + +--echo # End of 11.7 tests diff --git a/mysql-test/main/ps.result b/mysql-test/main/ps.result index 525e51e1b89..f60359b3847 100644 --- a/mysql-test/main/ps.result +++ b/mysql-test/main/ps.result @@ -5968,4 +5968,25 @@ DROP VIEW t1; # # End of 10.4 tests # +# +# MDEV-25008 Delete query gets stuck on mariadb , same query works on MySQL 8.0.21 +# +create table t1 (a int, b int, primary key (a), key (b)); +insert into t1 values (1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8); +CREATE TABLE t2 ( id INT(10) ); +insert into t2 values (1),(2); +set @@optimizer_switch="index_merge=off"; +set sql_safe_updates=1; +set @var1=1; +set @var2=2; +prepare stmt from 'update t1 set b=(SELECT 1 FROM t2 WHERE id = t1.a) where 1=? or b=2'; +execute stmt using @var1; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +execute stmt using @var2; +delete from t1 where a=1 or b=2; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +drop table t1, t2; +# +# End of 11.7 tests +# ALTER DATABASE test CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci; diff --git a/mysql-test/main/ps.test b/mysql-test/main/ps.test index b3a790f0965..e095c5e278b 100644 --- a/mysql-test/main/ps.test +++ b/mysql-test/main/ps.test @@ -5451,4 +5451,30 @@ DROP VIEW t1; --echo # End of 10.4 tests --echo # +--echo # +--echo # MDEV-25008 Delete query gets stuck on mariadb , same query works on MySQL 8.0.21 +--echo # + +create table t1 (a int, b int, primary key (a), key (b)); +insert into t1 values (1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8); +CREATE TABLE t2 ( id INT(10) ); +insert into t2 values (1),(2); + +# calls select_lex->optimize_unflattened_subqueries(false) in the err label +set @@optimizer_switch="index_merge=off"; +set sql_safe_updates=1; +set @var1=1; +set @var2=2; +prepare stmt from 'update t1 set b=(SELECT 1 FROM t2 WHERE id = t1.a) where 1=? or b=2'; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +execute stmt using @var1; +execute stmt using @var2; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +delete from t1 where a=1 or b=2; +drop table t1, t2; + +--echo # +--echo # End of 11.7 tests +--echo # + --source include/test_db_charset_restore.inc diff --git a/mysql-test/main/ps_mem_leaks.test b/mysql-test/main/ps_mem_leaks.test index bedd4e2d939..a468d90b425 100644 --- a/mysql-test/main/ps_mem_leaks.test +++ b/mysql-test/main/ps_mem_leaks.test @@ -146,7 +146,9 @@ DROP PROCEDURE p1; --echo # --echo # MDEV-34757: assertion of (mem_root->flags & 4) == 0 fails in 2nd ps execution with partition pruning --echo # -# same as the first MDEV-34444 testcase but with explain +# same as the first MDEV-34447 testcase but with explain; calling +# select_lex->optimize_unflattened_subqueries(false) under the +# emit_explain_and_leave label CREATE TABLE t1 (id INT, value INT); CREATE TABLE t2 (id INT); @@ -165,7 +167,9 @@ SELECT * FROM t1; DEALLOCATE PREPARE stmt; DROP TABLE t1, t2; -# 2nd ps mem leak; partition pruning +# 2nd ps mem leak; partition pruning; calls +# select_lex->optimize_unflattened_subqueries(false) under if +# (prune_partitions())... set @var1=5; set @var2=4; create table t1 (a int) partition by list(a) ( @@ -201,7 +205,9 @@ select * from t1; deallocate prepare stmt; drop table t1, t2; -# top level impossible where +# top level impossible where; calling +# select_lex->optimize_unflattened_subqueries(false) in if (... || +# (select && select->check_quick(...))) set @var1=1; set @var2=2; CREATE TABLE t1 ( id INT(10), value INT(10) ); @@ -215,7 +221,9 @@ execute stmt using @var1, @var1; deallocate prepare stmt; DROP TABLE t1,t2; -# top level impossible where, with explain +# top level impossible where, with explain; also calling +# select_lex->optimize_unflattened_subqueries(false) under the +# emit_explain_and_leave label set @var1=1; set @var2=2; CREATE TABLE t1 ( id INT(10), value INT(10) ); @@ -246,7 +254,9 @@ CALL p1(2); DROP TABLE t1; DROP PROCEDURE p1; -# partition pruning +# partition pruning; calling +# select_lex->optimize_unflattened_subqueries(false) under if +# (prune_partitions())... set @var1=5; set @var2=4; create table t1 (a int) partition by list(a) ( @@ -282,7 +292,15 @@ select * from t1; deallocate prepare stmt; drop table t1, t2; -# top level impossible where +# top level impossible where; + +# execute stmt using @var1, @var2; calling +# select_lex->optimize_unflattened_subqueries(false) under if (... +# (select && select->check_quick(...)) ...) +# +# execute stmt using @var1, @var1; calling +# select_lex->optimize_unflattened_subqueries(false)) after jumping to +# the cleanup label set @var1=1; set @var2=2; CREATE TABLE t1 ( id INT(10), value INT(10) ); @@ -296,7 +314,9 @@ execute stmt using @var1, @var1; deallocate prepare stmt; DROP TABLE t1,t2; -# top level impossible where, with explain +# top level impossible where, with explain; calling +# select_lex->optimize_unflattened_subqueries(false) under the +# send_nothing_and_leave label set @var1=1; set @var2=2; CREATE TABLE t1 ( id INT(10), value INT(10) ); diff --git a/mysql-test/main/update.result b/mysql-test/main/update.result index e445534c3de..b3ad1ec7828 100644 --- a/mysql-test/main/update.result +++ b/mysql-test/main/update.result @@ -802,4 +802,37 @@ DROP FUNCTION f1; DROP FUNCTION f2; DROP TABLE t1; # End of MariaDB 10.10 tests +# +# MDEV-25008: Delete query gets stuck on mariadb, same query works +# on MySQL 8.0.21 +# +CREATE TABLE t1 ( +id int NOT NULL PRIMARY KEY, +item_id varchar(100), +seller_name varchar(400), +variant varchar(400), +FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant) +)engine=innodb; +insert into t1 select seq,seq,seq,seq from seq_1_to_10000; +explain +UPDATE t1 SET item_id="foo" WHERE id NOT IN +(SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 index NULL PRIMARY 4 NULL # Using where +2 MATERIALIZED ALL NULL NULL NULL NULL # +3 DERIVED t1 ALL NULL NULL NULL NULL # Using temporary; Using filesort +drop table t1; +create table t1 (a int primary key, b int, c int, key(b)); +insert into t1 select seq, seq, seq from seq_1_to_20000; +create table t2 as select * from t1; +explain update t1 set c = 5 where b <= 2 and a not in (select b from t2); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 range b b 5 NULL 2 Using where +2 DEPENDENT SUBQUERY t2 ALL NULL NULL NULL NULL 20000 Using where +explain update t1 set c = 5 where b <= 3 and a not in (select b from t2); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 range b b 5 NULL 3 Using where +2 MATERIALIZED t2 ALL NULL NULL NULL NULL 20000 +drop table t1, t2; +# End of 11.7 tests ALTER DATABASE test CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci; diff --git a/mysql-test/main/update.test b/mysql-test/main/update.test index 4f69c7a9ba6..e62e75ad0b0 100644 --- a/mysql-test/main/update.test +++ b/mysql-test/main/update.test @@ -750,4 +750,40 @@ DROP TABLE t1; --echo # End of MariaDB 10.10 tests +--echo # +--echo # MDEV-25008: Delete query gets stuck on mariadb, same query works +--echo # on MySQL 8.0.21 +--echo # + +--source include/have_sequence.inc + +# original case +CREATE TABLE t1 ( + id int NOT NULL PRIMARY KEY, + item_id varchar(100), + seller_name varchar(400), + variant varchar(400), + FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant) +)engine=innodb; + +insert into t1 select seq,seq,seq,seq from seq_1_to_10000; + +# Masking the `rows` column as the value might vary a bit +--replace_column 9 # +explain +UPDATE t1 SET item_id="foo" WHERE id NOT IN + (SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable); + +drop table t1; + +# example of not full table +create table t1 (a int primary key, b int, c int, key(b)); +insert into t1 select seq, seq, seq from seq_1_to_20000; +create table t2 as select * from t1; +explain update t1 set c = 5 where b <= 2 and a not in (select b from t2); +explain update t1 set c = 5 where b <= 3 and a not in (select b from t2); +drop table t1, t2; + +--echo # End of 11.7 tests + --source include/test_db_charset_restore.inc diff --git a/sql/opt_range.cc b/sql/opt_range.cc index a32c0f67e1c..1124ffc0453 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -2663,9 +2663,7 @@ static int fill_used_fields_bitmap(PARAM *param) In the table struct the following information is updated: quick_keys - Which keys can be used - quick_rows - How many rows the key matches - opt_range_condition_rows - E(# rows that will satisfy the table - condition) + opt_range_condition_rows - E(# rows that will satisfy the table condition) IMPLEMENTATION opt_range_condition_rows value is obtained as follows: diff --git a/sql/opt_subselect.cc b/sql/opt_subselect.cc index 6d66bb875ca..e90f817f30e 100644 --- a/sql/opt_subselect.cc +++ b/sql/opt_subselect.cc @@ -6803,15 +6803,16 @@ bool JOIN::choose_subquery_plan(table_map join_tables) &dummy, &outer_lookup_keys); } + /* + In case of a DELETE or UPDATE, get number of scanned rows as an + (upper bound) estimate of how many times the subquery will be + executed. + */ + else if (outer_join && outer_join->sql_cmd_dml) + outer_lookup_keys= + rows2double(outer_join->sql_cmd_dml->get_scanned_rows()); else - { - /* - TODO: outer_join can be NULL for DELETE statements. - How to compute its cost? - */ outer_lookup_keys= 1; - } - /* B. Estimate the cost and number of records of the subquery both unmodified, and with injected IN->EXISTS predicates. diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 0e7ddad7fab..f58ed2f7c8e 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -8314,7 +8314,7 @@ bool setup_tables(THD *thd, Name_resolution_context *context, 0); SELECT_LEX *select_lex= select_insert ? thd->lex->first_select_lex() : thd->lex->current_select; - if (select_lex->first_cond_optimization) + if (select_lex->first_cond_optimization || !select_lex->leaf_tables_saved) { leaves.empty(); if (select_lex->prep_leaf_list_state != SELECT_LEX::SAVED) diff --git a/sql/sql_cmd.h b/sql/sql_cmd.h index c1b15eaadff..6709036be04 100644 --- a/sql/sql_cmd.h +++ b/sql/sql_cmd.h @@ -20,6 +20,8 @@ #ifndef SQL_CMD_INCLUDED #define SQL_CMD_INCLUDED +#include "my_base.h" + /* When a command is added here, be sure it's also added in mysqld.cc in "struct show_var_st status_vars[]= {" ... @@ -325,10 +327,12 @@ public: select_result *get_result() { return result; } + ha_rows get_scanned_rows() { return scanned_rows; } + protected: Sql_cmd_dml() : Sql_cmd(), lex(nullptr), result(nullptr), - m_empty_query(false) + m_empty_query(false), scanned_rows(0) {} /** @@ -395,6 +399,7 @@ protected: LEX *lex; /**< Pointer to LEX for this statement */ select_result *result; /**< Pointer to object for handling of the result */ bool m_empty_query; /**< True if query will produce no rows */ + ha_rows scanned_rows; /**< Number of scanned rows */ }; diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index 4960e0a29a8..f6fee04e776 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -352,6 +352,16 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) bool delete_record= false; bool delete_while_scanning= table_list->delete_while_scanning; bool portion_of_time_through_update; + /* + TRUE if we are after the call to + select_lex->optimize_unflattened_subqueries(true) and before the + call to select_lex->optimize_unflattened_subqueries(false), to + ensure a call to + select_lex->optimize_unflattened_subqueries(false) happens which + avoid 2nd ps mem leaks when e.g. the first execution produces + empty result and the second execution produces a non-empty set + */ + bool optimize_subqueries= FALSE; DBUG_ENTER("Sql_cmd_delete::delete_single_table"); @@ -388,9 +398,18 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) thd->lex->promote_select_describe_flag_if_needed(); - /* Apply the IN=>EXISTS transformation to all subqueries and optimize them. */ - if (select_lex->optimize_unflattened_subqueries(false)) + /* + Apply the IN=>EXISTS transformation to all constant subqueries + and optimize them. + + It is too early to choose subquery optimization strategies without + an estimate of how many times the subquery will be executed so we + call optimize_unflattened_subqueries() with const_only= true, and + choose between materialization and in-to-exists later. + */ + if (select_lex->optimize_unflattened_subqueries(true)) DBUG_RETURN(TRUE); + optimize_subqueries= TRUE; const_cond= (!conds || conds->const_item()); safe_update= (thd->variables.option_bits & OPTION_SAFE_UPDATES) && @@ -491,6 +510,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) #ifdef WITH_PARTITION_STORAGE_ENGINE if (prune_partitions(thd, table, conds)) { + if (optimize_subqueries && select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + optimize_subqueries= FALSE; free_underlaid_joins(thd, select_lex); query_plan.set_no_partitions(); @@ -529,6 +551,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) goto produce_explain_and_leave; delete select; + if (select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + optimize_subqueries= FALSE; free_underlaid_joins(thd, select_lex); /* Error was already created by quick select evaluation (check_quick()). @@ -559,6 +584,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) if (safe_update && !using_limit) { delete select; + if (optimize_subqueries && select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + optimize_subqueries= FALSE; free_underlaid_joins(thd, select_lex); my_message(ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE, ER_THD(thd, ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE), MYF(0)); @@ -568,7 +596,18 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) if (options & OPTION_QUICK) (void) table->file->extra(HA_EXTRA_QUICK); - query_plan.scanned_rows= select? select->records: table->file->stats.records; + /* + Estimate the number of scanned rows and have it accessible in + JOIN::choose_subquery_plan() from the outer join through + JOIN::sql_cmd_dml + */ + scanned_rows= query_plan.scanned_rows= select ? + select->records : table->file->stats.records; + select_lex->join->sql_cmd_dml= this; + DBUG_ASSERT(optimize_subqueries); + if (select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + optimize_subqueries= FALSE; if (order) { table->update_const_key_parts(conds); @@ -991,6 +1030,8 @@ cleanup: DBUG_PRINT("info",("%ld records deleted",(long) deleted)); } delete file_sort; + if (optimize_subqueries && select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); free_underlaid_joins(thd, select_lex); if (table->file->pushed_cond) table->file->cond_pop(); @@ -1014,6 +1055,9 @@ send_nothing_and_leave: delete select; delete file_sort; + if (!thd->is_error() && optimize_subqueries && + select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); free_underlaid_joins(thd, select_lex); if (table->file->pushed_cond) table->file->cond_pop(); diff --git a/sql/sql_lex.h b/sql/sql_lex.h index eee0035775e..1baf351d6ca 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -1057,6 +1057,7 @@ public: */ List in_funcs; List leaf_tables; + /* Saved leaf tables for subsequent executions */ List leaf_tables_exec; List leaf_tables_prep; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 48216b7a3f7..d6e9818a5a2 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -578,6 +578,7 @@ void JOIN::init(THD *thd_arg, List &fields_arg, is_orig_degenerated= false; with_ties_order_count= 0; prepared= false; + sql_cmd_dml= NULL; }; diff --git a/sql/sql_select.h b/sql/sql_select.h index 81d6992721a..576e02efcc9 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -33,6 +33,8 @@ #include "records.h" /* READ_RECORD */ #include "opt_range.h" /* SQL_SELECT, QUICK_SELECT_I */ #include "filesort.h" +#include "sql_delete.h" +#include "sql_update.h" #include "cset_narrowing.h" @@ -1736,6 +1738,13 @@ public: */ bool is_orig_degenerated; + /* + DELETE and UPDATE may have an imitation JOIN, which is not NULL, + but has NULL join_tab. In such cases we may want to access + sql_cmd_dml::scanned_rows to choose optimization strategies. + */ + Sql_cmd_dml *sql_cmd_dml; + JOIN(THD *thd_arg, List &fields_arg, ulonglong select_options_arg, select_result *result_arg) :fields_list(fields_arg) diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 26dad7946da..06841adbb83 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -376,6 +376,16 @@ bool Sql_cmd_update::update_single_table(THD *thd) List all_fields; killed_state killed_status= NOT_KILLED; bool has_triggers, binlog_is_row, do_direct_update= FALSE; + /* + TRUE if we are after the call to + select_lex->optimize_unflattened_subqueries(true) and before the + call to select_lex->optimize_unflattened_subqueries(false), to + ensure a call to + select_lex->optimize_unflattened_subqueries(false) happens which + avoid 2nd ps mem leaks when e.g. the first execution produces + empty result and the second execution produces a non-empty set + */ + bool need_to_optimize= FALSE; Update_plan query_plan(thd->mem_root); Explain_update *explain; query_plan.index= MAX_KEY; @@ -422,9 +432,18 @@ bool Sql_cmd_update::update_single_table(THD *thd) switch_to_nullable_trigger_fields(*fields, table); switch_to_nullable_trigger_fields(*values, table); - /* Apply the IN=>EXISTS transformation to all subqueries and optimize them */ - if (select_lex->optimize_unflattened_subqueries(false)) + /* + Apply the IN=>EXISTS transformation to all constant subqueries + and optimize them. + + It is too early to choose subquery optimization strategies without + an estimate of how many times the subquery will be executed so we + call optimize_unflattened_subqueries() with const_only= true, and + choose between materialization and in-to-exists later. + */ + if (select_lex->optimize_unflattened_subqueries(true)) DBUG_RETURN(TRUE); + need_to_optimize= TRUE; if (conds) { @@ -458,6 +477,9 @@ bool Sql_cmd_update::update_single_table(THD *thd) #ifdef WITH_PARTITION_STORAGE_ENGINE if (prune_partitions(thd, table, conds)) { + if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + need_to_optimize= FALSE; free_underlaid_joins(thd, select_lex); query_plan.set_no_partitions(); @@ -493,6 +515,9 @@ bool Sql_cmd_update::update_single_table(THD *thd) goto produce_explain_and_leave; delete select; + if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + need_to_optimize= FALSE; free_underlaid_joins(thd, select_lex); /* There was an error or the error was already sent by @@ -545,8 +570,20 @@ bool Sql_cmd_update::update_single_table(THD *thd) table->update_const_key_parts(conds); order= simple_remove_const(order, conds); - query_plan.scanned_rows= select? select->records: table->file->stats.records; - + + /* + Estimate the number of scanned rows and have it accessible in + JOIN::choose_subquery_plan() from the outer join through + JOIN::sql_cmd_dml + */ + scanned_rows= query_plan.scanned_rows= select ? + select->records : table->file->stats.records; + select_lex->join->sql_cmd_dml= this; + DBUG_ASSERT(need_to_optimize); + if (select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); + need_to_optimize= FALSE; + if (select && select->quick && select->quick->unique_key_range()) { /* Single row select (always "ordered"): Ok to use with key field UPDATE */ @@ -1308,6 +1345,9 @@ update_end: err: delete select; delete file_sort; + if (!thd->is_error() && need_to_optimize && + select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); free_underlaid_joins(thd, select_lex); table->file->ha_end_keyread(); if (table->file->pushed_cond) @@ -1328,6 +1368,9 @@ emit_explain_and_leave: int err2= thd->lex->explain->send_explain(thd, extended); delete select; + if (!thd->is_error() && need_to_optimize && + select_lex->optimize_unflattened_subqueries(false)) + DBUG_RETURN(TRUE); free_underlaid_joins(thd, select_lex); DBUG_RETURN((err2 || thd->is_error()) ? 1 : 0); }