The eight stages of DELETE:
- SELECT what you're deleting;
- fix the mistake in the query (there's always one);
- SELECT again;
- BEGIN TRANSACTION;
- DELETE;
- SELECT to confirm the right deletion -- twice for good measure, and maybe ROLLBACK once or twice if you're feeling twitchy;
- Hover finger anxiously over the un-entered COMMIT command for several seconds while resisting Dunning–Kruger effect;
- COMMIT. :)
Give the table an DeletedTimestamp and DeleatedReason column.
Mark the records as deleted.
Run around like a lunatic and fix queries so they don't show "deleted" columns to the rest of the app.
Make a page in the application where people can "restore" deleted items. Because they do stupid things too once in a while.
at least you need something like DELETE FROM foo WHERE deleted < NOW() - '30 days'::timedelta (or whatever the correct syntax is)
However, it won't save you if realize that there's an issue after you commit. Maybe I'm just paranoid but I like to have both a snapshot before a major change, plus I'm fond of having a spare (via delayed replication or log shipping).
Ideally, all SELECT queries are forced through a view layer (which already has an IsDeleted = 0 restriction), and you can just schedule a nightly job which deletes or archives all the rows with IsDeleted = 1, then refresh the table statistics automatically.
I like to have an intermediary table where I can verify what's going to be deleted (or updated) before it runs. Even better it lets you save the mappings of old ids so when you find out some downstream system was using them you can fix that too.
I know it's just an example but soft deletes solve a huge range of problems.