15 min czytania

Kopia bazy bez SQL Agenta: harmonogram zadań i własna wysyłka logu

SQL Server Express nie ma SQL Server Agenta ani planów konserwacji, więc kopie trzeba zaplanować samemu. Skrypt PowerShell z sqlcmd uruchamiany z Harmonogramu zadań na koncie gMSA, retencja plików i odtwarzanie kopii logu na drugim serwerze jako odpowiednik log shippingu.

Rys. 1. Kopia logu co 15 minut trafia z dysku serwera na udział i od razu jest odtwarzana na drugim serwerze, więc przełączenie to tylko RECOVERY i zmiana rekordu A; każda kopia logu wykonana poza skryptem przerywa ten łańcuch.

Serwer z SQL Server Express od kilku lat obsługuje bazę systemu CRM. Kreator planu konserwacji jest niedostępny, bo w tej edycji nie ma SQL Server Agenta, więc kopie wykonywał ręcznie administrator przed aktualizacjami aplikacji. Ostatni plik .bak ma osiem miesięcy, a baza w modelu FULL ma log kilka razy większy od pliku danych, bo nikt nie wykonał kopii logu.

Brak Agenta nie oznacza braku kopii. BACKUP i RESTORE działają w Express tak samo jak w wyższych edycjach, brakuje tylko mechanizmu, który uruchomi je o czasie. Ten wpis pokazuje, jak zastąpić go Harmonogramem zadań, skryptem PowerShell z sqlcmd i kontem gMSA, a na drugim serwerze odtwarzać kopie logu jako własny odpowiednik log shippingu.

Czego Express nie ma i czym to zastąpić

Tabela funkcji edycji SQL Server 2025 i 2022 wymienia kilka elementów związanych z kopiami, których Express nie zawiera:

FunkcjaExpressZastępstwo
SQL Server Agent, plany konserwacjibrakHarmonogram zadań Windows i skrypt PowerShell
Wbudowany log shippingbrakskrypt kopiujący pliki kopii logu i zadanie odtwarzania na drugim serwerze
Kompresja kopii (COMPRESSION)brakmiejsce na dysku i udziale liczone dla plików pełnej wielkości
Szyfrowanie kopii (ENCRYPTION)brakuprawnienia NTFS do katalogu i udziału, szyfrowanie woluminu
Database Mailbrakkod wyniku zadania i wiek ostatniej kopii w systemie monitoringu
BACKUP i RESTORE: kopia pełna, różnicowa, kopia logujestnie dotyczy

Plik .bak z Express można odtworzyć na dowolnej edycji tej samej lub nowszej wersji. Kopie logu wykonuje się dla baz w modelu FULL, a nowe bazy dziedziczą model odzyskiwania z bazy model, którego wartość domyślna zależy od edycji. Różnicę między SIMPLE i FULL opisuje czwarta część przewodnika.

Konto zadań i uprawnienia

Zadania kopii działają na osobnym koncie gMSA CORP\svc-sqlbackup$, a nie na koncie usługi CORP\svc-sql$, więc przejęcie zadania nie daje tożsamości silnika bazy. Konto tworzy się tak samo jak konto usługi, z grupą GG-SQL-Hosts jako listą serwerów uprawnionych do pobrania hasła (piąta część przewodnika). Test-ADServiceAccount -Identity svc-sqlbackup na obu serwerach powinien zwrócić True. W SQL Serverze konto dostaje login i rolę db_backupoperator w każdej bazie, czyli uprawnienia BACKUP DATABASE, BACKUP LOG i CHECKPOINT bez dostępu do danych:

CREATE LOGIN [CORP\svc-sqlbackup$] FROM WINDOWS;

-- Użytkownik z rolą db_backupoperator we wszystkich bazach poza tempdb
DECLARE @sql nvarchar(max) = N'';
SELECT @sql += N'USE ' + QUOTENAME(name) + N'; CREATE USER [CORP\svc-sqlbackup$] FOR LOGIN [CORP\svc-sqlbackup$]; '
             + N'ALTER ROLE db_backupoperator ADD MEMBER [CORP\svc-sqlbackup$];' + NCHAR(10)
FROM sys.databases
WHERE name <> N'tempdb' AND state_desc = N'ONLINE';
EXEC sys.sp_executesql @sql;

Plik kopii zapisuje proces SQL Servera, a nie konto zadania. Prawo zapisu dostaje więc identyfikator usługi NT SERVICE\MSSQL$CRM, który obejmuje tylko tę instancję, a nie ERP i HR działające na tym samym koncie gMSA. Konto zadań tworzy katalogi baz, kopiuje pliki na udział i usuwa stare:

New-Item -ItemType Directory -Path 'D:\SQLBackup\CRM' -Force | Out-Null
# Konto zadań: tworzenie katalogów, kopiowanie i usuwanie plików
icacls 'D:\SQLBackup' /grant 'CORP\svc-sqlbackup$:(OI)(CI)M'
# Instancja CRM: zapis i odczyt plików kopii
icacls 'D:\SQLBackup\CRM' /grant 'NT SERVICE\MSSQL$CRM:(OI)(CI)M'

# Udział na serwerze docelowym kopii, tu srv-sql-02
New-SmbShare -Name 'SQLBackup' -Path 'D:\SQLBackup' -ChangeAccess 'CORP\svc-sqlbackup$' -ReadAccess 'CORP\GG-SQL-Admins'

Skrypt kopii

-Type Full wykonuje pełną kopię wszystkich baz poza tempdb, także systemowych. -Type Log obejmuje bazy w FULL z rozpoczętym łańcuchem kopii: last_log_backup_lsn w sys.database_recovery_status ma wartość NULL, gdy baza nie ma jeszcze pełnej kopii, a BACKUP LOG zakończyłby się wtedy błędem 4214. Nowa baza dostaje więc pierwszą kopię logu po najbliższej kopii pełnej.

param(
    [Parameter(Mandatory)][ValidateSet('Full', 'Log')][string]$Type,
    [string]$Instance = "$env:COMPUTERNAME,14331",
    [string]$Root = 'D:\SQLBackup\CRM',
    [string]$Share = '\\srv-sql-02\SQLBackup\CRM',
    [int]$RetentionDays = 14
)
$stamp  = Get-Date -Format 'yyyyMMdd_HHmmss'
$log    = Join-Path $Root ('backup_{0}_{1:yyyyMM}.log' -f $Type, (Get-Date))
$failed = 0

# Kopia logu: bazy w FULL z rozpoczętym łańcuchem; pełna kopia: wszystkie poza tempdb
$filter = "d.recovery_model_desc = 'FULL' AND r.last_log_backup_lsn IS NOT NULL"
if ($Type -eq 'Full') { $filter = "d.name <> 'tempdb'" }
$query = "SET NOCOUNT ON; SELECT d.name FROM sys.databases AS d " +
         "JOIN sys.database_recovery_status AS r ON r.database_id = d.database_id " +
         "WHERE d.state_desc = 'ONLINE' AND $filter;"
$databases = sqlcmd -S $Instance -E -C -b -h -1 -W -w 1024 -Q $query | Where-Object { $_ }
if ($LASTEXITCODE -ne 0) { exit 1 }

foreach ($db in $databases) {
    $dir = Join-Path $Root $db
    New-Item -ItemType Directory -Path $dir -Force | Out-Null
    if ($Type -eq 'Full') {
        $sql = "BACKUP DATABASE [$db] TO DISK = N'$dir\${db}_${stamp}_FULL.bak' WITH CHECKSUM, INIT;"
    } else {
        $sql = "BACKUP LOG [$db] TO DISK = N'$dir\${db}_${stamp}_LOG.trn' WITH CHECKSUM, INIT;"
    }
    sqlcmd -S $Instance -E -C -b -Q $sql | Out-File -FilePath $log -Append -Encoding utf8
    if ($LASTEXITCODE -ne 0) { $failed++ }
}

Skrypt łączy się z lokalną instancją przez port, a nie przez nazwę sql-crm, żeby po przełączeniu rekordu A zadanie na starym serwerze nie zaczęło kopiować nowego serwera na własny dysk. Nazwa pliku ma postać CrmDB_20261018_101500_LOG.trn, więc kolejność alfabetyczna jest kolejnością wykonania, z czego korzystają retencja i odtwarzanie.

WITH CHECKSUM sprawdza sumy kontrolne stron i zapisuje sumę całej kopii, którą później weryfikuje RESTORE; strona z błędną sumą przerywa kopię. Przełącznik -b daje kod wyjścia 1 przy błędzie o ważności powyżej 10, a -C ufa certyfikatowi serwera bez weryfikacji, co jest potrzebne przy certyfikacie samopodpisanym, bo nowsze wersje sqlcmd domyślnie wymagają szyfrowania połączenia. Kod 1 skryptu trafia do Harmonogramu zadań jako wynik ostatniego uruchomienia i przy braku Database Mail jest głównym sygnałem dla monitoringu.

# Kopia na udział: przy kopii logu tylko pliki .trn, przy pełnej także .bak
$mask = if ($Type -eq 'Full') { '*.bak', '*.trn' } else { '*.trn' }
robocopy $Root $Share $mask /S /R:3 /W:10 /NP /NJH /NJS "/LOG+:$log" | Out-Null
if ($LASTEXITCODE -ge 8) { $failed++ }

# Retencja po udanej pełnej kopii: pliki starsze niż N dni i starsze od ostatniej pełnej kopii
if ($Type -eq 'Full' -and $failed -eq 0) {
    $limit = (Get-Date).AddDays(-$RetentionDays)
    foreach ($folder in Get-ChildItem -Path $Root -Directory) {
        $files = Get-ChildItem -Path $folder.FullName -File | Where-Object Extension -in '.bak', '.trn' | Sort-Object Name
        $lastFull = $files | Where-Object Name -like '*_FULL.bak' | Select-Object -Last 1
        if (-not $lastFull) { continue }
        $files | Where-Object { $_.Name -lt $lastFull.Name -and $_.LastWriteTime -lt $limit } | Remove-Item
    }
}
if ($failed -gt 0) { exit 1 }
exit 0

Dysk lokalny, a potem udział

Kopia wprost na udział też działa, ale wymaga prawa zapisu dla konta usługi CORP\svc-sql$, a niedostępny udział oznacza wtedy brak kopii. Kopia lokalna skopiowana zaraz potem daje szybkie odtworzenie z dysku serwera i kopię, która przetrwa jego awarię, a odstęp kilku sekund nie zwiększa utraty danych. Kod robocopy 8 lub wyższy oznacza, że co najmniej jeden plik się nie skopiował. Przy kopii logu skrypt kopiuje tylko pliki .trn, żeby nie sięgać po .bak, który może właśnie zapisywać nocna kopia pełna.

Retencja działa tylko po udanej kopii pełnej i usuwa pliki starsze niż 14 dni, ale wyłącznie sprzed ostatniej pełnej kopii, więc ta kopia i kopie logu po niej zostają zawsze. Przed pierwszym uruchomieniem warto dopisać -WhatIf do Remove-Item. Na srv-sql-02 tę samą pętlę uruchamia osobne zadanie. Udział na drugim serwerze SQL leży w tej samej domenie, więc kopia poza zasięgiem kont domenowych jest osobnym elementem planu.

Harmonogram zadań na koncie gMSA

Konto gMSA nie ma hasła do wpisania w kreatorze, więc zadanie rejestruje się w PowerShellu, z -LogonType Password i bez podawania hasła:

$principal = New-ScheduledTaskPrincipal -UserId 'CORP\svc-sqlbackup$' -LogonType Password
$script    = 'C:\Scripts\Backup-SqlInstance.ps1'

$fullAction  = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -File $script -Type Full"
$fullTrigger = New-ScheduledTaskTrigger -Daily -At '01:00'
Register-ScheduledTask -TaskPath '\SQL\' -TaskName 'CRM - FULL' -Action $fullAction -Trigger $fullTrigger -Principal $principal

# Powtarzanie co 15 minut; bez -RepetitionDuration wzorzec obowiązuje bez końca
$logAction  = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -File $script -Type Log"
$logTrigger = New-ScheduledTaskTrigger -Once -At '00:05' -RepetitionInterval (New-TimeSpan -Minutes 15)
Register-ScheduledTask -TaskPath '\SQL\' -TaskName 'CRM - LOG' -Action $logAction -Trigger $logTrigger -Principal $principal

Start o 00:05 rozsuwa kopię logu i pełną kopię o 01:00, a domyślne ustawienie MultipleInstances równe IgnoreNew nie uruchamia zadania, które jeszcze działa.

Drugi serwer: odtwarzanie kopii logu

Na srv-sql-02 działa instancja CRM w tej samej wersji SQL Servera, na porcie 14331 i na koncie CORP\svc-sql$, więc po przełączeniu nie zmienia się ani ciąg połączenia, ani SPN z drugiej części przewodnika. Kopii z nowszej wersji nie da się odtworzyć na starszej, a STANDBY nie działa, gdy baza wymaga aktualizacji do nowszej wersji, dlatego obie instancje aktualizuje się razem.

Konto zadań dostaje tu ten sam blok uprawnień co na srv-sql-01, bo po przełączeniu stąd będą działać kopie, oraz dwa dodatkowe. RESTORE istniejącej bazy mogą wykonać tylko członkowie ról sysadmin i dbcreator oraz właściciel bazy:

-- RESTORE istniejącej bazy wymaga roli dbcreator
ALTER SERVER ROLE dbcreator ADD MEMBER [CORP\svc-sqlbackup$];
-- Odczyt historii odtworzeń w msdb
USE [msdb];
ALTER ROLE db_datareader ADD MEMBER [CORP\svc-sqlbackup$];

Bazę inicjuje się raz, pełną kopią bez odzyskania. Przy innych ścieżkach plików niż na srv-sql-01 potrzebna jest opcja MOVE z nazwami logicznymi z RESTORE FILELISTONLY.

-- Bez odzyskania: baza zostaje w stanie RESTORING i przyjmuje kolejne kopie logu
RESTORE DATABASE [CrmDB]
FROM DISK = N'D:\SQLBackup\CRM\CrmDB\CrmDB_20261018_010000_FULL.bak'
WITH NORECOVERY;
param(
    [string]$Instance = "$env:COMPUTERNAME,14331",
    [string]$Root = 'D:\SQLBackup\CRM'
)
$log    = Join-Path $Root ('restore_{0:yyyyMM}.log' -f (Get-Date))
$failed = 0

foreach ($folder in Get-ChildItem -Path $Root -Directory) {
    $db = $folder.Name
    # Ostatnio odtworzony plik, tylko dla bazy w stanie RESTORING lub STANDBY
    $query = "SET NOCOUNT ON; SELECT TOP (1) f.physical_device_name " +
             "FROM msdb.dbo.restorehistory AS h " +
             "JOIN msdb.dbo.backupset AS s ON s.backup_set_id = h.backup_set_id " +
             "JOIN msdb.dbo.backupmediafamily AS f ON f.media_set_id = s.media_set_id " +
             "JOIN sys.databases AS d ON d.name = h.destination_database_name " +
             "WHERE h.destination_database_name = N'$db' AND (d.state_desc = 'RESTORING' OR d.is_in_standby = 1) " +
             "ORDER BY h.restore_history_id DESC;"
    $last = sqlcmd -S $Instance -E -C -b -h -1 -W -w 1024 -Q $query | Where-Object { $_ }
    if ($LASTEXITCODE -ne 0) { $failed++; continue }
    if (-not $last) { continue }
    $lastName = Split-Path -Path $last -Leaf

    # Kopie logu nowsze od ostatnio odtworzonej, w kolejności nazw
    $pending = Get-ChildItem -Path $folder.FullName -Filter '*_LOG.trn' | Where-Object Name -gt $lastName | Sort-Object Name
    foreach ($file in $pending) {
        $out = sqlcmd -S $Instance -E -C -b -Q "RESTORE LOG [$db] FROM DISK = N'$($file.FullName)' WITH NORECOVERY;"
        $out | Out-File -FilePath $log -Append -Encoding utf8
        if ($LASTEXITCODE -eq 0) { continue }
        # 4326: kopia kończy się przed bieżącym punktem bazy, jej zmiany są już w bazie
        if ($out -match 'Msg 4326,') { continue }
        $failed++
        break
    }
}
if ($failed -gt 0) { exit 1 }
exit 0

Skrypt porównuje tylko nazwę ostatnio odtworzonego pliku, bo kolumna physical_device_name może zawierać ścieżkę z serwera, na którym wykonano kopię, i odtwarza nowsze pliki w kolejności nazw. Ciągłość LSN sprawdza sam SQL Server: brakujący plik zatrzymuje odtwarzanie z błędem 4305. Błąd 4326 oznacza kopię, której zmiany baza już zawiera, na przykład kopię logu wykonaną w trakcie pełnej kopii użytej do inicjalizacji, więc skrypt ją pomija. Zadanie rejestruje się tak samo jak kopię logu, z -At '00:10', czyli pięć minut po kopii na srv-sql-01.

Zamiast NORECOVERY można użyć STANDBY = N'D:\SQLStandby\CrmDB.tuf'. Baza jest wtedy dostępna do odczytu między odtworzeniami, ale każde odtworzenie wymaga wyłącznego dostępu, więc otwarte połączenia raportów je blokują. Plik .tuf musi leżeć poza katalogiem objętym retencją, bo jego usunięcie wymusza inicjalizację bazy od nowa.

Przełączenie na srv-sql-02

Przy planowanym przełączeniu ostatnia kopia logu na srv-sql-01 wykonuje się z NORECOVERY, więc stara baza przechodzi w stan RESTORING i nie przyjmie już zmian:

-- Kopia końca logu; baza przechodzi w stan RESTORING i nie przyjmuje zmian
BACKUP LOG [CrmDB] TO DISK = N'D:\SQLBackup\CRM\CrmDB\CrmDB_20261018_143000_LOG.trn' WITH NORECOVERY, CHECKSUM;

Ręczne uruchomienie zadania kopii logu (Start-ScheduledTask) kopiuje ten plik na udział, a ręczne uruchomienie zadania odtwarzania na srv-sql-02 go odtwarza. Przy awarii srv-sql-01 od razu wykonuje się odzyskanie na plikach, które dotarły na udział:

-- Koniec odtwarzania: baza przechodzi w stan ONLINE i nie przyjmie już kopii logu
RESTORE DATABASE [CrmDB] WITH RECOVERY;
# Usuwa rekord z adresem srv-sql-01 i dodaje rekord z adresem srv-sql-02, TTL 5 minut
Remove-DnsServerResourceRecord -ZoneName corp.example.com -RRType A -Name sql-crm -RecordData 10.10.20.11 -ComputerName dc01.corp.example.com -Force
Add-DnsServerResourceRecordA -ZoneName corp.example.com -Name sql-crm -IPv4Address 10.10.20.12 -TimeToLive 00:05:00 -ComputerName dc01.corp.example.com

Klienci pamiętają stary adres przez czas TTL rekordu, dlatego TTL obniża się z wyprzedzeniem, tymi samymi poleceniami z adresem 10.10.20.11. Loginy Windows zakłada się na srv-sql-02 wcześniej, a loginy SQL z tym samym SID co na srv-sql-01, inaczej użytkownicy w bazie zostaną bez loginu; szczegóły opisuje trzecia część przewodnika. Zadania kopii na srv-sql-02 czekają zarejestrowane i wyłączone, z parametrem -Share wskazującym udział \\srv-sql-01\SQLBackup\CRM, utworzony tym samym poleceniem New-SmbShare:

Disable-ScheduledTask -TaskPath '\SQL\' -TaskName 'CRM - RESTORE'
Enable-ScheduledTask -TaskPath '\SQL\' -TaskName 'CRM - FULL'
Enable-ScheduledTask -TaskPath '\SQL\' -TaskName 'CRM - LOG'
# Pierwsza pełna kopia na nowym serwerze od razu
Start-ScheduledTask -TaskPath '\SQL\' -TaskName 'CRM - FULL'

Ile danych można stracić

WariantMaksymalna utrata danychCzas przywróceniaCo trzeba utrzymać
Tylko pełna kopia nocnazmiany z całego dnia, do 24 godzinodtworzenie jednego pliku .bak, zależnie od rozmiaru bazy i szybkości kopiowania z udziałujedno zadanie, retencję, miejsce na udziale
Pełna kopia nocna i kopia logu co 15 minutdo 15 minut i czas skopiowania pliku na udziałkopia pełna i wszystkie kopie logu po niej, przy awarii o 16:00 około 60 plików odtwarzanych po koleidwa zadania, bazy w FULL, ciągły łańcuch kopii logu, miejsce na pliki logu
To samo i odtwarzanie na srv-sql-02jak wyżejkilkanaście minut: ostatnie kopie logu, RECOVERY, zmiana rekordu Adrugą instancję w tej samej wersji, zadanie odtwarzania, loginy na obu serwerach, spisaną procedurę przełączenia

Wariant wybiera się według RPO i RTO uzgodnionych z właścicielem systemu. Trzeci zamienia odtwarzanie kilkudziesięciu plików pod presją czasu w procedurę, którą da się przećwiczyć.

Test odtworzenia

RESTORE VERIFYONLY sprawdza kompletność i czytelność kopii oraz sumy kontrolne, ale nie strukturę danych. Odtwarzanie co 15 minut na srv-sql-02 potwierdza ciągłość łańcucha, jednak baza nie przechodzi tam odzyskiwania i nikt nie sprawdza jej spójności. Pełny test to odtworzenie pod inną nazwą i kontrola spójności:

-- Nazwy logiczne plików potrzebne w MOVE
RESTORE FILELISTONLY FROM DISK = N'D:\SQLBackup\CRM\CrmDB\CrmDB_20261018_010000_FULL.bak';

RESTORE DATABASE [CrmDB_Test]
FROM DISK = N'D:\SQLBackup\CRM\CrmDB\CrmDB_20261018_010000_FULL.bak'
WITH MOVE N'CrmDB' TO N'D:\SQLTest\CrmDB_Test.mdf',
     MOVE N'CrmDB_log' TO N'D:\SQLTest\CrmDB_Test_log.ldf',
     CHECKSUM, RECOVERY;

-- Kontrola spójności; brak komunikatów oznacza brak błędów
DBCC CHECKDB (N'CrmDB_Test') WITH NO_INFOMSGS;

DROP DATABASE [CrmDB_Test];

Test do punktu w czasie dokłada kopie logu z NORECOVERY i ostatnią z STOPAT. Częstość, wykonawcę i protokół testów opisuje wpis Backup, którego nikt nie odtwarzał, nie istnieje.

Sprawdź u siebie

Ostatnia kopia każdego typu dla każdej bazy, z miejscem zapisu i informacją o sumie kontrolnej. Dobry wynik to typ D nie starszy niż doba, typ L nie starszy niż 30 minut dla baz w FULL, has_backup_checksums równe 1 i ścieżka w D:\SQLBackup. Ścieżka spoza tego katalogu przy typie L oznacza kopię logu wykonaną poza skryptem, czyli przerwany łańcuch na drugim serwerze.

WITH last_backup AS (
    SELECT b.database_name, b.type, b.backup_finish_date, b.has_backup_checksums, f.physical_device_name,
           ROW_NUMBER() OVER (PARTITION BY b.database_name, b.type ORDER BY b.backup_finish_date DESC) AS rn
    FROM msdb.dbo.backupset AS b
    JOIN msdb.dbo.backupmediafamily AS f ON f.media_set_id = b.media_set_id
)
SELECT database_name, type, backup_finish_date,
       DATEDIFF(MINUTE, backup_finish_date, GETDATE()) AS minutes_ago,
       has_backup_checksums, physical_device_name
FROM last_backup
WHERE rn = 1
ORDER BY database_name, type;

Wynik ostatniego uruchomienia zadań, na każdym z serwerów. Dobry wynik to LastTaskResult równe 0, LastRunTime nie starsze niż interwał zadania i wypełnione NextRunTime. Wartość 1 to błąd zgłoszony przez skrypt i opisany w pliku logu w D:\SQLBackup\CRM, 267011 oznacza zadanie, które jeszcze się nie uruchomiło, a rosnące NumberOfMissedRuns to uruchomienia pominięte przez harmonogram.

Get-ScheduledTask -TaskPath '\SQL\' | Get-ScheduledTaskInfo |
    Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns

Historia odtworzeń na srv-sql-02 zestawiona z czasem wykonania kopii na srv-sql-01. Dobry wynik to wpisy typu L co 15 minut, backup_finish_date ostatniego wpisu nie starsze niż około 30 minut i stan RESTORING albo is_in_standby równe 1. Ostatnie odtworzenie sprzed kilku godzin oznacza zatrzymany łańcuch, a przyczynę, najczęściej błąd 4305, pokazuje plik logu zadania odtwarzania.

SELECT TOP (10) h.destination_database_name, h.restore_date, h.restore_type,
       s.backup_finish_date, d.state_desc, d.is_in_standby
FROM msdb.dbo.restorehistory AS h
JOIN msdb.dbo.backupset AS s ON s.backup_set_id = h.backup_set_id
JOIN sys.databases AS d ON d.name = h.destination_database_name
ORDER BY h.restore_date DESC;

Nagłówek konkretnego pliku kopii, przydatny przy szukaniu brakującego ogniwa. Polecenie tylko czyta plik i wymaga uprawnienia CREATE DATABASE. BackupType równe 1 oznacza kopię pełną, 2 kopię logu, a HasBackupChecksums powinno mieć wartość 1. FirstLSN kopii logu musi być równe LastLSN poprzedniej kopii logu tej bazy; różnica oznacza, że między tymi plikami była kopia, której nie ma w katalogu.

RESTORE HEADERONLY FROM DISK = N'D:\SQLBackup\CRM\CrmDB\CrmDB_20261018_101500_LOG.trn';