Using T-SQL Snapshot Backup - Point in Time Recovery
Introduction
In this post, the second in our series, I will guide you through using the new T-SQL Snapshot Backup feature in SQL Server 2022 to take a snapshot backup and perform point-in-time database restores using a snapshot backup as the base of the restore. We will explore how to manage storage-level operations, such as cloning snapshots and executing an instantaneous point-in-time restore of a database from the snapshot with minimal impact on your infrastructure. Additionally, I will demonstrate a PowerShell script that utilizes dbatools and the PureStoragePowerShellSDK2 modules to automate the process.
You can grab the whole script for this blog post here.
Taking a T-SQL Snapshot Backup
In the previous post in this series, “Using T-SQL Snapshot Backup - Are Snapshots Backups?” we covered the theory of using T-SQL Snapshot Backup for backup and restore operations. Now let’s get down to business and walk through the process of taking a T-SQL Snapshot Backup.
Initialize the Script’s Variables and Connections
-
To begin, we’ll set up some PowerShell variables to establish a connection to our SQL Server. We’ll also set some variables describing where our primary storage is, a Pure Storage FlashArray, and which volumes contain our databases. I’m utilizing dbatools and the Pure Storage PowerShell SDK2 to manage this process.
Import-Module dbatools Import-Module PureStoragePowerShellSDK2 $TargetSQLServer = 'SqlServer1' # SQL Server Name $ArrayName = 'flasharray1.example.com' # FlashArray $DbName = 'Database1' # Name of database $BackupShare = '\\FileServer1\SHARE\BACKUP' # File system location to write the backup metadata file $PGroupName = 'SqlServer1_Pg' # Name of the Protection Group on FlashArray1 $FlashArrayDbVol = 'Fa_Sql_Volume_1' # Volume name on FlashArray containing database files $TargetDisk = '6000c29240f79ca82ef017e1fdc000a7' # The serial number of the Windows volume containing database files
-
I am using the dbatools module here because it allows me to create a persistent SMO connection to our SQL instance and use it across multiple cmdlet calls in my script. Unlike the
Invoke-SqlCmd
from theSqlServer
module, which establishes a connection, executes the query, and then disconnects the session. If we useInvoke-SqlCmd
to initiate the database freeze, the database will immediately thaw when the session disconnects. Since dbatools maintains the connection, the database will remain frozen even after executing the cmdlet. To do this, I establish the SMO connection usingConnect-DbaInstance
, store the connection in$SqlInstance
, and then pass it across various cmdlet calls, ensuring that the same SPID/connection is reused within SQL Server.$SqlInstance = Connect-DbaInstance -SqlInstance $TargetSQLServer -TrustServerCertificate -NonPooledConnection
-
Let’s get some information about our database; take note of the size (in MB). That’s a pretty big database, nearly 3.5TB on disk. We’re going to restore it nearly instantly later on in this script. Who’s excited?
Get-DbaDatabase -SqlInstance $SqlInstance -Database $DbName | Select-Object Name, SizeMB Name SizeMB ---- ------ Database1 3484091.81
Connect to Our Storage Environment
-
In order to connect to the FlashArray’s REST API, we will prompt for user credentials. These will be used to create a connection object stored in the
$FlashArray
variable for subsequent operations in the script, including taking a storage-based snapshot after freezing the database for write IO.$Credential = Get-Credential $FlashArray = Connect-Pfa2Array –EndPoint $ArrayName -Credential $Credential -IgnoreCertificateError
Taking a T-SQL Based Snapshot Backup
-
Next, we need to freeze the database for write operations. We will create a query string for the T-SQL we want to run and then pass that string as a parameter to
Invoke-DbaQuery
. This code reuses the$SqlInstance
SMO connection that was established earlier, so when this code finishes running, the database will still be frozen for write IO. I added the-Verbose
flag to expose some additional information. In the output, you can see the backup locks being acquired and the output confirming that the database is frozen for write IO.$Query = "ALTER DATABASE $DbName SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON" Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose VERBOSE: Database 'Database1' acquired suspend locks in session 57. VERBOSE: I/O is frozen on database Database1. No user action is required. However, if I/O is not resumed promptly, you could cancel the backup. VERBOSE: Database 'Database1' successfully suspended for snapshot backup in session 57.
-
Next, take a snapshot of the Protection Group while the database is frozen for write IO. This snapshot will contain an application-consistent set of data that can be used for restores, replicated to other arrays in the same site, or transferred between sites for disaster recovery. In the output below, you can see the information returned when taking a snapshot on a FlashArray. Let’s store this information in the variable
$Snapshot
so we can use it later to encode some of these details in the metadata-only backup file’s header.$Snapshot = New-Pfa2ProtectionGroupSnapshot -Array $FlashArray -SourceName $PGroupName $Snapshot Name : SqlServer1-pg.2121 Created : 9/1/2024 7:34:46 PM Destroyed : False Pod : Source : @{Id='53340721-d581-366f-078d-6d6be001fab3'; Name='SqlServer1-pg'} Space : Suffix : 2121 TimeRemaining : EradicationConfig : @{ManualEradication='enabled'} Id : 7200bda4-b855-9c01-627e-5bf6eeec5a16
-
Now, it’s time to take a metadata-only backup. The metadata-only backup file name is dynamically generated using the database name and the current date. The backup command contains the database name
$DbName
, the backup file locationTO DISK='$BackupFile'
, and theWITH METADATA_ONLY
option to specify that it’s a snapshot backup without copying the data. TheMEDIADESCRIPTION
part stores the snapshot and FlashArray names in the backup metadata as a pipe-delimited string. This information can help locate the snapshot associated with this metadata-only backup file. Finally, the T-SQL Snapshot Backup is executed using the cmdletInvoke-DbaQuery
, which writes the metadata-only backup file to the specified location after thawing the database if successful.I am using the verbose flag again. When the command is executed, the output will show that the database is thawed, write IO is resumed, and the backup locks are released. Additionally, it will produce output similar to that of a full backup, reporting the number of pages copied from the database files. However, in this case, it will show that 0 pages are copied because we are performing a
METADATA_ONLY
snapshot backup.$BackupFile = "$BackupShare\$DbName_$(Get-Date -Format FileDateTime).bkm" $Query = "BACKUP DATABASE $DbName TO DISK='$BackupFile' WITH METADATA_ONLY, MEDIADESCRIPTION='$($Snapshot.Name)|$($FlashArray.ArrayName)'" Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose VERBOSE: I/O was resumed on database Database1. No user action is required. VERBOSE: Database 'Database1' released suspend locks in session 57. VERBOSE: Database 'Database1' originally suspended for snapshot backup in session 57 successfully resumed in session 57. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_Base_1' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci1_01' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci2_01' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci3_01' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci4_01' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci5_01' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci6_01' on file 1. VERBOSE: Processed 0 pages for database 'Database1', file 'Database1_part_ci7_01' on file 1. VERBOSE: BACKUP DATABASE successfully processed 0 pages in 0.007 seconds (0.000 MB/sec).
-
Now that we have a snapshot backup as a base for a FULL database restore, let’s take a log backup so that we can later showcase how to perform a point-in-time recovery.
$LogBackup = Backup-DbaDatabase -SqlInstance $SqlInstance -Database $DbName -Type Log -Path $BackupShare -CompressBackup
What Does SQL Server Think Happened?
-
Let’s check the SQL Server Error Log to see what SQL Server thinks happened. Here you can see that the snapshot backup process begins for
Database1
. The database optionsuspend_for_snapshot_backup
is set toON
, and the database acquires backup locks, and freezes I/O operations. Then after we take ourMETADATA_ONLY
snapshot backup, the I/O operations forDatabase1
are resumed, and the backup locks are released. The log reports that the backup operation completes successfully, and we see that again 0 pages being copied.Get-DbaErrorLog -SqlInstance $SqlInstance -LogNumber 0 | Format-Table SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:34:29 PM spid57 Setting database option suspend_for_snapshot_backup to ON for database 'Database1'. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:34:29 PM spid57 Database 'Database1' acquired suspend locks in session 57. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:34:29 PM spid57 I/O is frozen on database Database1. No user action is required. However, if I/O is not resumed promptly, you could cancel the backup. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:34:29 PM spid57 Database 'Database1' successfully suspended for snapshot backup in session 57. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:35:13 PM spid57 I/O is frozen on database Database1. No user action is required. However, if I/O is not resumed promptly, you could cancel the backup. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:35:13 PM spid57 I/O was resumed on database Database1. No user action is required. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:35:13 PM spid57 Database 'Database1' released suspend locks in session 57. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:35:13 PM spid57 Database 'Database1' originally suspended for snapshot backup in session 57 successfully resumed in session 57. SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:35:13 PM Backup Database backed up. Database: Database1, creation date(time): 2023/10/28(20:43:06), pages dumped: 186122570, first LSN: 185573:2497964:53, last LSN: 185573:2497987:1, number of dump devices: 1, device info… SqlServer1 MSSQLSERVER SqlServer1 9/1/2024 7:35:13 PM Backup BACKUP DATABASE successfully processed 0 pages in 0.007 seconds (0.000 MB/sec).…
-
The backup is recorded in MSDB as a Full backup with a snapshot.
Get-DbaDbBackupHistory -SqlInstance $SqlInstance -Database $DbName -Last SqlInstance Database Type TotalSize DeviceType Start Duration End ----------- -------- ---- --------- ---------- ----- -------- --- SqlServer1 Database1 Full 1.39 TB Disk 2024-09-01 19:35:11.000 00:00:02 2024-09-01 19:35:13.000
-
Let’s take a look at the information in the backup header. Here, I’ve summarized important details such as ‘BackupType’, ‘BackupPath’, and data size. This header format is consistent across all backup types in SQL Server. As you develop automation processes around this feature, it should be familiar territory for a DBA.
Read-DbaBackupHeader -SqlInstance $SqlInstance -Path $BackupFile BackupName : BackupDescription : BackupType : 1 ExpirationDate : Compressed : 0 Position : 1 DeviceType : 2 UserName : DOMAIN\anocentino ServerName : SqlServer1 DatabaseName : Database1 DatabaseVersion : 957 ...output ommitted... BackupPath : \\w2016-anthony\SHARE\BACKUP\Database1_20240901T1235110742.bkm BackupSize : 1.39 TB CompressedBackupSize : 1.39 TB
Performing a Point-in-Time Restore from a T-SQL Snapshot Backup
Now that we have a T-SQL snapshot backup and a log backup, let’s perform a point-in-time restore using these two backups.
-
First, we need to take the database offline, which we’d have to do anyway if we were restoring a full backup.
$Query = "ALTER DATABASE $DbName SET OFFLINE WITH ROLLBACK IMMEDIATE" Invoke-DbaQuery -SqlInstance $SqlInstance -Database master -Query $Query
-
Since this is a storage-level operation, the volume of the database files needs to be taken offline.
Get-Disk | Where-Object { $_.SerialNumber -eq $TargetDisk } | Set-Disk -IsOffline $True
-
By using the following code, I can retrieve the
MEDIADESCRIPTION
from the metadata-only backup file, where we stored the pipe-delimited string containing the location of FlashArray where the snapshot is located and the snapshot name. I’m storing this information in the variables$ArrayName
and$SnapshotName
. This is a crucial part of any snapshot backup plan as it allows us to accurately identify the snapshot associated with the metadata-only file. In this case, the FlashArray isflasharray1.example.com
and the snapshot name on that array isSqlServer1-pg.2121
, which contains our backup.$Query = "RESTORE LABELONLY FROM DISK = '$BackupFile'" $Labels = Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose $ArrayName = (($Labels | Select-Object MediaDescription -ExpandProperty MediaDescription).Split('|'))[1] $SnapshotName = (($Labels | Select-Object MediaDescription -ExpandProperty MediaDescription).Split('|'))[0] $ArrayName $SnapshotName flasharray1.example.com SqlServer1-pg.2121
-
In this example, we are performing an in-place database restore. This process involves overwriting the current volume with the contents of the snapshot and reverting the database to its previous state. On a Pure Storage FlashArray (or Cloud Block Store), the cmdlet “New-Pfa2Volume” is used to clone a snapshot. In this code, we are overwriting the volume’s contents with the snapshot’s contents to revert the database to its previous state.
New-Pfa2Volume -Array $FlashArray -Name $FlashArrayDbVol -SourceName ($SnapshotName + ".$FlashArrayDbVol") -Overwrite $true
In our previous post, we discussed additional restore scenarios. We could clone to a new set of volumes on the same instance, restoring the database with a new name. Alternatively, we could clone the volumes to another instance of SQL Server.
-
After restoring the data to its original state, let’s bring the disk back online.
Get-Disk | Where-Object { $_.SerialNumber -eq $TargetDisk} | Set-Disk -IsOffline $False
-
With the database files physically in-place, and reverted back in time, we can use the following code to create the
RESTORE
command as a string:RESTORE DATABASE $DbName
to initiate the restore process, and specify the location of the metadata-only backup file using theFROM DISK = '$BackupFile'
option. Since this is a T-SQL Snapshot backup-based restore, we need to include theMETADATA_ONLY
option, which informs the database engine not to copy the pages during the restore process. Additionally, we should incorporate theREPLACE
option to overwrite the existing database and include theNORECOVERY
option to keep the database inRESTORING
mode after the command is executed. If you wish to bring the database online at this point, you can omit theNORECOVERY
option. TheInvoke-DbaQuery
command is used to execute the query string and initiates the restore process.$Query = "RESTORE DATABASE $DbName FROM DISK = '$BackupFile' WITH METADATA_ONLY, REPLACE, NORECOVERY" Invoke-DbaQuery -SqlInstance $SqlInstance -Database master -Query $Query -Verbose VERBOSE: RESTORE DATABASE successfully processed 0 pages in 1.184 seconds (0.000 MB/sec).
The full restore duration includes the instant snapshot revert and about 1 second of database operations. That’s just wild!
-
With the full restore completed, let’s check the database’s current status. Its in
RESTORING
mode.Get-DbaDbState -SqlInstance $SqlInstance -Database $DbName Access : MULTI_USER ComputerName : SqlServer1 DatabaseName : Database1 InstanceName : MSSQLSERVER RW : READ_WRITE SqlInstance : SqlServer1 Status : RESTORING
-
Now, it’s time to perform the log restore. I am leaving the database in the
RESTORING
mode by adding the-NoRecovery
parameter.Restore-DbaDatabase -SqlInstance $SqlInstance -Database $DbName -Path $LogBackup.BackupPath -NoRecovery -Continue
-
With the restore complete, let’s online the database.
$Query = "RESTORE DATABASE $DbName WITH RECOVERY" Invoke-DbaQuery -SqlInstance $SqlInstance -Database master -Query $Query
-
Let’s again check the current state of the database…its ONLINE
Get-DbaDbState -SqlInstance $SqlInstance -Database 'Database1' Access : MULTI_USER ComputerName : SqlServer1 DatabaseName : Database1 InstanceName : MSSQLSERVER RW : READ_WRITE SqlInstance : SqlServer1 Status : ONLINE
How Long Did The Backup Take?
Lastly, let’s look at how long this process takes, from freezing the database for a consistent snapshot to creating the snapshot on our FlashArray and performing a metadata-only backup. In my lab, this usually takes 400-800ms. But remember, the freeze is only for writes, so it is very fast and only impacts writes. When the database is frozen, you can measure it using the WRITELOG
wait type. Most applications can tolerate a write IO freeze of a few hundred milliseconds. Also, it’s likely that you’ll perform this operation during a maintenance window.
$Start = (Get-Date)
$Query = "ALTER DATABASE $DbName SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON"
Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose
$Snapshot = New-Pfa2ProtectionGroupSnapshot -Array $FlashArray -SourceName $PGroupName
#We'll use MEDIADESCRIPTION to hold some information about our snapshot
$BackupFile = "$BackupShare\$DbName_$(Get-Date -Format FileDateTime).bkm"
$Query = "BACKUP DATABASE $DbName
TO DISK='$BackupFile'
WITH METADATA_ONLY, MEDIADESCRIPTION='$($Snapshot.Name)|$($FlashArray.ArrayName)'"
Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose
$Stop = (Get-Date)
Write-Output "The snapshot time takes...$(($Stop - $Start).Milliseconds)ms!"
The snapshot time takes...480ms
Wrapping Things Up
In this post, which is the second in our series, we used the T-SQL Snapshot Backup feature for point-in-time recovery of a database. Consider the business impact of this capability. You can perform an instantaneous full database restore and control your precise recovery point with a point-in-time restore.
You can grab the whole script for this blog post here.
Using T-SQL Snapshot Backup Blog Series
This article is part of a blog series covering the Using T-SQL Snapshot Backup. Check out the other posts in this series:
- Using T-SQL Snapshot Backup - Are Snapshots Backups?
- Using T-SQL Snapshot Backup - Point in Time Recovery
- Using T-SQL Snapshot Backup - Point in Time Recovery - Azure Edition
You can watch Bob Ward and me present a deep dive on this feature at PASS Summit 2023 in the session “Are Snapshots Backups?”.