Did you just run an UPDATE against your 10 million row users table without a WHERE clause? Did you know that in MySQL 5.5 that sometimes you can recover from a bad UPDATE statement? This is possible if you are running in binlog_format=ROW !
Imagine this scenario:
1 2 3 4 5 6 | CREATE TABLE `t1` ( `c1` int(11) NOT NULL AUTO_INCREMENT, `c2` varchar(10) NOT NULL, PRIMARY KEY (`c1`) ) ENGINE=InnoDB; INSERT INTO `t1` (`c2`) VALUES ('michael'), ('peter'), ('aamina'); |
We run an accidental UPDATE statement that changes a row:
1 | UPDATE `t1` SET `c2` = 'tom' WHERE `c1` = 2; |
If we examine this UPDATE using the Binary Logging format of STATEMENT the entry would look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # at 464 #121019 16:10:42 server id 1 end_log_pos 532 Query thread_id=1 exec_time=0 error_code=0 SET TIMESTAMP=1350677442/*!*/; BEGIN /*!*/; # at 532 #121019 16:10:42 server id 1 end_log_pos 638 Query thread_id=1 exec_time=0 error_code=0 SET TIMESTAMP=1350677442/*!*/; UPDATE `t1` SET `c2` = 'tom' WHERE `c1` = 2 /*!*/; # at 638 #121019 16:10:42 server id 1 end_log_pos 665 Xid = 19 COMMIT/*!*/; |
Now let’s see what it looks like if you were using the Binary Logging format of ROW:
1 2 3 4 5 6 7 8 9 | # at 665 #121019 16:12:34 server id 1 end_log_pos 733 Query thread_id=1 exec_time=0 error_code=0 SET TIMESTAMP=1350677554/*!*/; BEGIN /*!*/; # at 733 # at 777 #121019 16:12:34 server id 1 end_log_pos 777 Table_map: `test`.`t1` mapped to number 39 #121019 16:12:34 server id 1 end_log_pos 827 Update_rows: table id 39 flags: STMT_END_F |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | BINLOG ' MrSBUBMBAAAALAAAAAkDAAAAACcAAAAAAAEABHRlc3QAAnQxAAIDDwIKAAA= MrSBUBgBAAAAMgAAADsDAAAAACcAAAAAAAEAAv///AIAAAADdG9t/AIAAAAFdmFkaW0= '/*!*/; ### UPDATE test.t1 ### WHERE ### @1=2 /* INT meta=0 nullable=0 is_null=0 */ ### @2='tom' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ ### SET ### @1=2 /* INT meta=0 nullable=0 is_null=0 */ ### @2='vadim' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ # at 827 #121019 16:12:34 server id 1 end_log_pos 854 Xid = 21 COMMIT/*!*/; |
Of interest is that the old and new versions of the changed column is stored in the binary log — in this example in @2.
This example looks trivial to fix when it is just one row changed — you don’t need anything fancy to recover from this failure case and can just run another UPDATE reversing the old and new values and be done. But like I started out saying, what if this UPDATE statement affected all rows in your very important big table? It might look more like this:
1 | UPDATE `t1` SET `c2` = 'matt'; |
And when we examine the binary log (still in binlog_format=ROW):
1 2 3 4 5 6 7 8 9 | # at 854 #121019 16:13:38 server id 1 end_log_pos 922 Query thread_id=1 exec_time=0 error_code=0 SET TIMESTAMP=1350677618/*!*/; BEGIN /*!*/; # at 922 # at 966 #121019 16:13:38 server id 1 end_log_pos 966 Table_map: `test`.`t1` mapped to number 39 #121019 16:13:38 server id 1 end_log_pos 1062 Update_rows: table id 39 flags: STMT_END_F |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | BINLOG ' crSBUBMBAAAALAAAAMYDAAAAACcAAAAAAAEABHRlc3QAAnQxAAIDDwIKAAA= crSBUBgBAAAAYAAAACYEAAAAACcAAAAAAAEAAv///AEAAAAHbWljaGFlbPwBAAAABG1hdHT8AgAA AAV2YWRpbfwCAAAABG1hdHT8AwAAAAZhYW1pbmH8AwAAAARtYXR0 '/*!*/; ### UPDATE test.t1 ### WHERE ### @1=1 /* INT meta=0 nullable=0 is_null=0 */ ### @2='michael' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ ### SET ### @1=1 /* INT meta=0 nullable=0 is_null=0 */ ### @2='matt' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ ### UPDATE test.t1 ### WHERE ### @1=2 /* INT meta=0 nullable=0 is_null=0 */ ### @2='vadim' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ ### SET ### @1=2 /* INT meta=0 nullable=0 is_null=0 */ ### @2='matt' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ ### UPDATE test.t1 ### WHERE ### @1=3 /* INT meta=0 nullable=0 is_null=0 */ ### @2='aamina' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ ### SET ### @1=3 /* INT meta=0 nullable=0 is_null=0 */ ### @2='matt' /* VARSTRING(10) meta=10 nullable=0 is_null=0 */ # at 1062 #121019 16:13:38 server id 1 end_log_pos 1089 Xid = 22 COMMIT/*!*/; |
As an exercise to the reader you would then need to:
- Stop database activity (set read_only or put up a holding page on your web server, etc) so that changes are no longer hitting your database. You need time to think.
- Take a database backup if you can.
- Identify which binary log has the offending UPDATE statement.
- Parse the binary log using mysqlbinlog -vv and match on the old version of @1 to the new version of @1, old version of @2 to the new version of @2, etc.
- Reverse the entries row entries and create new UPDATE statements that filter on the new version and SET the old version.
I hope this helps someone out there!
Note: this recovery example ONLY works when binlog_format=ROW — MIXED doesn’t help as in my testing as MySQL chooses to binlog UPDATEs in STATEMENT format when in MIXED mode.


Hi, I add this feature for mysqlbinlog, you can see: http://bugs.mysql.com/bug.php?id=65178, or http://mysql.taobao.org/index.php/Patch_source_code#Add_flashback_feature_for_mysqlbinlog
This patch is based Percona 5.5.18
I add a option “-B/–flashback” for mysqlbinlog, it can do the same thing like you said.
you can try my patch.
very good!
Thanks for the link P. Linux! Have you tried to submit this feature request to Percona Server as well?
https://launchpad.net/percona-server
Hi Michael ,
I have submitted to MySQL bug system.
I open a feature request to Percona Server now, Thanks!
yes, this is a quite obvious exploitation of the fact that row based binlog stores the before image of a modified (deleted or updated) row in the binlog.
However this will soon be history. MySQL 5.6 adds a new variable binlog_row_image:
http://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html#sysvar_binlog_row_image
When this is set to “minimal”, the binlog will only contain the PK field of the before image (assuming each table has a PK, which is pretty reasonable).
This new behavior is meant as an optimization, which it certainly is. Just imagine a mass delete from a wide table. Full before image would need much more I/O bandwidth for logging and log shipping than logging just the PK. One however must be aware that enabling this new feature has side effects.
PS: for MySQL cluster the above trick will not work anyway. It defaults to log minimal before images (–ndb-log-updated-only defaults to TRUE) and what’s more: it defaults to log updates as inserts (–ndb-log-update-as-write defaults to TRUE). See http://dev.mysql.com/doc/refman/5.5/en/mysql-cluster-replication-conflict-resolution.html