Commit 1fe423ae authored by unknown's avatar unknown

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.


mysql-test/r/update.result:
  Testcase for BUG#12915
mysql-test/t/update.test:
  Testcase for BUG#12915
sql/mysql_priv.h:
  BUG#12915: Added init_read_record_idx function.
sql/opt_range.cc:
  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/opt_range.h:
  BUG#12915: Added get_index_for_order() function
sql/records.cc:
  BUG#12915: Added init_read_record_idx(), rr_index() that allow to scan
  index using init_read_record()/read_record.read_record()
sql/sql_delete.cc:
  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/sql_update.cc:
  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/structs.h:
  BUG#12915: Added init_read_record_idx(), rr_index() that allow to scan
  index using init_read_record()/READ_RECORD::read_record()
parent 7b8a6e45
...@@ -263,3 +263,62 @@ test ...@@ -263,3 +263,62 @@ test
delete from t1 where count(*)=1; delete from t1 where count(*)=1;
ERROR HY000: Invalid use of group function ERROR HY000: Invalid use of group function
drop table t1; 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;
...@@ -227,4 +227,33 @@ select DATABASE(); ...@@ -227,4 +227,33 @@ select DATABASE();
delete from t1 where count(*)=1; delete from t1 where count(*)=1;
drop table t1; 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 # End of 4.1 tests
...@@ -1105,6 +1105,8 @@ void change_byte(byte *,uint,char,char); ...@@ -1105,6 +1105,8 @@ void change_byte(byte *,uint,char,char);
void init_read_record(READ_RECORD *info, THD *thd, TABLE *reg_form, void init_read_record(READ_RECORD *info, THD *thd, TABLE *reg_form,
SQL_SELECT *select, SQL_SELECT *select,
int use_record_cache, bool print_errors); 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); void end_read_record(READ_RECORD *info);
ha_rows filesort(THD *thd, TABLE *form,struct st_sort_field *sortorder, ha_rows filesort(THD *thd, TABLE *form,struct st_sort_field *sortorder,
uint s_length, SQL_SELECT *select, uint s_length, SQL_SELECT *select,
......
...@@ -582,6 +582,94 @@ SEL_ARG *SEL_ARG::clone_tree() ...@@ -582,6 +582,94 @@ SEL_ARG *SEL_ARG::clone_tree()
return root; 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 Test if a key can be used in different ranges
......
...@@ -167,4 +167,6 @@ class FT_SELECT: public QUICK_SELECT { ...@@ -167,4 +167,6 @@ class FT_SELECT: public QUICK_SELECT {
QUICK_SELECT *get_quick_select_for_ref(THD *thd, TABLE *table, QUICK_SELECT *get_quick_select_for_ref(THD *thd, TABLE *table,
struct st_table_ref *ref); struct st_table_ref *ref);
uint get_index_for_order(TABLE *table, ORDER *order, ha_rows limit);
#endif #endif
...@@ -29,7 +29,59 @@ static int rr_from_cache(READ_RECORD *info); ...@@ -29,7 +29,59 @@ static int rr_from_cache(READ_RECORD *info);
static int init_rr_cache(READ_RECORD *info); static int init_rr_cache(READ_RECORD *info);
static int rr_cmp(uchar *a,uchar *b); 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, void init_read_record(READ_RECORD *info,THD *thd, TABLE *table,
SQL_SELECT *select, SQL_SELECT *select,
...@@ -182,6 +234,57 @@ static int rr_quick(READ_RECORD *info) ...@@ -182,6 +234,57 @@ static int rr_quick(READ_RECORD *info)
} }
/*
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) static int rr_sequential(READ_RECORD *info)
{ {
int tmp; int tmp;
......
...@@ -37,6 +37,7 @@ int mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds, ...@@ -37,6 +37,7 @@ int mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds,
bool using_limit=limit != HA_POS_ERROR; bool using_limit=limit != HA_POS_ERROR;
bool transactional_table, log_delayed, safe_update, const_cond; bool transactional_table, log_delayed, safe_update, const_cond;
ha_rows deleted; ha_rows deleted;
uint usable_index= MAX_KEY;
DBUG_ENTER("mysql_delete"); DBUG_ENTER("mysql_delete");
if ((open_and_lock_tables(thd, table_list))) if ((open_and_lock_tables(thd, table_list)))
...@@ -119,30 +120,47 @@ int mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds, ...@@ -119,30 +120,47 @@ int mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds,
tables.table = table; tables.table = table;
tables.alias = table_list->alias; 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) || if (thd->lex->select_lex.setup_ref_array(thd, order->elements) ||
setup_order(thd, thd->lex->select_lex.ref_pointer_array, &tables, setup_order(thd, thd->lex->select_lex.ref_pointer_array, &tables,
fields, all_fields, (ORDER*) order->first) || fields, all_fields, (ORDER*) order->first))
!(sortorder=make_unireg_sortorder((ORDER*) order->first, &length)) || {
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, (table->sort.found_records = filesort(thd, table, sortorder, length,
select, HA_POS_ERROR, select, HA_POS_ERROR,
&examined_rows)) &examined_rows))
== HA_POS_ERROR) == 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; delete select;
free_underlaid_joins(thd, &thd->lex->select_lex); select= 0;
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;
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; deleted=0L;
init_ftfuncs(thd, &thd->lex->select_lex, 1); init_ftfuncs(thd, &thd->lex->select_lex, 1);
thd->proc_info="updating"; thd->proc_info="updating";
......
...@@ -61,7 +61,8 @@ int mysql_update(THD *thd, ...@@ -61,7 +61,8 @@ int mysql_update(THD *thd,
bool safe_update= thd->options & OPTION_SAFE_UPDATES; bool safe_update= thd->options & OPTION_SAFE_UPDATES;
bool used_key_is_modified, transactional_table, log_delayed; bool used_key_is_modified, transactional_table, log_delayed;
int error=0; int error=0;
uint used_index; uint used_index= MAX_KEY;
bool need_sort= TRUE;
#ifndef NO_EMBEDDED_ACCESS_CHECKS #ifndef NO_EMBEDDED_ACCESS_CHECKS
uint want_privilege; uint want_privilege;
#endif #endif
...@@ -145,6 +146,11 @@ int mysql_update(THD *thd, ...@@ -145,6 +146,11 @@ int mysql_update(THD *thd,
send_ok(thd); // No matching records send_ok(thd); // No matching records
DBUG_RETURN(0); 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 running in safe sql mode, don't allow updates without keys */
if (table->quick_keys.is_clear_all()) if (table->quick_keys.is_clear_all())
{ {
...@@ -157,6 +163,7 @@ int mysql_update(THD *thd, ...@@ -157,6 +163,7 @@ int mysql_update(THD *thd,
} }
} }
init_ftfuncs(thd, &thd->lex->select_lex, 1); init_ftfuncs(thd, &thd->lex->select_lex, 1);
/* Check if we are modifying a key that we are used to search with */ /* Check if we are modifying a key that we are used to search with */
if (select && select->quick) if (select && select->quick)
{ {
...@@ -164,13 +171,15 @@ int mysql_update(THD *thd, ...@@ -164,13 +171,15 @@ int mysql_update(THD *thd,
used_key_is_modified= (!select->quick->unique_key_range() && used_key_is_modified= (!select->quick->unique_key_range() &&
check_if_key_used(table, used_index, fields)); 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) else if ((used_index=table->file->key_used_on_scan) < MAX_KEY)
used_key_is_modified=check_if_key_used(table, used_index, fields); used_key_is_modified=check_if_key_used(table, used_index, fields);
else else
{
used_key_is_modified=0; used_key_is_modified=0;
used_index= MAX_KEY;
}
if (used_key_is_modified || order) if (used_key_is_modified || order)
{ {
/* /*
...@@ -181,10 +190,11 @@ int mysql_update(THD *thd, ...@@ -181,10 +190,11 @@ int mysql_update(THD *thd,
if (used_index < MAX_KEY && old_used_keys.is_set(used_index)) if (used_index < MAX_KEY && old_used_keys.is_set(used_index))
{ {
table->key_read=1; 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 Doing an ORDER BY; Let filesort find and sort the rows we are going
...@@ -225,7 +235,11 @@ int mysql_update(THD *thd, ...@@ -225,7 +235,11 @@ int mysql_update(THD *thd,
DISK_BUFFER_SIZE, MYF(MY_WME))) DISK_BUFFER_SIZE, MYF(MY_WME)))
goto err; 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"; thd->proc_info="Searching rows for update";
uint tmp_limit= limit; uint tmp_limit= limit;
...@@ -251,6 +265,10 @@ int mysql_update(THD *thd, ...@@ -251,6 +265,10 @@ int mysql_update(THD *thd,
error= 1; // Aborted error= 1; // Aborted
limit= tmp_limit; limit= tmp_limit;
end_read_record(&info); 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 */ /* Change select to use tempfile */
if (select) if (select)
{ {
......
...@@ -132,6 +132,7 @@ typedef struct st_read_record { /* Parameter to read_record */ ...@@ -132,6 +132,7 @@ typedef struct st_read_record { /* Parameter to read_record */
byte *cache,*cache_pos,*cache_end,*read_positions; byte *cache,*cache_pos,*cache_end,*read_positions;
IO_CACHE *io_cache; IO_CACHE *io_cache;
bool print_error, ignore_not_found_rows; bool print_error, ignore_not_found_rows;
bool first; /* used only with rr_index_read */
} READ_RECORD; } READ_RECORD;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment