List:Internals« Previous MessageNext Message »
From:Sergey Petrunia Date:September 30 2005 1:21pm
Subject:bk commit into 4.1 tree (sergefp:1.2463) BUG#12915
View as plain text  
Below is the list of changes that have just been committed into a local
4.1 repository of psergey. When psergey does a push these changes will
be propagated to the main repository and, within 24 hours after the
push, to the public repository.
For information on how to access the public repository
see http://dev.mysql.com/doc/mysql/en/installing-source-tree.html

ChangeSet
  1.2463 05/09/30 15:21:37 sergefp@stripped +9 -0
  BUG#12915: Added single-table UPDATE/DELTE ... ORDER BY ... LIMIT
  optimization: now can use index to find records to update/delete
  when there is no WHERE clause.

  sql/structs.h
    1.41 05/09/30 15:21:34 sergefp@stripped +1 -0
    BUG#12915: Added init_read_record_idx(), rr_index() that allow to scan
    index using init_read_record()/READ_RECORD::read_record()

  sql/sql_update.cc
    1.144 05/09/30 15:21:34 sergefp@stripped +25 -7
    BUG#12915: Added single-table UPDATE ... ORDER BY ... LIMIT optimization: 
    now can use index to find records to update when there is no WHERE clause.

  sql/sql_delete.cc
    1.137 05/09/30 15:21:34 sergefp@stripped +32 -14
    BUG#12915: Added single-table DELETE ... ORDER BY ... LIMIT optimization: 
    now can use index to find records to delete when there is no WHERE clause.

  sql/records.cc
    1.30 05/09/30 15:21:34 sergefp@stripped +104 -1
    BUG#12915: Added init_read_record_idx(), rr_index() that allow to scan
    index using init_read_record()/read_record.read_record()

  sql/opt_range.h
    1.40 05/09/30 15:21:34 sergefp@stripped +2 -0
    BUG#12915: Added get_index_for_order() function

  sql/opt_range.cc
    1.143 05/09/30 15:21:34 sergefp@stripped +88 -0
    BUG#12915: Added get_index_for_order() - find an index that can be
    used to get first N table records in given ordering cheaper then 
    one would with full table scan.

  sql/mysql_priv.h
    1.368 05/09/30 15:21:34 sergefp@stripped +2 -0
    BUG#12915: Added init_read_record_idx function.

  mysql-test/t/update.test
    1.23 05/09/30 15:21:34 sergefp@stripped +29 -0
    Testcase for BUG#12915

  mysql-test/r/update.result
    1.25 05/09/30 15:21:34 sergefp@stripped +59 -0
    Testcase for BUG#12915

# This is a BitKeeper patch.  What follows are the unified diffs for the
# set of deltas contained in the patch.  The rest of the patch, the part
# that BitKeeper cares about, is below these diffs.
# User:	sergefp
# Host:	pslp.mylan
# Root:	/home/psergey/mysql-4.1-sept30-test

--- 1.367/sql/mysql_priv.h	2005-09-14 16:31:32 +04:00
+++ 1.368/sql/mysql_priv.h	2005-09-30 15:21:34 +04:00
@@ -1105,6 +1105,8 @@
 void init_read_record(READ_RECORD *info, THD *thd, TABLE *reg_form,
 		      SQL_SELECT *select,
 		      int use_record_cache, bool print_errors);
+void init_read_record_idx(READ_RECORD *info, THD *thd, TABLE *table, 
+                          bool print_error, uint idx);
 void end_read_record(READ_RECORD *info);
 ha_rows filesort(THD *thd, TABLE *form,struct st_sort_field *sortorder,
 		 uint s_length, SQL_SELECT *select,

--- 1.142/sql/opt_range.cc	2005-09-06 17:49:08 +04:00
+++ 1.143/sql/opt_range.cc	2005-09-30 15:21:34 +04:00
@@ -582,6 +582,94 @@
   return root;
 }
 
+
+/*
+  Find an index that allows to retrieve first #limit records in the given
+  order cheaper then one would retrieve them using full table scan.
+
+  SYNOPSIS
+    get_index_for_order()
+      table  Table to be accessed
+      order  Required ordering
+      limit  Number of records that will be retrieved
+
+  DESCRIPTION
+    Run through all table indexes and find the shortest index that allows
+    records to be retrieved in given order. If there is such index and
+    reading first #limit records from it is cheaper then scanning the entire
+    table, return it.
+
+    This function is used only by UPDATE/DELETE, so we take into account how
+    the UPDATE/DELETE code will work:
+     * index can only be scanned in forward direction
+     * HA_EXTRA_KEYREAD will not be used
+    Perhaps these assumptions could be relaxed
+
+  RETURN
+    index number
+    MAX_KEY if no such index was found.
+*/
+
+uint get_index_for_order(TABLE *table, ORDER *order, ha_rows limit)
+{
+  uint idx;
+  uint match_key= MAX_KEY, match_key_len= MAX_KEY_LENGTH + 1;
+  ORDER *ord;
+  
+  for (ord= order; ord; ord= ord->next)
+    if (!ord->asc)
+      return MAX_KEY;
+
+  for (idx= 0; idx < table->keys; idx++)
+  {
+    if (!(table->keys_in_use_for_query.is_set(idx)))
+      continue;
+    KEY_PART_INFO *keyinfo= table->key_info[idx].key_part;
+    uint partno= 0;
+    
+    /* 
+      The below check is sufficient considering we now have either BTREE 
+      indexes (records are returned in order for any index prefix) or HASH 
+      indexes (records are not returned in order for any index prefix).
+    */
+    if (!(table->file->index_flags(idx, 0, 1) & HA_READ_ORDER))
+      continue;
+    for (ord= order; ord; ord= ord->next, partno++)
+    {
+      Item *item= order->item[0];
+      if (!(item->type() == Item::FIELD_ITEM &&
+           ((Item_field*)item)->field->eq(keyinfo[partno].field)))
+        break;
+    }
+    
+    if (!ord && table->key_info[idx].key_length < match_key_len)
+    {
+      /* 
+        Ok, the ordering is compatible and this key is shorter then
+        previous match (we want shorter keys as we'll have to read fewer
+        index pages for the same number of records)
+      */
+      match_key= idx;
+      match_key_len= table->key_info[idx].key_length;
+    }
+  }
+
+  if (match_key != MAX_KEY)
+  {
+    /* 
+      Found an index that allows records to be retrieved in the requested 
+      order. Now we'll check if using the index is cheaper then doing a table
+      scan.
+    */
+    double full_scan_time= table->file->scan_time();
+    double index_scan_time= table->file->read_time(match_key, 1, limit);
+    if (index_scan_time > full_scan_time)
+      match_key= MAX_KEY;
+  }
+  return match_key;
+}
+
+
 /*
   Test if a key can be used in different ranges
 

--- 1.39/sql/opt_range.h	2005-06-04 00:45:57 +04:00
+++ 1.40/sql/opt_range.h	2005-09-30 15:21:34 +04:00
@@ -167,4 +167,6 @@
 QUICK_SELECT *get_quick_select_for_ref(THD *thd, TABLE *table,
 				       struct st_table_ref *ref);
 
+uint get_index_for_order(TABLE *table, ORDER *order, ha_rows limit);
+
 #endif

--- 1.29/sql/records.cc	2004-10-06 20:14:29 +04:00
+++ 1.30/sql/records.cc	2005-09-30 15:21:34 +04:00
@@ -29,7 +29,59 @@
 static int init_rr_cache(READ_RECORD *info);
 static int rr_cmp(uchar *a,uchar *b);
 
-	/* init struct for read with info->read_record */
+static int rr_index(READ_RECORD *info);
+
+
+
+/*
+  Initialize READ_RECORD structure to perform full index scan
+
+  SYNOPSIS 
+    init_read_record_idx()
+      info         READ_RECORD structure to initialize.
+      thd          Thread handle
+      table        Table to be accessed 
+      print_error  If true, call table->file->print_error() if an error
+                   occurs (except for end-of-records error)
+      idx          index to scan
+  
+  DESCRIPTION
+    Initialize READ_RECORD structure to perform full index scan (in forward
+    direction) using read_record.read_record() interface.
+    
+    This function has been added at late stage and is used only by
+    UPDATE/DELETE. Other statements perform index scans using
+    join_read_first/next functions.
+*/
+
+void init_read_record_idx(READ_RECORD *info, THD *thd, TABLE *table,
+                          bool print_error, uint idx)
+{
+  bzero((char*) info,sizeof(*info));
+  info->thd=thd;
+  info->table=table;
+  info->file= table->file;
+  info->forms= &info->table;		/* Only one table */
+    
+  info->record= table->record[0];
+  info->ref_length= table->file->ref_length;
+
+  info->select=NULL;
+  info->print_error=print_error;
+  info->ignore_not_found_rows= 0;
+  table->status=0;			/* And it's always found */
+
+  if (!table->file->inited)
+  {
+    table->file->ha_index_init(idx);
+    table->file->extra(HA_EXTRA_RETRIEVE_PRIMARY_KEY);
+  }
+  info->read_record= rr_index;
+  info->first= TRUE;
+}
+
+
+/* init struct for read with info->read_record */
 
 void init_read_record(READ_RECORD *info,THD *thd, TABLE *table,
 		      SQL_SELECT *select,
@@ -181,6 +233,57 @@
   return tmp;
 }
 
+
+/*
+  Read next index record. The calling convention of this function is 
+  compatible with READ_RECORD::read_record.
+
+  SYNOPSIS
+    rr_index()
+      info  Scan info
+
+  RETURN
+    0   Ok
+    -1  End of records 
+    1   Error   
+*/
+
+static int rr_index(READ_RECORD *info)
+{
+  int tmp;
+  while (1)
+  {
+    if (info->first)
+    {
+      info->first= FALSE;
+      tmp= info->file->index_first(info->record);
+    }
+    else
+      tmp= info->file->index_next(info->record);
+    
+    if (!tmp)
+      break;
+    if (info->thd->killed)
+    {
+      my_error(ER_SERVER_SHUTDOWN,MYF(0));
+      return 1;
+    }
+    if (tmp != HA_ERR_RECORD_DELETED)
+    {
+      if (tmp == HA_ERR_END_OF_FILE)
+	tmp= -1;
+      else
+      {
+	if (info->print_error)
+	  info->table->file->print_error(tmp,MYF(0));
+	if (tmp < 0)				// Fix negative BDB errno
+	  tmp=1;
+      }
+      break;
+    }
+  }
+  return tmp;
+}
 
 static int rr_sequential(READ_RECORD *info)
 {

--- 1.136/sql/sql_delete.cc	2005-09-25 22:22:20 +04:00
+++ 1.137/sql/sql_delete.cc	2005-09-30 15:21:34 +04:00
@@ -37,6 +37,7 @@
   bool 		using_limit=limit != HA_POS_ERROR;
   bool		transactional_table, log_delayed, safe_update, const_cond; 
   ha_rows	deleted;
+  uint usable_index= MAX_KEY;
   DBUG_ENTER("mysql_delete");
 
   if ((open_and_lock_tables(thd, table_list)))
@@ -119,30 +120,47 @@
     tables.table = table;
     tables.alias = table_list->alias;
 
-    table->sort.io_cache = (IO_CACHE *) my_malloc(sizeof(IO_CACHE),
-                                             MYF(MY_FAE | MY_ZEROFILL));
       if (thd->lex->select_lex.setup_ref_array(thd, order->elements) ||
 	  setup_order(thd, thd->lex->select_lex.ref_pointer_array, &tables,
-		      fields, all_fields, (ORDER*) order->first) ||
-	  !(sortorder=make_unireg_sortorder((ORDER*) order->first, &length)) ||
+                    fields, all_fields, (ORDER*) order->first))
+    {
+      delete select;
+      free_underlaid_joins(thd, &thd->lex->select_lex);
+      DBUG_RETURN(-1);			// This will force out message
+    }
+    
+    if (!select && limit != HA_POS_ERROR)
+      usable_index= get_index_for_order(table, (ORDER*)(order->first), limit);
+
+    if (usable_index == MAX_KEY)
+    {
+      table->sort.io_cache= (IO_CACHE *) my_malloc(sizeof(IO_CACHE),
+                                                   MYF(MY_FAE | MY_ZEROFILL));
+    
+      if ( !(sortorder=make_unireg_sortorder((ORDER*) order->first, &length)) ||
 	  (table->sort.found_records = filesort(thd, table, sortorder, length,
 					   select, HA_POS_ERROR,
 					   &examined_rows))
 	  == HA_POS_ERROR)
-    {
+      {
+        delete select;
+        free_underlaid_joins(thd, &thd->lex->select_lex);
+        DBUG_RETURN(-1);			// This will force out message
+      }
+      /*
+        Filesort has already found and selected the rows we want to delete,
+        so we don't need the where clause
+      */
       delete select;
-      free_underlaid_joins(thd, &thd->lex->select_lex);
-      DBUG_RETURN(-1);			// This will force out message
+      select= 0;
     }
-    /*
-      Filesort has already found and selected the rows we want to delete,
-      so we don't need the where clause
-    */
-    delete select;
-    select= 0;
   }
 
-  init_read_record(&info,thd,table,select,1,1);
+  if (usable_index==MAX_KEY)
+    init_read_record(&info,thd,table,select,1,1);
+  else
+    init_read_record_idx(&info, thd, table, 1, usable_index);
+
   deleted=0L;
   init_ftfuncs(thd, &thd->lex->select_lex, 1);
   thd->proc_info="updating";

--- 1.143/sql/sql_update.cc	2005-09-25 22:22:21 +04:00
+++ 1.144/sql/sql_update.cc	2005-09-30 15:21:34 +04:00
@@ -61,7 +61,8 @@
   bool		safe_update= thd->options & OPTION_SAFE_UPDATES;
   bool		used_key_is_modified, transactional_table, log_delayed;
   int		error=0;
-  uint		used_index;
+  uint		used_index= MAX_KEY;
+  bool          need_sort= TRUE;
 #ifndef NO_EMBEDDED_ACCESS_CHECKS
   uint		want_privilege;
 #endif
@@ -145,6 +146,11 @@
     send_ok(thd);				// No matching records
     DBUG_RETURN(0);
   }
+  if (!select && limit != HA_POS_ERROR)
+  {
+    if (MAX_KEY != (used_index= get_index_for_order(table, order, limit)))
+      need_sort= FALSE;
+  }
   /* If running in safe sql mode, don't allow updates without keys */
   if (table->quick_keys.is_clear_all())
   {
@@ -157,6 +163,7 @@
     }
   }
   init_ftfuncs(thd, &thd->lex->select_lex, 1);
+  
   /* Check if we are modifying a key that we are used to search with */
   if (select && select->quick)
   {
@@ -164,13 +171,15 @@
     used_key_is_modified= (!select->quick->unique_key_range() &&
 			   check_if_key_used(table, used_index, fields));
   }
+  else if (used_index != MAX_KEY)
+  {
+    used_key_is_modified= check_if_key_used(table, used_index, fields);
+  }
   else if ((used_index=table->file->key_used_on_scan) < MAX_KEY)
     used_key_is_modified=check_if_key_used(table, used_index, fields);
   else
-  {
     used_key_is_modified=0;
-    used_index= MAX_KEY;
-  }
+
   if (used_key_is_modified || order)
   {
     /*
@@ -181,10 +190,11 @@
     if (used_index < MAX_KEY && old_used_keys.is_set(used_index))
     {
       table->key_read=1;
-      table->file->extra(HA_EXTRA_KEYREAD);
+      table->file->extra(HA_EXTRA_KEYREAD); //todo: psergey: check
     }
 
-    if (order)
+    /* note: can actually avoid sorting below.. */
+    if (order && need_sort)
     {
       /*
 	Doing an ORDER BY;  Let filesort find and sort the rows we are going
@@ -225,7 +235,11 @@
 			   DISK_BUFFER_SIZE, MYF(MY_WME)))
 	goto err;
 
-      init_read_record(&info,thd,table,select,0,1);
+      if (used_index == MAX_KEY)
+        init_read_record(&info,thd,table,select,0,1);
+      else
+        init_read_record_idx(&info, thd, table, 1, used_index);
+
       thd->proc_info="Searching rows for update";
       uint tmp_limit= limit;
 
@@ -251,6 +265,10 @@
 	error= 1;				// Aborted
       limit= tmp_limit;
       end_read_record(&info);
+
+      /* if we got here we must not use index in the main update loop below */
+      used_index= MAX_KEY;
+      
       /* Change select to use tempfile */
       if (select)
       {

--- 1.40/sql/structs.h	2005-09-14 15:18:11 +04:00
+++ 1.41/sql/structs.h	2005-09-30 15:21:34 +04:00
@@ -132,6 +132,7 @@
   byte	*cache,*cache_pos,*cache_end,*read_positions;
   IO_CACHE *io_cache;
   bool print_error, ignore_not_found_rows;
+  bool first; /* used only with rr_index_read */
 } READ_RECORD;
 
 

--- 1.24/mysql-test/r/update.result	2005-09-22 01:38:24 +04:00
+++ 1.25/mysql-test/r/update.result	2005-09-30 15:21:34 +04:00
@@ -263,3 +263,62 @@
 delete from t1 where count(*)=1;
 ERROR HY000: Invalid use of group function
 drop table t1;
+create table t1 ( a int, index (a) );
+insert into t1 values (0),(0),(0),(0),(0),(0),(0),(0);
+flush status;
+select a from t1 order by a limit 1;
+a
+0
+show status like 'handler_read%';
+Variable_name	Value
+Handler_read_first	1
+Handler_read_key	0
+Handler_read_next	0
+Handler_read_prev	0
+Handler_read_rnd	0
+Handler_read_rnd_next	0
+flush status;
+update t1 set a=unix_timestamp() order by a limit 1;
+show status like 'handler_read%';
+Variable_name	Value
+Handler_read_first	1
+Handler_read_key	0
+Handler_read_next	0
+Handler_read_prev	0
+Handler_read_rnd	1
+Handler_read_rnd_next	0
+flush status;
+delete from t1 order by a limit 1;
+show status like 'handler_read%';
+Variable_name	Value
+Handler_read_first	1
+Handler_read_key	0
+Handler_read_next	0
+Handler_read_prev	0
+Handler_read_rnd	0
+Handler_read_rnd_next	0
+flush status;
+delete from t1 order by a desc limit 1;
+show status like 'handler_read%';
+Variable_name	Value
+Handler_read_first	0
+Handler_read_key	0
+Handler_read_next	0
+Handler_read_prev	0
+Handler_read_rnd	1
+Handler_read_rnd_next	9
+alter table t1 disable keys;
+flush status;
+delete from t1 order by a limit 1;
+show status like 'handler_read%';
+Variable_name	Value
+Handler_read_first	0
+Handler_read_key	0
+Handler_read_next	0
+Handler_read_prev	0
+Handler_read_rnd	1
+Handler_read_rnd_next	9
+select count(*) from t1;
+count(*)
+5
+drop table t1;

--- 1.22/mysql-test/t/update.test	2005-09-22 01:38:16 +04:00
+++ 1.23/mysql-test/t/update.test	2005-09-30 15:21:34 +04:00
@@ -227,4 +227,33 @@
 delete from t1 where count(*)=1;
 drop table t1;
 
+# BUG#12915: Optimize "DELETE|UPDATE ... ORDER BY ... LIMIT n" to use an index
+create table t1 ( a int, index (a) );
+insert into t1 values (0),(0),(0),(0),(0),(0),(0),(0);
+
+flush status;
+select a from t1 order by a limit 1;
+show status like 'handler_read%';
+
+flush status;
+update t1 set a=unix_timestamp() order by a limit 1;
+show status like 'handler_read%';
+
+flush status;
+delete from t1 order by a limit 1;
+show status like 'handler_read%';
+
+flush status;
+delete from t1 order by a desc limit 1;
+show status like 'handler_read%';
+
+alter table t1 disable keys;
+
+flush status;
+delete from t1 order by a limit 1;
+show status like 'handler_read%';
+
+select count(*) from t1;
+
+drop table t1;
 # End of 4.1 tests
Thread
bk commit into 4.1 tree (sergefp:1.2463) BUG#12915Sergey Petrunia30 Sep