Partial and Logical Data Recovery
Most real data loss is partial and logical. Restoring the whole database over a live system is usually the wrong tool and often makes it worse.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
One table is wrong and the rest of the database is fine — now what?
Backups are designed for total loss, and total loss is rare. The common event is a subset of data made wrong by code or by a person, discovered later, while correct writes continue arriving on top of it.
We have backups. Restore last night's backup over production and we are back to a known-good state.
That discards every correct write made since the backup — usually far more data than the incident damaged. The cure is larger than the disease.
- That discards every correct write made since the backup — usually far more data than the incident damaged. The cure is larger than the disease.
- The damage often predates the most recent backup. Restoring the newest copy restores the corruption too.
- A full restore takes as long as the data volume demands, so a problem affecting one table becomes a full outage for the entire system.
- Downstream systems already consumed the bad data: caches, search indexes, analytics, invoices, emails already sent. Rolling back the database does not roll those back (Operating a Cache).
- Without knowing the exact scope and start time of the damage, any recovery is a guess, and a confident-looking one is worse than an honest uncertain one.
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- Logical corruption is not a storage failure. The database did exactly what it was told; the instruction was wrong. Nothing at the storage layer will detect it, which is why it survives replication and snapshots.
- The recovery shape is almost always: restore a copy aside, extract the correct subset, reconcile into the live system — not restore over the top.
- Two questions bound the whole operation: what is the blast radius in rows, and when did it start. Everything else is procedure.
- Point-in-time recovery is the tool that makes this tractable: a side restore to just before the damage gives you a queryable copy of the truth to diff against (Backup Operations).
- Reconciliation is application logic, not a database operation. Deciding which of two versions of a row wins requires knowing what the rows mean.
- Stopping the bleeding comes first. If the bad writer is still running, every minute enlarges the scope you will later have to reconcile (Stop the Harm Before You Understand It).
Not every data problem is a restore problem
The first job is classification, because the classes need different tools and choosing the wrong one wastes the time when scope is still growing.
| What happened | Is a full restore right? | What actually works |
|---|---|---|
| Storage or instance lost | Yes | Restore or promote a replica; this is the case backups were designed for |
| Table dropped, nothing else touched | Rarely | Side restore, extract the table, reload it, then reconcile rows written since |
| Column overwritten by a bad migration or job | No | Side restore to just before; diff and repair the affected column in batches |
| Rows deleted for one tenant | No | Side restore; extract that tenant's rows; reinsert with the original keys |
| Slow drift from a logic bug over weeks | No | Recompute from source of truth or event history; restore points may all contain the bug |
| Data leaked or wrongly retained | No | A deletion and audit problem, not a recovery problem (Audit Logs for Privileged Actions in Security) |
The partial recovery procedure
Every step exists because skipping it has caused a second, worse incident somewhere. The ordering matters most at the start: stopping the writer and preserving evidence come before anything that feels like progress.
- 1Stop the bleeding
Disable the job, revert the deploy, revoke the credential, flip the flag off.
fails by Recovery starts while damage continues, so the scope keeps moving.
evidence The write rate of the damaging pattern drops to zero on a dashboard.
- 2Preserve evidence
Snapshot the current, damaged state before touching it.
fails by A hasty repair destroys the only record of what the damage looked like.
evidence A snapshot identifier recorded in the incident timeline.
- 3Scope
Determine which rows, which columns, and the start instant, from the data and the change record.
fails by Guessing; a correction that is too wide damages good data, too narrow leaves the incident half-fixed.
evidence A query that returns the affected set with a count, agreed by two people.
- 4Side restore
Restore to a separate instance at a point just before the damage began.
fails by Chosen point falls inside the damage window; or quota, network or key problems block the restore (Restore Drills).
evidence A spot check on the side copy confirms known-good values for affected rows.
- 5Diff
Compare side copy and live for the scoped set; classify differences into damage and legitimate later writes.
fails by Treating every difference as damage, which overwrites correct new work.
evidence A reviewed diff summary with counts per category.
- 6Dry run
Execute the correction with writes disabled and report what it would change.
fails by Skipped under time pressure — the most expensive shortcut in this procedure.
evidence Dry-run output matching the expected count from the scope step.
- 7Repair in batches
Apply the correction in bounded, resumable, idempotent batches.
fails by A single unbounded statement that locks the table, saturates the database, or cannot be stopped.
evidence Per-batch records of rows changed; live error and latency signals stay flat (Operating a Production Database).
- 8Repair downstream
Invalidate caches, reindex search, recompute derived data, handle external side effects.
fails by Database correct, product still wrong, users still seeing stale values.
evidence Reads through the application return corrected values.
- 9Assert and record
Run invariants, then write down exactly what changed.
fails by Nobody can reconstruct the final state during the postmortem.
evidence Invariant checks pass; a change record exists (Postmortems).
The correction is the dangerous part
By the time you are repairing, the original bug is understood and the pressure is to move fast. This is where a data incident becomes an outage, because a correction is a large write against a production database with a very human author.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Unbounded UPDATE on a large table | Locks and replication lag; the application times out | A single long transaction touching far more rows than intended | Batch with explicit bounds and pauses; watch lag and error rate between batches |
| Restore over the live database | Hours of correct writes gone | Total restore used for partial damage | Side restore and reconcile; keep the live system serving |
| Recovery point chosen from memory | The "clean" copy contains the corruption | Damage start time was assumed rather than established from data | Bracket the start from the data, verify known-good values on the side copy before diffing |
| Database repaired, cache untouched | Users still see wrong values after the all-clear | Derived state holds the bad values with its own lifetime | Invalidate or flush the affected keys as an explicit step (Operating a Cache) |
| Repair script re-run after an interruption | Double-applied corrections | The correction was not idempotent | Track repaired rows explicitly; make re-running a no-op |
UPDATE orders SET status = 'paid' WHERE status = 'pending'; -- no bound, no dry run, no batches, no record of prior values
-- 1. scope, agreed and counted first
CREATE TABLE repair_scope AS
SELECT o.id, o.status AS old_status, s.status AS good_status
FROM orders o
JOIN side_restore.orders s USING (id)
WHERE o.updated_at >= :damage_start
AND o.updated_at < :damage_end
AND o.status <> s.status;
-- 2. dry run: this count must match the agreed scope
SELECT count(*) FROM repair_scope;
-- 3. repair in bounded, resumable batches
UPDATE orders o
SET status = r.good_status, updated_at = now()
FROM (SELECT * FROM repair_scope
WHERE NOT repaired ORDER BY id LIMIT :batch) r
WHERE o.id = r.id;
-- mark repaired, pause, re-check live signals, continueThe left statement has no bound, no record of what it changed, no way to stop halfway and no way to undo. The right one keeps the prior value for every touched row, so the repair itself is reversible; it matches a count agreed in advance, so a scope error is caught before any write; and it yields between batches, so the database keeps serving traffic while the repair runs.
How to do it properly
Most important first.
- Stop the source of damage before anything else — disable the job, revert the deploy, revoke the credential, turn off the flag.
- Establish scope and start time from the data itself and the change record: which rows, which columns, from which timestamp, by which actor (The Audit Trail).
- Restore to a side copy at a point just before the damage began. Never restore over the live system while a partial recovery is still viable.
- Diff and reconcile deliberately, in batches, with a dry run first and a preserved record of what each batch changed.
- Repair downstream consumers explicitly: invalidate caches, reindex search, recompute derived data, and decide what to do about actions already taken in the outside world.
- Prefer soft deletes and append-only history for high-value data — it converts a recovery operation into a query (Event Sourcing in Architecture).
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
Scope discipline contains it: a bounded, dry-run, batched correction affects only the rows you named. An unbounded UPDATE issued in a hurry does not stop at the tenant, and the recovery becomes the larger incident.
What can go wrong
- The recovery script itself corrupts more data, because it ran against the wrong scope with no dry run.
- Reconciliation overwrites correct new writes with restored old values, creating a second, harder-to-detect incident.
- The chosen recovery point is inside the damage window, so the "clean" copy is not clean.
- The damage is fixed in the database and not in the search index, cache or warehouse, so the system continues to serve wrong answers.
- "We have backups, so any data problem is solved." Backups solve total loss. Partial logical damage needs a scope, a point in time, a side copy and reconciliation.
- "Restore the latest backup" is the reflex and is usually wrong twice: it may contain the damage, and it discards correct writes made since.
- "The database is fixed, so the incident is over." Derived stores and external side effects are part of the system's state.
- "Point-in-time recovery means we can undo anything." It means you can reconstruct state at an instant. Merging that with everything correct that happened since is still your problem.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- A row count and a scope definition for the damage, agreed before the correction runs.
- A dry run whose output was reviewed, followed by batch-by-batch records of what changed.
- Post-recovery assertions that pass on the live system: referential integrity, invariants, and spot checks against the side copy.
- Downstream systems confirmed consistent — cache invalidated, index rebuilt, derived data recomputed.
- The preserved snapshot of the damaged state is the rollback. Take it before you start; without it, a bad reconciliation is unrecoverable.
- Some of it cannot be rolled back at all — emails sent, payments captured, webhooks delivered. Those need a business remedy, not a technical one, and should be named early (Telling People What Is Happening).
- Automate: side restores to a point in time, diff generation between the side copy and live, batched and resumable correction jobs, and post-recovery assertions.
- Automate the guardrails hardest — dry-run-by-default, a required row-count bound, and a refusal to run without an explicit scope.
- Keep human: the choice of recovery point, the reconciliation policy where old and new disagree, and the decision to accept remaining inconsistency rather than chase it.
- A side restore costs infrastructure and time before any repair begins, and is almost always faster overall than a full restore plus re-doing lost work.
- Batched, reviewed correction is slower than a single statement and is the difference between a recoverable mistake and an unrecoverable one.
- Append-only and soft-delete designs make recovery dramatically easier and cost storage, query complexity and privacy-deletion work forever after.
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- DATABASE-SPECIFICWhether you can restore to an arbitrary instant depends on continuous log archival being configured and unbroken; some engines and managed tiers offer only periodic snapshots, which forces you to the nearest copy and widens the reconciliation work. Row-level history via versioned tables or change data capture, where available, changes this lesson from a restore into a query.
- CLOUD-SPECIFICManaged point-in-time recovery typically restores into a *new* instance, which is exactly what you want here — but it means paying for a second instance during recovery and having the quota and network configuration to launch it. Verify that path in a drill; it is often where the recovery stalls.
- GENERALThe shape — stop the bleeding, preserve evidence, scope, side restore, reconcile in batches, repair downstream — applies to object storage, search indexes and message state as well as to relational data.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.
- — Distributed Systems — reconstructing a consistent state across shards or services when the damage crossed a boundary, and why a per-service restore point is not a system-wide one.