From e0a4b8387c0a0eb3d5d92b3d8bbd3db891a567fc Mon Sep 17 00:00:00 2001
From: Martin Hansson <martin.hansson@sun.com>
Date: Thu, 24 Jun 2010 15:21:23 +0200
Subject: [PATCH] Bug#41660: Sort-index_merge for non-first join table may
 require O(#scans) memory

When an index merge operation was restarted, it would
re-allocate the Unique object controlling the duplicate row
ID elimination. Fixed by making the Unique object a member
of QUICK_INDEX_MERGE_SELECT and thus reusing it throughout
the lifetime of this object.
---
 mysql-test/r/error_simulation.result | 35 ++++++++++++++++++++++++++
 mysql-test/t/error_simulation.test   | 29 ++++++++++++++++++++++
 sql/opt_range.cc                     | 37 ++++++++++++++--------------
 sql/opt_range.h                      |  3 ++-
 sql/sql_class.h                      |  3 +++
 5 files changed, 88 insertions(+), 19 deletions(-)

diff --git a/mysql-test/r/error_simulation.result b/mysql-test/r/error_simulation.result
index fc58687cc86..b6b79cb596b 100644
--- a/mysql-test/r/error_simulation.result
+++ b/mysql-test/r/error_simulation.result
@@ -48,5 +48,40 @@ Got one of the listed errors
 SET SESSION debug=DEFAULT;
 DROP TABLE t1;
 #
+# Bug#41660: Sort-index_merge for non-first join table may require 
+# O(#scans) memory
+#
+CREATE TABLE t1 (a INT);
+INSERT INTO t1 VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);
+CREATE TABLE t2 (a INT, b INT, filler CHAR(100), KEY(a), KEY(b));
+INSERT INTO t2 SELECT 1000, 1000, 'filler' FROM t1 A, t1 B, t1 C;
+INSERT INTO t2 VALUES (1, 1, 'data');
+# the example query uses LEFT JOIN only for the sake of being able to
+# demonstrate the issue with a very small dataset. (left outer join 
+# disables the use of join buffering, so we get the second table 
+# re-scanned for every record in the outer table. if we used inner join,
+# we would need to have thousands of records and/or more columns in both
+# tables so that the join buffer is filled and re-scans are triggered).
+SET SESSION debug = '+d,only_one_Unique_may_be_created';
+EXPLAIN
+SELECT * FROM t1 LEFT JOIN t2 ON ( t2.a < 10 OR t2.b < 10 );
+id	select_type	table	type	possible_keys	key	key_len	ref	rows	Extra
+x	x	x	x	x	x	x	x	x	
+x	x	x	x	x	x	x	x	x	Using sort_union(a,b); Using where
+SELECT * FROM t1 LEFT JOIN t2 ON ( t2.a < 10 OR t2.b < 10 );
+a	a	b	filler
+0	1	1	data
+1	1	1	data
+2	1	1	data
+3	1	1	data
+4	1	1	data
+5	1	1	data
+6	1	1	data
+7	1	1	data
+8	1	1	data
+9	1	1	data
+SET SESSION debug = DEFAULT;
+DROP TABLE t1, t2;
+#
 # End of 5.1 tests
 #
diff --git a/mysql-test/t/error_simulation.test b/mysql-test/t/error_simulation.test
index 7a48a2e3231..8e65c338f36 100644
--- a/mysql-test/t/error_simulation.test
+++ b/mysql-test/t/error_simulation.test
@@ -45,6 +45,35 @@ SHOW CREATE TABLE t1;
 SELECT * FROM t1;
 DROP TABLE t1;
 
+-- echo #
+-- echo # Bug#41660: Sort-index_merge for non-first join table may require 
+-- echo # O(#scans) memory
+-- echo #
+
+CREATE TABLE t1 (a INT);
+INSERT INTO t1 VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);
+
+CREATE TABLE t2 (a INT, b INT, filler CHAR(100), KEY(a), KEY(b));
+INSERT INTO t2 SELECT 1000, 1000, 'filler' FROM t1 A, t1 B, t1 C;
+INSERT INTO t2 VALUES (1, 1, 'data');
+
+--echo # the example query uses LEFT JOIN only for the sake of being able to
+--echo # demonstrate the issue with a very small dataset. (left outer join 
+--echo # disables the use of join buffering, so we get the second table 
+--echo # re-scanned for every record in the outer table. if we used inner join,
+--echo # we would need to have thousands of records and/or more columns in both
+--echo # tables so that the join buffer is filled and re-scans are triggered).
+
+SET SESSION debug = '+d,only_one_Unique_may_be_created';
+
+--replace_column 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x
+EXPLAIN
+SELECT * FROM t1 LEFT JOIN t2 ON ( t2.a < 10 OR t2.b < 10 );
+SELECT * FROM t1 LEFT JOIN t2 ON ( t2.a < 10 OR t2.b < 10 );
+
+SET SESSION debug = DEFAULT;
+
+DROP TABLE t1, t2;
 
 --echo #
 --echo # Bug#42064: low memory crash when importing hex strings, in Item_hex_string::Item_hex_string
diff --git a/sql/opt_range.cc b/sql/opt_range.cc
index 2f4281d539f..f2f14a2dcc9 100644
--- a/sql/opt_range.cc
+++ b/sql/opt_range.cc
@@ -1194,7 +1194,7 @@ QUICK_RANGE_SELECT::~QUICK_RANGE_SELECT()
 
 QUICK_INDEX_MERGE_SELECT::QUICK_INDEX_MERGE_SELECT(THD *thd_param,
                                                    TABLE *table)
-  :pk_quick_select(NULL), thd(thd_param)
+  :unique(NULL), pk_quick_select(NULL), thd(thd_param)
 {
   DBUG_ENTER("QUICK_INDEX_MERGE_SELECT::QUICK_INDEX_MERGE_SELECT");
   index= MAX_KEY;
@@ -1236,6 +1236,7 @@ QUICK_INDEX_MERGE_SELECT::~QUICK_INDEX_MERGE_SELECT()
   List_iterator_fast<QUICK_RANGE_SELECT> quick_it(quick_selects);
   QUICK_RANGE_SELECT* quick;
   DBUG_ENTER("QUICK_INDEX_MERGE_SELECT::~QUICK_INDEX_MERGE_SELECT");
+  delete unique;
   quick_it.rewind();
   while ((quick= quick_it++))
     quick->file= NULL;
@@ -8153,7 +8154,6 @@ int QUICK_INDEX_MERGE_SELECT::read_keys_and_merge()
   List_iterator_fast<QUICK_RANGE_SELECT> cur_quick_it(quick_selects);
   QUICK_RANGE_SELECT* cur_quick;
   int result;
-  Unique *unique;
   handler *file= head->file;
   DBUG_ENTER("QUICK_INDEX_MERGE_SELECT::read_keys_and_merge");
 
@@ -8172,9 +8172,22 @@ int QUICK_INDEX_MERGE_SELECT::read_keys_and_merge()
   if (cur_quick->init() || cur_quick->reset())
     DBUG_RETURN(1);
 
-  unique= new Unique(refpos_order_cmp, (void *)file,
-                     file->ref_length,
-                     thd->variables.sortbuff_size);
+  if (unique == NULL)
+  {
+    DBUG_EXECUTE_IF("index_merge_may_not_create_a_Unique", abort(); );
+    DBUG_EXECUTE_IF("only_one_Unique_may_be_created", 
+                    DBUG_SET("+d,index_merge_may_not_create_a_Unique"); );
+
+    unique= new Unique(refpos_order_cmp, (void *)file,
+                       file->ref_length,
+                       thd->variables.sortbuff_size);
+  }
+  else
+    unique->reset();
+
+  DBUG_ASSERT(file->ref_length == unique->get_size());
+  DBUG_ASSERT(thd->variables.sortbuff_size == unique->get_max_in_memory_size());
+
   if (!unique)
     DBUG_RETURN(1);
   for (;;)
@@ -8189,10 +8202,7 @@ int QUICK_INDEX_MERGE_SELECT::read_keys_and_merge()
       if (cur_quick->file->inited != handler::NONE) 
         cur_quick->file->ha_index_end();
       if (cur_quick->init() || cur_quick->reset())
-      {
-        delete unique;
         DBUG_RETURN(1);
-      }
     }
 
     if (result)
@@ -8200,17 +8210,13 @@ int QUICK_INDEX_MERGE_SELECT::read_keys_and_merge()
       if (result != HA_ERR_END_OF_FILE)
       {
         cur_quick->range_end();
-        delete unique;
         DBUG_RETURN(result);
       }
       break;
     }
 
     if (thd->killed)
-    {
-      delete unique;
       DBUG_RETURN(1);
-    }
 
     /* skip row if it will be retrieved by clustered PK scan */
     if (pk_quick_select && pk_quick_select->row_in_ranges())
@@ -8219,10 +8225,7 @@ int QUICK_INDEX_MERGE_SELECT::read_keys_and_merge()
     cur_quick->file->position(cur_quick->record);
     result= unique->unique_add((char*)cur_quick->file->ref);
     if (result)
-    {
-      delete unique;
       DBUG_RETURN(1);
-    }
   }
 
   /*
@@ -8231,7 +8234,6 @@ int QUICK_INDEX_MERGE_SELECT::read_keys_and_merge()
     sequence.
   */
   result= unique->get(head);
-  delete unique;
   doing_pk_scan= FALSE;
   /* index_merge currently doesn't support "using index" at all */
   head->set_keyread(FALSE);
@@ -10277,7 +10279,7 @@ QUICK_GROUP_MIN_MAX_SELECT(TABLE *table, JOIN *join_arg, bool have_min_arg,
                            uint use_index, double read_cost_arg,
                            ha_rows records_arg, uint key_infix_len_arg,
                            uchar *key_infix_arg, MEM_ROOT *parent_alloc)
-  :join(join_arg), index_info(index_info_arg),
+  :file(table->file), join(join_arg), index_info(index_info_arg),
    group_prefix_len(group_prefix_len_arg),
    group_key_parts(group_key_parts_arg), have_min(have_min_arg),
    have_max(have_max_arg), seen_first_key(FALSE),
@@ -10286,7 +10288,6 @@ QUICK_GROUP_MIN_MAX_SELECT(TABLE *table, JOIN *join_arg, bool have_min_arg,
    max_functions_it(NULL)
 {
   head=       table;
-  file=       head->file;
   index=      use_index;
   record=     head->record[0];
   tmp_record= head->record[1];
diff --git a/sql/opt_range.h b/sql/opt_range.h
index e7d8297faf8..edae1e4114a 100644
--- a/sql/opt_range.h
+++ b/sql/opt_range.h
@@ -500,6 +500,7 @@ class QUICK_RANGE_SELECT_GEOM: public QUICK_RANGE_SELECT
 
 class QUICK_INDEX_MERGE_SELECT : public QUICK_SELECT_I
 {
+  Unique *unique;
 public:
   QUICK_INDEX_MERGE_SELECT(THD *thd, TABLE *table);
   ~QUICK_INDEX_MERGE_SELECT();
@@ -684,7 +685,7 @@ class QUICK_ROR_UNION_SELECT : public QUICK_SELECT_I
 class QUICK_GROUP_MIN_MAX_SELECT : public QUICK_SELECT_I
 {
 private:
-  handler *file;         /* The handler used to get data. */
+  handler * const file;   /* The handler used to get data. */
   JOIN *join;            /* Descriptor of the current query */
   KEY  *index_info;      /* The index chosen for data access */
   uchar *record;          /* Buffer where the next record is returned. */
diff --git a/sql/sql_class.h b/sql/sql_class.h
index 19a433746e4..4c1d4a98db0 100644
--- a/sql/sql_class.h
+++ b/sql/sql_class.h
@@ -2935,6 +2935,9 @@ class Unique :public Sql_alloc
   void reset();
   bool walk(tree_walk_action action, void *walk_action_arg);
 
+  uint get_size() const { return size; }
+  ulonglong get_max_in_memory_size() const { return max_in_memory_size; }
+
   friend int unique_write_to_file(uchar* key, element_count count, Unique *unique);
   friend int unique_write_to_ptrs(uchar* key, element_count count, Unique *unique);
 };
-- 
2.30.9