Saturday, August 25, 2007

how to undelete rows from a fixed length myisam table

You have inadvertently deleted some rows from your table and want them back. This is semi-doable with fixed row format of MyISAM. I put together a few steps, which I'll compliment with a program that does it for you, later.

  • save output of SHOW TABLE STATUS LIKE 't1' and SHOW CREATE TABLE `t1`.
  • shutdown mysql server asap!
  • backup t1.frm, t1.MYI, t1.MYD immediately
  • create a new table t1_recover, which has no auto-inc and no unique/pk.
  • remove the files t1_recover.MYD and t1_recover.MYI
  • write a C program that scans through t1.MYD reading blocks of length Avg_row_length and checking if first byte indicates a row is marked as deleted.
  • if it's deleted, dump it as is into the file t1_recover.MYD
  • goto mysql prompt. issue REPAIR TABLE `t1_recover` USE_FRM
Some notes.

You cannot recover entire record. You'll lose first 6 bytes of each record (or how every long data pointer length is). Typically, this is the first 2 int columns trashed.

You cannot determine which columns contained NULL in the recovered records (assuming some columns are nullable).

If you anticipate to have to use this recovery method often, then you can create a fake char(9) column at the beginning of the table, so that when a row is deleted, this column will be overridden instead of your real data..

Here is the code! myisam undeleter

Example usage below:
1) create a table and delete some records:

mysql> create table mydel(id int, name char(15), address char(15), bday datetime, daysleft decimal(10,5));
Query OK, 0 rows affected (0.06 sec)

mysql> insert into mydel values (1,'shane','down south','2005-09-01',10.5);
Query OK, 1 row affected (0.01 sec)

mysql> insert into mydel values (2,'susan car','up north','2001-02-01',14.1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into mydel values (3,'bobby scott','chinatown','1976-12-21',55.5);
Query OK, 1 row affected (0.00 sec)

mysql> delete from mydel where id=2 or id=3;
Query OK, 2 rows affected (0.00 sec)

mysql> flush tables;
Query OK, 0 rows affected (0.00 sec)

mysql> show table status;
| Name | Engine | Row_format | Rows | Avg_row_length | Data_length |
| mydel | MyISAM | Fixed | 1 | 49 | 147 |

2) next, build and run the app:

> gcc -o undeleter -lm ./undeleter.c
> ./undeleter /5.1/data/test/mydel.MYD /5.1/data/test/mydel_recover.MYD 49
allocating a block of memory for records, 49 bytes
starting to read records
found deleted record at offset 49
found deleted record at offset 98
records copied into '/5.1/data/test/mydel_recover.MYD': 2
records present in '/5.1/data/test/mydel.MYD': 1

next steps:
1. remove the MYI for '/5.1/data/test/mydel_recover.MYD'
2. make sure '/5.1/data/test/mydel_recover.MYD' definition .frm is created without any unique or PK keys
3. repair table use_frm

3) next, sort out the recovered table

sbester@www:/5.1/data/test> cp ./mydel.frm ./mydel_recover.frm

mysql> repair table mydel_recover use_frm;
| Table | Op | Msg_type | Msg_text |
| test.mydel_recover | repair | warning | Number of rows changed from 0 to 2 |
| test.mydel_recover | repair | status | OK |
2 rows in set (0.00 sec)

mysql> select * from mydel_recover;
| id | name | address | bday | daysleft |
| -1 | san car | up north | 2001-02-01 00:00:00 | 14.10000 |
| 0 | ☺bby scott | chinatown | 1976-12-21 00:00:00 | 55.50000 |
2 rows in set (0.00 sec)

Notice! I just cp the .frm. That's only because I didn't need to strip out the
PK or auto-increment column. As you seen, `id` is trashed. `name`
is partially trash. the `id` would have caused alot of duplicate key
errors if you still had a PK.

More information here: MySQL_Internals_MyISAM

1 comment:

Anonymous said...
This comment has been removed by a blog administrator.