Недавно я глубоко погрузился в изучение дисковых структур SQL Server, и одно из моих любимых направлений — это перечитывание серии статей Пола Рэндала (Paul Randal) о страницах заголовков файлов. Если вы её не читали, сделайте это прямо сейчас. В ней рассказывается о том, что такое страницы заголовков файлов, что они содержат и что происходит при их повреждении. Эта статья развивает эту концепцию. Я буду использовать DBCC FILEHEADER для чтения заголовка каждого файла пользовательской базы данных на сервере и отвечу на вопрос, который возникает чаще, чем можно подумать: можно ли определить, какие файлы принадлежат одной базе данных, исключительно по заголовку файла, без обращения к sys.databases?
Короткий ответ — да, и поле, которое делает это возможным, называется BindingId. Давайте разбираться.
Что такое страница заголовка файла?
Страница 0 каждого файла данных SQL Server зарезервирована как страница заголовка файла. Она содержит метаданные о самом файле: логическое имя, размер, файловая группа, настройки роста, значения LSN и GUID, который связывает файл с его базой данных.
Вы можете прочитать её с помощью DBCC PAGE для страницы 0, но DBCC FILEHEADER даёт вам более чистый, уже декодированный вывод без необходимости копаться в шестнадцатеричном представлении. Команда принимает имя или идентификатор базы данных и идентификатор файла:
DBCC FILEHEADER (N'TestDB1', 1) WITH NO_INFOMSGS;
Результатом является одна широкая строка с каждым полем из заголовка файла и уже декодированным. Вот ключевые столбцы из этого вывода:
FileId LogicalName BindingId
------ ----------- ------------------------------------
1 TestDB1 DCADD8A7-406E-4095-8B62-22F57A07EF33
Ключевые поля в заголовке файла
Полный вывод содержит 38 столбцов, но три показанных выше — это те, которые важны для этой статьи:
- FileId: Идентификатор файла в пределах базы данных. Файл 1 всегда является первичным файлом данных.
- LogicalName: Логическое имя файла, известное SQL Server, независимо от физического имени файла на диске.
- BindingId: GUID, который идентифицирует, какой базе данных принадлежит этот файл. Каждый файл в одной базе данных имеет одинаковый BindingId. Именно так SQL Server проверяет принадлежность файла к базе данных во время присоединения (attach) и восстановления.
Просмотр всех заголовков файлов в экземпляре
Для этой статьи я запускаю SQL Server 2025 CU3 в контейнере Docker с двумя пользовательскими базами данных: TestDB1 (mdf, вторичный ndf и журнал) и TestDB2 (mdf и журнал). Это даёт нам 5 файлов в 2 базах данных. Вот команды для чтения заголовка файла для каждого файла в моём экземпляре.
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;
Вот что показывает вывод DBCC FILEHEADER для каждой базы данных: все три файла TestDB1 — первичный файл данных, вторичный NDF и журнал — имеют одинаковый GUID. У TestDB2 свой собственный. Этот GUID и есть ваш ключ группировки.
BindingId Database Files
------------------------------------ ---------- -----
DCADD8A7-406E-4095-8B62-22F57A07EF33 TestDB1 3
162F268E-5D83-4667-9CE7-FDCB8DFE9CC1 TestDB2 2
Чтение BindingId непосредственно из файла
Вот сценарий, который делает это действительно полезным в ситуации аварийного восстановления (DR): SQL Server не работает, у вас есть каталог с файлами .mdf, .ndf и .ldf, и вам нужно выяснить, какие из них принадлежат друг другу, прежде чем вы сможете что-либо присоединить.
BindingId хранится в необработанном файле на странице 0 (страница заголовка файла, тип 15) со смещением байта 247. SQL Server кодирует GUID: первые три компонента в формате little-endian, а последние два компонента — как простой массив байтов. Эта функция PowerShell читает его напрямую, без необходимости в SQL Server:
function Read-BindingId { param([string]$Path) if (-not (Test-Path -Path $Path -PathType Leaf)) { throw "$Path : not a file" } $stream = [System.IO.File]::OpenRead($Path) $bytes = [byte[]]::new(263) $null = $stream.Read($bytes, 0, 263) # Read 263 bytes into $bytes, starting at offset 0 $stream.Close() $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 -AutoSizeName 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
Сгруппируйте по GUID, и вы точно узнаете, какие файлы идут вместе. Я запустил это на файлах, скопированных из контейнера во время работы SQL Server, и получил те же GUID, которые возвращает DBCC FILEHEADER. Те же данные, другой путь.
Единственное, чего не может сказать BindingId
BindingId говорит вам, какие файлы должны быть вместе, но он не говорит вам имя базы данных.
Имя базы данных хранится на загрузочной странице (boot page, страница 9 файла 1), а не в заголовке файла. Таким образом, если у вас есть набор отсоединённых файлов без работающего экземпляра SQL Server, вы можете определить, какие файлы образуют набор, используя только DBCC FILEHEADER. Но чтобы получить фактическое имя базы данных, вам нужен DBCC DBINFO для первичного файла, или вам нужно перекрестно ссылаться на sys.databases. Это различие имеет значение в сценариях аварийного восстановления, когда вы пытаетесь восстановить, какие файлы принадлежат каждой базе, прежде чем вы сможете что-либо присоединить.
Завершение
DBCC FILEHEADER — это одна из тех недокументированных команд, которые существуют уже очень давно и невероятно полезны для изучения дисковых структур ваших баз данных. BindingId — это поле, которое должен уметь использовать в своём арсенале каждый администратор баз данных. Это связующее звено, объединяющее все файлы базы данных на уровне хранилища, и SQL Server использует его каждый раз при открытии базы данных, чтобы убедиться, что файлы действительно принадлежат ей. Если вы когда-нибудь окажетесь в ситуации аварийного восстановления, пытаясь собрать воедино, какие файлы относятся к какой базе данных, прежде чем вы сможете что-либо присоединить, начните с этого. Попробуйте это в своей лабораторной среде.

Комментариев нет:
Отправить комментарий