0

In my Django web application, I had a user delete a few records that seemed safe to delete. Turns out they were connected via ForeignKeyField fields to a fairly large set of other, highly important records. I now know that I can manage how deletes are handled, but this knowledge comes after this event occurred.

My main question is: is there an easy way to fix this or is it simply a matter of meticulously restoring each record, one by one, from backups?

More Details

Every night, I do a backup of the MySQL database using mysqldump. So I have a backup of all the data the day before this happened. The problem is that these backup files will restore a database in full. Given that we didn't notice the problem for a week or so, restoring the whole database I think is not an option since it would override other legitimate changes from the day the deletes happened to now.

I think my only option is to manually, one by one, pick out the records from the MySQL dump file, and manually INSERT them back into the MySQL DB. This feels like a bad idea since it's heavily prone to human error - my own typing.

Is this the only way or is there a better way?!

Garfonzo
  • 3,876
  • 6
  • 43
  • 78

1 Answers1

1

The approach I would take would be restore a backup to a different database. (The way we have our nightly mysqldump job configured, a separate dump for each database, and the SQL in the dump file doesn't contain any reference to the database name, so it's easy for us to create new database, e.g.

CREATE DATABASE restore_YYYYMMDD_dbname ;

And then run the gzipped mysqldump into the new "restore" database:

gunzip -c dbname.backup_YYYMMDD.sql.gz | \
  mysql u root -pSECRET -c --database restore_YYYYMMDD_dbname 

Obviously, we need sufficient disk space, and that will crank for however long it cranks.

Then I can write SQL to discover the deleted rows. Since we have a unique id column as a PRIMARY KEY in almost every table, we just use an anti-join to find rows in the restored table which don't have a corresponding row in the current database table

For example:

SELECT r.*
  FROM restore_YYYYMMDD_dbname.mytable r
  LEFT
  JOIN dbname.mytable t
    ON t.id = r.id
 WHERE t.id IS NULL

We may not want to restore every one of those rows, we can tweak the query to add some additional predicates to the WHERE clause to get it down to the rows we actually want. Then we can use that query as a source for an INSERT ... SELECT

INSERT INTO dbname.mytable 
SELECT r.*
  FROM ...

We have to do each table in the right order, so we don't violate foreign key constraints. (We could use SET FOREIGN_KEY_CHECKS=0, if we're sure we know what we're doing; but it's safer just to do the operations in the right order.

Finding "changed" rows is a little more complicated than the deleted rows, but we can do the same thing, writing queries to do that as well.


The way we have our mysqldump process setup, it's pretty straightforward to do this. It's still a manual process, but we let SQL do a lot of the tedious work for us.

If you've not tested restoring your database from a mysqldump to a different database, you may want to test that in a DIFFERENT environment first, so you don't inadvertently mess up the current database.

spencer7593
  • 106,611
  • 15
  • 112
  • 140
  • This is a great idea that I'd not thought about - doing it all within the database itself. I was trying to write Python scripts to extract it from the backup files themselves (painful and felt unreliable). I do have a development server that I do all my testing on, before doing it live on the production database. I have felt the sting of restoring a backup file to the *production* database, thinking I was restoring to a different database. So, I'll certainly be removing the reference to the database in the backup file. Thanks for the response, I feel I have hope now! – Garfonzo May 20 '15 at 02:36
  • @Garfonzo: We went through some gyrations several years ago to get the options on the `mysqldump` right, to make sure no timezone conversions happen, to make sure the database name doesn't appear anywhere in the SQL, make sure we get a "consistent" backup, and so on. That's all specifically designed so that we can "clone" the database, we can take that backup file anywhere, and restore that database to any database name we want. Including being able to restore to a new database on the same MySQL server. – spencer7593 May 20 '15 at 02:47
  • ... we still have to write the SQL to "find" the missing rows, and that can be tedious. But once we get a SELECT statement that gets us the rows we want, inserting those back to the original table is easy. (Most of the tables don't have triggers, and most of the triggers we do have on tables are mostly just inserting to audit tables.) – spencer7593 May 20 '15 at 02:50
  • Thanks for the great info, it's very helpful on my end. I've been hit with hardware failure which required DB restores - but that was easy with a full restore. Now that I'm getting into more detailed situations (single records being deleted, cascading deletes wiping out chunks of data) I'll need to refine my backup scripts, as you mentioned, so that they aren't tied to a specific DB. It will make future scenarios simpler. Thanks again for the help! – Garfonzo May 20 '15 at 02:55
  • 1
    FYI - here are the options we use on our `mysqldump` that we run for each separate database **`--insert-ignore --single-transaction --quick --tz-utc --routines --no-create-db --skip-add-drop-table $DB`**. (We run a query to get a list of database names. and then run a separate mysqldump for each database `$DB`. – spencer7593 May 20 '15 at 14:00