在官方文档中replace的语义有这样一句话:”It either inserts, or deletes and inserts.”, 然而通过上面的实验可看出replace操作在row格式下的binlog中记录的并不是delete+insert操作,而是直接update。 那么是不是所有情况都是记录的update操作呢,可以实验观察:
sql_insert.cc: ... /* Check if there is more uniq keys after field */
staticintlast_uniq_key(TABLE *table,uint keynr) { /* When an underlying storage engine informs that the unique key conflicts are not reported in the ascending order by setting the HA_DUPLICATE_KEY_NOT_IN_ORDER flag, we cannot rely on this information to determine the last key conflict. The information about the last key conflict will be used to do a replace of the new row on the conflicting row, rather than doing a delete (of old row) + insert (of new row). Hence check for this flag and disable replacing the last row by returning 0 always. Returning 0 will result in doing a delete + insert always. */ if (table->file->ha_table_flags() & HA_DUPLICATE_KEY_NOT_IN_ORDER){ return0; } while (++keynr < table->s->keys){ if (table->key_info[keynr].flags & HA_NOSAME){ return0; } } return1; } ...
/* The manual defines the REPLACE semantics that it is either an INSERT or DELETE(s) + INSERT; FOREIGN KEY checks in InnoDB do not function in the defined way if we allow MySQL to convert the latter operation internally to an UPDATE. We also should not perform this conversion if we have timestamp field with ON UPDATE which is different from DEFAULT. Another case when conversion should not be performed is when we have ON DELETE trigger on table so user may notice that we cheat here. Note that it is ok to do such conversion for tables which have ON UPDATE but have no ON DELETE triggers, we just should not expose this fact to users by invoking ON UPDATE triggers. */ if (last_uniq_key(table,key_nr) && !table->file->referenced_by_foreign_key() && (!table->triggers || !table->triggers->has_delete_triggers())) { if ((error=table->file->ha_update_row(table->record[1], table->record[0])) && error != HA_ERR_RECORD_IS_THE_SAME) goto err; if (error != HA_ERR_RECORD_IS_THE_SAME) info->stats.deleted++; else error= 0; thd->record_first_successful_insert_id_in_cur_stmt(table->file->insert_id_for_cur_row); /* Since we pretend that we have done insert we should call its after triggers. */ goto after_trg_n_copied_inc; } else { ... } ...
I (Matz) am not sure of the reason for the last_uniq_key() check as, but I'm guessing that it's something along the following lines.
Suppose that we got the duplicate key to be a key that is not the last unique key for the table and we perform an update: then there might be another key for which the unique check will fail, so we're better off just deleting the row and inserting the correct row.
commit d9790a406c69a46eb197cea725c1e7c7e480ac41 Author: heikki@hundin.mysql.fi <> Date: Mon Feb 923:57:292004 +0200 Many files: Do not let REPLACE to perform internally an UPDATE if the table is referenced by a FOREIGN KEY: the manual says that REPLACE must resolve a duplicate key error semantically with DELETE(s) + INSERT, and not by an UPDATE; the internal update caused foreign key checks and cascaded operations to behave in a semantically wrong way ... - if (last_uniq_key(table,key_nr)) + /* + The manual defines the REPLACE semantics that it is either an INSERT or + DELETE(s) + INSERT; FOREIGN KEY checks do not function in the defined + way if we allow MySQL to convert the latter operation internally to an + UPDATE. + */ + + if (last_uniq_key(table,key_nr) + && !table->file->referenced_by_foreign_key()) { if ((error=table->file->update_row(table->record[1],table->record[0]))) goto err; ...
WL#1266"Separate auto-set logic from TIMESTAMP type." Final version of patch. Adds support for specifying of DEFAULT NOW() and/or ON UPDATE NOW() clauses for TIMESTAMP field definition. Current implementation allows only one such field per table and uses several unireg types for storing info about this properties of field. It should be replaced with better implementation when new .frm format is introduced. ... an INSERT or DELETE(s) + INSERT; FOREIGN KEY checks in InnoDB donot function in the defined way if we allow MySQL to convert the latter operation internally to an UPDATE. + We also should not perform this conversion if we have + timestamp field with ON UPDATE which is different from DEFAULT. */ if (last_uniq_key(table,key_nr) && - !table->file->referenced_by_foreign_key()) + !table->file->referenced_by_foreign_key() && + table->timestamp_default_now == table->timestamp_on_update_now) { if ((error=table->file->update_row(table->record[1], table->record[0]))) ...
commit 2511990c978137fe0bc81657e160a6d537bc957f Author: dlenev@brandersnatch.localdomain <> Date: Fri Oct 118:54:062004 +0400
Support for TIMESTAMP columns holding NULL values. Unlike all other column types TIMESTAMP is NOT NULL by default, so in order to have TIMESTAMP column holding NULL valaues you have to specify NULL as one of its attributes (this needed for backward compatibility). Main changes: Replaced TABLE::timestamp_default_now/on_update_now members with TABLE::timestamp_auto_set_type flag which is used everywhere for determining if we should auto-set value of TIMESTAMP field during this operation ornot. We are also use Field_timestamp::set_time() instead of handler::update_timestamp() in handlers. ... if (last_uniq_key(table,key_nr) && !table->file->referenced_by_foreign_key() && - table->timestamp_default_now == table->timestamp_on_update_now) + (table->timestamp_field_type == TIMESTAMP_NO_AUTO_SET || + table->timestamp_field_type == TIMESTAMP_AUTO_SET_ON_BOTH)) { if ((error=table->file->update_row(table->record[1], table->record[0])))
commit 007a20591892beb3734ed4d29fdfe28b750f435a Author: dlenev@brandersnatch.localdomain <> Date: Tue May 2422:19:332005 +0400
Fix for bugs: #5860"Multi-table UPDATE does not activate update triggers" #6812"Triggers are not activated for INSERT ... SELECT" #8755"Trigger is not activated by LOAD DATA". This patch also implements proper handling of triggers for special forms of insert like REPLACE or INSERT ... ON DUPLICATE KEY UPDATE. Also now we don't call after trigger in case when we have failed to inserted/update ordelete row. Trigger failure should stop statement execution. I have not properly tested handling of errors which happen inside of triggers in this patch, since it is simplier to dothis once we will be able to access tables from triggers. ... (table->timestamp_field_type == TIMESTAMP_NO_AUTO_SET || table->timestamp_field_type == TIMESTAMP_AUTO_SET_ON_BOTH)) { + if (table->triggers && + table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, + TRG_ACTION_BEFORE, TRUE)) + goto before_trg_err; if ((error=table->file->update_row(table->record[1], table->record[0]))) goto err; info->deleted++;
commit 6eee956d7538410f5fd5ee0f8216b2d89937950a Author: V Narayanan <v.narayanan@sun.com> Date: Thu Dec 316:48:022009 +0530
WL#4454 change sql_insert.cc::last_uniq_key to match keys in any order Introduce a flag that will enable the REPLACE command to work correctly with an underlying storage engine that does not report unique key conflicts in the ascending order. staticintlast_uniq_key(TABLE *table,uint keynr) { + /* + When an underlying storage engine informs that the unique key + conflicts are not reported in the ascending order by setting + the HA_DUPLICATE_KEY_NOT_IN_ORDER flag, we cannot rely on this + information to determine the last key conflict. + + The information about the last key conflict will be used to + do a replace of the new row on the conflicting row, rather + than doing a delete (of old row) + insert (of new row). + + Hence check for this flag and disable replacing the last row + by returning 0 always. Returning 0 will result in doing + a delete + insert always. + */ + if (table->file->ha_table_flags() & HA_DUPLICATE_KEY_NOT_IN_ORDER) + return0; + while (++keynr < table->s->keys) if (table->key_info[keynr].flags & HA_NOSAME) return0; ...
cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14
commit 0d466406d7e33e6c497e91f2865ce3d32cae916f Author: Martin Hansson <martin.hansson@oracle.com> Date: Tue Jan 3116:16:162012 +0100
WL#5874: CURRENT_TIMESTAMP as DEFAULT for DATETIME columns. ... if (last_uniq_key(table,key_nr) && !table->file->referenced_by_foreign_key() && - (table->timestamp_field_type == TIMESTAMP_NO_AUTO_SET || - table->timestamp_field_type == TIMESTAMP_AUTO_SET_ON_BOTH) && (!table->triggers || !table->triggers->has_delete_triggers())) { if ((error=table->file->ha_update_row(table->record[1], ...
从sql_insert.cc中关于last_uniq_key()的相关变更记录中可以看到在2000年的最初版本中即出现了如果last_uniq_key()则update的逻辑,但是作者并未说明这样做的意图,前文所述log_event.cc中的那段相关注释(“I (Matz) am not sure of the reason for the last_uniq_key() check as…”)第一次出现是在2005年的一次commit:
commit ad126d90e019f223470e73e1b2b528f9007c4532 Author: lars@mysql.com <> Date: Thu Dec 2206:39:022005 +0100
WL#1012: All changes as one single changeset. This includes both code and test cases. ... + + REPLACE is defined as either INSERT or DELETE + INSERT. If + possible, we can replace it with an UPDATE, but that will not + work on InnoDB if FOREIGN KEY checks are necessary. + + I (Matz) am not sure of the reason for the last_uniq_key() + check as, but I'm guessing that it's something along the + following lines. + + Suppose that we got the duplicate key to be a key that is not + the last unique key for the table and we perform an update: + then there might be another key for which the unique check will + fail, so we're better off just deleting the row and inserting + the correct row. + */ + if (last_uniq_key(table, keynum) && + !table->file->referenced_by_foreign_key()) + { + error=table->file->ha_update_row(table->record[1], + table->record[0]); + return error; + } ...
提到了update自增列时考虑更新auto_increment,但官方回复因存在并发、死锁相关问题且这样的场景太少,没被修复。 Update auto_increment column to last ID + 1 causes error on next insert https://bugs.mysql.com/bug.php?id=12434