Proving the Restore, Part 9: Backups From a Different Database
Two backup files, same database name, same server name, LSNs that line up. They can still be from different databases.
Database names get reused. A database is restored under a new name, then renamed back. A refresh from production overwrites a test database, and last week’s backups from the old contents are still on the share. Names and dates won’t separate those; the header carries identifiers that will.

FamilyGUID and BindingID
Two uniqueidentifier columns describe database identity, and the difference between them is what makes the check work.[1]
FamilyGUID is the ID of the original database when it was created. It stays the same when the database is restored.
BindingID is the binding ID for the database, and a new value is assigned when a database is restored.
So FamilyGUID tracks lineage across restores while BindingID changes at each one. Backups sharing a FamilyGUID descend from the same original CREATE DATABASE. Backups with different FamilyGUID values came from databases that were separately created, regardless of what they’re called now.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
SELECT [h].[source_file] , [h].[DatabaseName] , [h].[BackupType] , [h].[BackupFinishDate] , [h].[FamilyGUID] , [h].[BindingID] FROM #header_results AS [h] ORDER BY [h].[FamilyGUID] , [h].[BackupFinishDate]; |
More than one distinct FamilyGUID in a set you intended to restore together means the set is mixed. Grouping makes that obvious:
|
1 2 3 4 5 6 7 8 9 10 |
SELECT [h].[FamilyGUID] , [backups] = COUNT(*) , [names] = COUNT(DISTINCT [h].[DatabaseName]) , [earliest] = MIN([h].[BackupFinishDate]) , [latest] = MAX([h].[BackupFinishDate]) FROM #header_results AS [h] GROUP BY [h].[FamilyGUID]; |
One row is what you want. Two rows with the same DatabaseName and different FamilyGUID values is the case worth catching, because nothing else in the filename or the dates would have told you.
Recovery forks
Lineage answers whether backups descend from the same database. It doesn’t answer whether they descend along the same branch of that database’s history, and restores create branches.
Recover a database to a point in time, bring it online, and everything after that recovery point is a new branch. Log backups taken from the original timeline and log backups taken after the recovery both trace back to the same FamilyGUID, but they describe divergent histories. Restoring across the divergence isn’t valid.
Three columns describe this.[1]
| Column | Meaning |
|---|---|
FirstRecoveryForkID |
ID for the starting recovery fork |
RecoveryForkID |
ID for the ending recovery fork |
ForkPointLSN |
The LSN of the fork point, when the two differ. NULL otherwise |
For data backups, FirstRecoveryForkID equals RecoveryForkID. A log backup that spans a fork point has different values in the two, and ForkPointLSN gives the LSN where the branch happened.
|
1 2 3 4 5 6 7 8 9 10 11 |
SELECT [h].[source_file] , [h].[BackupFinishDate] , [h].[FirstRecoveryForkID] , [h].[RecoveryForkID] , [h].[ForkPointLSN] FROM #header_results AS [h] WHERE [h].[ForkPointLSN] IS NOT NULL OR [h].[FirstRecoveryForkID] <> [h].[RecoveryForkID]; |
Rows here mean the set spans at least one recovery fork. That doesn’t make the backups useless, but it does mean the restore path has to stay on one branch, and working out which branch you want is a decision rather than a calculation.
msdb records the same information as first_recovery_fork_guid and last_recovery_fork_guid on backupset, so the check works against instance history as well as against the files.[2]
Counting distinct RecoveryForkID values across a set gives a quicker signal for automated checks:
|
1 2 3 4 |
SELECT [forks] = COUNT(DISTINCT [h].[RecoveryForkID]) FROM #header_results AS [h]; |
More than one is worth reporting.
Where this shows up
Restoring a copy alongside the original. Restore last night’s backup as Sales_test and back it up, and those backups share FamilyGUID with production while having their own BindingID. Lineage alone won’t separate them; the database name and the fork will.
A database refreshed from another environment. The target’s contents are replaced, so backups taken before and after the refresh have different FamilyGUID values under the same name. This is the case where checking lineage earns its keep, because both sets look plausible on disk.
A failed point-in-time recovery followed by a retry. Each recovery creates a fork. Backups taken between attempts belong to branches that were abandoned.
Adding it to the validation
Lineage and fork checks are cheap once the headers are in a table, and they catch a class of problem the LSN checks can’t see. A set with a broken log chain fails visibly. A set with two lineages and a clean chain within each looks fine until the restore rejects it.
Reasonable severities:
| Finding | Severity |
|---|---|
More than one distinct FamilyGUID |
error, the set is mixed |
More than one distinct RecoveryForkID |
warning, needs a decision about which branch |
ForkPointLSN not NULL on any backup |
warning, the set spans a fork |
DatabaseName differs within one FamilyGUID |
informational, expected after a restore under a new name |
The last one is worth reporting rather than suppressing. Seeing that the lineage includes backups taken under a different name is often the first clue about where a set came from.
Next
Lineage, continuity, and a matching differential base tell you the set is coherent. Whether it reaches the moment you actually want is a separate question, and part 10 covers proving that before you start.
Have you had two databases with the same name and different lineage on one share? I’d like to hear how it was untangled. Bluesky or LinkedIn.
References
- RESTORE HEADERONLY (Transact-SQL) – Microsoft Learn. Documents
FamilyGUIDas the ID of the original database, which stays the same when the database is restored,BindingIDas reassigned on restore, and theFirstRecoveryForkID,RecoveryForkID, andForkPointLSNcolumns. ↩ - backupset – Microsoft Learn. Records
first_recovery_fork_guidandlast_recovery_fork_guid, themsdbequivalents of the fork columns returned by the header. ↩