Reading SQL Server File Headers with DBCC FILEHEADER

Page content

I’ve been doing a deep dive into SQL Server on-disk structures lately, and one of my favorite rabbit holes is revisiting Paul Randal’s series on file header pages. If you haven’t read it, go do that now. It covers what file header pages are, what they contain, and what happens when they corrupt. This post takes that concept and runs with it. I’ll use DBCC FILEHEADER to read the file header of every user database file on a server and answer a question that comes up more than you’d think: can you determine which files belong together as a database purely from the file header, without querying sys.databases?

The short answer is yes, and the field that makes it possible is BindingId. Let’s dig in.

What Is a File Header Page?

Page 0 of every SQL Server data file is reserved as the file header page. It holds metadata about the file itself: logical name, size, filegroup, growth settings, LSN values, and the GUID that binds the file to its database.

You can read it with DBCC PAGE against page 0, but DBCC FILEHEADER gives you a cleaner, already-decoded output without digging through hex. The command takes a database name or ID and a file ID:

DBCC FILEHEADER (N'TestDB1', 1) WITH NO_INFOMSGS;

The result is a single wide row with every field from the file header already decoded for you. Here are the key columns from that output:

FileId  LogicalName  BindingId                             
------  -----------  ------------------------------------  
1       TestDB1      DCADD8A7-406E-4095-8B62-22F57A07EF33  

Key Fields in the File Header

The full output is 38 columns wide, but the three shown above are the ones we care about for this post:

  • FileId: The file’s ID within the database. File 1 is always the primary data file.
  • LogicalName: The logical name of the file as SQL Server knows it, independent of the physical filename on disk.
  • BindingId: A GUID that identifies which database this file belongs to. Every file in the same database shares the same BindingId. This is how SQL Server verifies file membership during attach and recovery.

Looking at all the File Headers on an Instance

For this post I’m running SQL Server 2025 CU3 in a Docker container with two user databases: TestDB1 (an mdf, a secondary ndf, and a log) and TestDB2 (mdf and log). That gives us 5 files across 2 databases. Here’s the commands to read the file header for each file on my instance.

DBCC FILEHEADER (N'TestDB1', 1) WITH NO_INFOMSGS;
DBCC FILEHEADER (N'TestDB1', 2) WITH NO_INFOMSGS;
DBCC FILEHEADER (N'TestDB1', 3) WITH NO_INFOMSGS;
DBCC FILEHEADER (N'TestDB2', 1) WITH NO_INFOMSGS;
DBCC FILEHEADER (N'TestDB2', 2) WITH NO_INFOMSGS;

Here’s what the output of DBCC FILEHEADER for each database shows: all three TestDB1 files — the primary data file, the secondary NDF, and the log — share the same GUID. TestDB2 has its own. That GUID is your grouping key.

BindingId Database Files
DCADD8A7-406E-4095-8B62-22F57A07EF33 TestDB1 3
162F268E-5D83-4667-9CE7-FDCB8DFE9CC1 TestDB2 2

Reading BindingId Directly from the File

Here’s the scenario that makes this really useful in a DR situation: SQL Server is down, you have a directory of .mdf, .ndf, and .ldf files, and you need to figure out which ones belong together before you can attach anything.

The BindingId is stored in the raw file at page 0 (the file header page, type 15) at byte offset 247. SQL Server encodes GUIDs with the first three components in little-endian order and the last two components as a straight byte array. This PowerShell function reads it directly, no SQL Server required:

function Read-BindingId {
    param([string]$Path)

    if (-not (Test-Path -Path $Path -PathType Leaf)) {
        throw "$Path : not a file"
    }

    $bytes     = [System.IO.File]::ReadAllBytes($Path)
    $pageType  = $bytes[1]

    if ($pageType -ne 15) {
        throw "$Path : page 0 type $pageType, expected 15 (FileHeader)"
    }

    # First three GUID components are little-endian
    $d1 = [System.BitConverter]::ToUInt32($bytes, 247)
    $d2 = [System.BitConverter]::ToUInt16($bytes, 251)
    $d3 = [System.BitConverter]::ToUInt16($bytes, 253)

    # Last two components are raw bytes (big-endian)
    $d4 = $bytes[255], $bytes[256]
    $d5 = $bytes[257], $bytes[258], $bytes[259], $bytes[260], $bytes[261], $bytes[262]

    "{0:X8}-{1:X4}-{2:X4}-{3}-{4}" -f $d1, $d2, $d3,
        (($d4 | ForEach-Object { "{0:X2}" -f $_ }) -join ""),
        (($d5 | ForEach-Object { "{0:X2}" -f $_ }) -join "")
}

Get-ChildItem -Path /tmp -Include *.mdf,*.ndf,*.ldf -Recurse |
    Select-Object Name, @{ Name='BindingId'; Expression={ Read-BindingId $_.FullName } } |
    Sort-Object BindingId |
    Format-Table -AutoSize
Name              BindingId
----              ---------
TestDB1.mdf       DCADD8A7-406E-4095-8B62-22F57A07EF33
TestDB1_data2.ndf DCADD8A7-406E-4095-8B62-22F57A07EF33
TestDB1_log.ldf   DCADD8A7-406E-4095-8B62-22F57A07EF33
TestDB2.mdf       162F268E-5D83-4667-9CE7-FDCB8DFE9CC1
TestDB2_log.ldf   162F268E-5D83-4667-9CE7-FDCB8DFE9CC1

Group by GUID and you know exactly which files go together. I ran this against files I copied off the container while SQL Server was still running and got the same GUIDs that DBCC FILEHEADER returns. Same data, different path.

The One Thing BindingId Can’t Tell You

BindingId tells you which files belong together. It does not tell you the database name.

The database name lives in the boot page (page 9 of file 1), not the file header. So if you have a pile of detached files with no running SQL Server instance, you can absolutely figure out which files form a set using DBCC FILEHEADER alone. But to get the actual database name, you need DBCC DBINFO against the primary file, or you need to cross-reference with sys.databases. That distinction matters in DR scenarios when you’re trying to reconstruct which files belong together before you can attach anything.

Wrapping Up

DBCC FILEHEADER is one of those undocumented commands that’s been around forever and is incredibly useful for poking around the on-disk structures of your databases. BindingId is the field every DBA should have in their back pocket. It’s the glue that ties all of a database’s files together at the storage layer, and SQL Server uses it every time a database is opened to verify that the files actually belong. If you’re ever in a DR situation trying to piece together which files go with which database before you can attach anything, start here. Get out in your lab and try it.