powershelldba.de · Uwe Janke

Differential Backup After a COPY_ONLY Full: How the Restore Chain Really Works

A 12 AM full backup gets corrupted. Hours later, after a COPY_ONLY full and a fresh differential were taken, a restore using only the copy-only full plus that differential succeeds anyway — even though the differential was never based on the copy-only backup. Here is what SQL Server is actually checking, with real FirstLSN, CheckpointLSN, DatabaseBackupLSN and DifferentialBaseLSN values from a live restore test.

The Scenario

A fairly ordinary backup schedule:

Now the 12:00 full backup is corrupted. The obvious, textbook-safe restore path is: COPY_ONLY full → every log backup since. That always works, because it never needs a differential at all. But a second, less obvious sequence also restores cleanly:

COPY_ONLY full (05:00) → log backup (06:00) → differential backup (later) → log backup (later)

That should not work. A COPY_ONLY full backup is explicitly documented to not reset the differential base — meaning that "later" differential is still based on the 12:00 full, the one backup file that never enters this restore chain at all. So how does SQL Server validate it?

differential base differential base — inherited through the COPY_ONLY full 12:00 FULL corrupted 01:00 LOG 02:00 DIFF 03:00 LOG 04:00 LOG 05:00 COPY_ONLY FULL used in restore 06:00+ LOG LATER DIFF used in restore LATER LOG differential base pointer (metadata only, no data needed) backups actually read during this restore

Both differentials point back to the 12:00 full, never to the 05:00 COPY_ONLY full — yet restoring the copy-only full plus the later differential still succeeds, because the pointer itself, not the corrupted file, travels forward.

Two Independent Checks, Not One

It is easy to assume there is a single "backup chain" that RESTORE validates. In reality there are two separate, unrelated checks, and this scenario only makes sense once they are pulled apart.

1. Log chain continuity (FirstLSN / LastLSN)

Every log backup records a FirstLSN and LastLSN. To apply one, its FirstLSN must line up with wherever the database's redo currently stands. This check is purely sequential and has nothing to do with which full or differential backup sits underneath — it is why "COPY_ONLY full → every log since" always works on its own.

2. Differential base match (DatabaseBackupLSN + GUID)

A differential backup only applies on top of the exact full backup it was based on. That relationship is recorded, not inferred: every backup stamps a DatabaseBackupLSN (visible via RESTORE HEADERONLY, and as database_backup_lsn in msdb.dbo.backupset) plus a differential_base_guid. When a differential is restored, SQL Server compares those values against what the currently restored database is carrying — not against whichever full backup file happens to be sitting on disk.

What COPY_ONLY Really Does to the Differential Base

Every database tracks one "current differential base" pointer (visible via DBCC DBINFO as dbi_differentialBaseLSN / dbi_differentialBaseGuid). A regular full backup does two things: it copies all the data, and it resets that pointer to itself, becoming the new base for every differential taken afterward.

A COPY_ONLY full backup captures exactly the same data — but by design it does not reset that pointer. Microsoft's own description of the feature is understated but precise: it exists to take a backup "without affecting the overall backup and restore procedures for the database." Two consequences follow, and only the first one is generally well known:

The second point is the whole answer. Restoring the 05:00 COPY_ONLY full does not tell the target database "your base is me." It tells it "your base is still the 12:00 full" — a backup file the restore sequence never touches, because the pointer travels as metadata inside the copy-only backup, independent of whether the original file still exists.

Proof: Real Values From a Restore Test

Theory is cheap; here are the actual numbers from reproducing this exact nine-step sequence on a SQL Server 2022 instance and querying msdb.dbo.backupset:

Backup is_copy_only checkpoint_lsn database_backup_lsn differential_base_guid
Full — 12:00 0 …276… 0 —
Diff — 02:00 0 …334… …276… 42f9dccd…
COPY_ONLY Full — 05:00 1 …361… (own) …276… (still 12:00!) —
Diff — later 0 …381… …276… (still 12:00!) 42f9dccd… (same as 02:00 diff)

The COPY_ONLY full's own checkpoint_lsn is 361, its own point in time. But its database_backup_lsn — the base it records itself as carrying — is still 276, the 12:00 full. The later differential, taken well after the copy-only full existed, is stamped with the identical base LSN and the identical GUID as the 02:00 differential. It never references the copy-only backup at all.

The Restore Itself

-- Never references the corrupted 12:00 full anywhere below:

RESTORE DATABASE MyDb
  FROM DISK = 'DiffDemo_06_CopyOnlyFull.bak'
  WITH NORECOVERY;

RESTORE LOG MyDb
  FROM DISK = 'DiffDemo_07_Log.trn'
  WITH NORECOVERY;

RESTORE DATABASE MyDb                 -- the differential, based on the
  FROM DISK = 'DiffDemo_08_Diff.bak'  -- 12:00 full, restores cleanly
  WITH NORECOVERY;                    -- against the copy-only chain

RESTORE LOG MyDb
  FROM DISK = 'DiffDemo_09_Log.trn'
  WITH RECOVERY;
✅ Result: zero errors through all four steps, and the restored database's row count matched the live source exactly. The corrupted 12:00 full backup file was never opened, never referenced, and was not needed for the restore to be both valid and correct.

Correctness, not just permission, follows from how a differential backup is built: it is cumulative, holding the latest version of every page changed since the base, as of its own backup time — not as of the intermediate copy-only full. Restoring it on top of the copy-only full simply overwrites those pages with their more current state. Which specific full backup file's bytes happen to be on disk never enters into it; only the LSN and GUID bookkeeping has to line up.

The Boundary Case

⚠️ What breaks this: the moment a regular, non-COPY_ONLY full backup runs, it resets the differential base pointer to itself. Any differential still holding the old base GUID will then fail to restore against it with a differential-backup-mismatch error. COPY_ONLY's entire value is that it is the one type of full backup that never does this — take an out-of-band full for a dev refresh, an offsite copy, or a one-off audit, and the differential chain around it stays completely undisturbed.

Practical Takeaway

The one lesson worth carrying forward: COPY_ONLY is not "a full backup that behaves slightly differently." It is a full backup that is deliberately invisible to the differential chain, in exactly the way that makes this restore sequence work.

← Back to Blog