SQLServerCentral Article

Database Restore Automation

,

Welcome to this article describing a two script framework, one T-SQL and one PoSh, for automating SQL database restores. It can help to initialize database mirroring and availability groups, execute test restores to verify production backup file integrity, implement a basic log shipping solution and to perform side by side upgrades. No changes should be necessary to either script, save then call them passing parameters to suit your purpose.

sp_LogShippingLight

A stored procedure is provided that generates a T-SQL restore script. The procedure should be created on the primary server where backups were taken, parameters are available offering various restore options and overrides. A T-SQL RESTORE script is constructed and returned to the caller, it is not executed. Run without parameters it builds and returns a restore script for all user databases to the current point in time. If supplying parameters, specify only those required, comment out those not required IE:

DECLARE @RC int
EXECUTE @RC = [master].[dbo].[sp_LogShippingLight]
   @Database = 'workspace'
  --,@WithMoveDataFiles
  --,@WithMoveLogFile
  --,@FromFileFullUNC
  --,@FromFileDiffUNC
  --,@FromFileLogUNC
  --,@StopAt
  --,@StandbyMode
  --,@IncludeSystemDBs
  ,@WithRecovery = 1
  ,@WithCHECKDB = 1;
GO

Below is a description of all parameters and their default values. If a STOPAT date time parameter is supplied with a value previous to the last full backup, the necessary full, differential and log backups are identified from database backup history and an optimized restore script is constructed to that point in time.

Parameter
Example Description
@Database workspace Restrict restore script generated to a specific database, defaults to all user databases
@WithMoveDataFiles x:\datafilesNew\ Overrides WITH MOVE for data file folder, defaults to actual
@WithMoveLogFile y:\logfilesNew\ Overrides WITH MOVE for log file folder, defaults to actual
@FromFileFullUNC \\backups\full\ UNC path to full backup file, defaults to actual drive and folder
@FromFileDiffUNC \\backups\diff\ UNC path to differential backup file, defaults to actual drive and folder
@FromFileLogUNC \\backups\log\ UNC path to log backup files, defaults to actual drive and folder
@StopAt 23/12/2012    12:31:00 Stop at a specific date time, defaults to current (datetime2)
@StandbyMode 0   or 1 Leave database in Standby (1) or No Recovery (0), defaults to 0.
@IncludeSystemDBs 0   or 1 If not restricting to a specific user database, include restore commands for system databases? Defaults to 0 = No
@WithRecovery 0   or 1 Include WITH RECOVERY, defaults to 0 = No
@WithCHECKDB 0   or 1 Include CHECKDB, defaults to 0 = No

ps_LogShippingLight

The companion PowerShell script can be scheduled as a SQL Agent job to automate restores on a standby server, called from a command prompt or by an SSIS package to refresh source data. As with the procedure, no changes should be necessary to the script, save then call it passing parameters. The PowerShell script calls the T-SQL procedure and executes the restore commands on a standby server using the UNC path of the backup folders to override the FROM FILE backup path. It accepts all the stored procedure parameters and the following additional ones to support incremental restores in a log shipping type configuration. Assuming the script was saved as x:\PS\ps_LogShippingLight.ps1

 X:\PS\ps_LogShippingLight.ps1 -DBName "workspace" -PrimaryServer "PBV02\I08A" -StandbyServer "PBV02\I08B" -WithMoveDataFiles "x:\SQL08\I08B\Data\" -WithMoveLogFile  "x:\SQL08\I08B\Logs\" -FromFileFullUNC "\\pbv02\Backups\FULL\" -FromFileDiffUNC "\\pbv02\Backups\DIFF\" -FromFileLogUNC "\\pbv02\Backups\LOG\" -Initialize "1"  -WithRecovery "1" -WithCHECKDB "1"

An escape character ` is needed if a path parameter contains $, IE “\\s1-sql\backups\S1-SQL`$I08\db_workspace\FULL\” . $ is a special character in PowerShell so the escape is required.

Parameter Example Description
$PrimaryServer PBV02\I08A Primary server name, required, backup source
$StandbyServer PBV02\I08B Standby server name, required, restore  target which can be the primary server for disaster recovery.
$RestoreLog X:\Backups\Log1.csv Records the last LSN restored, defaults to ‘backup folder . database name.csv’
$Initialize 0 or 1 Continue restoring only new diff and log backups (No) or start again from full (Yes). Defaults to Yes.
$KillConnections 0 or 1 Kill blocking users (1) or end restore (0), defaults to 1

sp_LogShippingLight

The T-SQL code below compiles in SQL Server Versions 2005 or higher. The entire procedure is effectively a single SELECT statement making repeated calls to a CTE based on backup history. Most of the functionality in the SQL Server Management Studio Restore Database Task wizard is available with the command line interface offering automation possibilities.

The procedure needs permissions to query backup history tables in msdb and data file details in the master database, it constructs and returns T-SQL RESTORE commands only, it does not run them.  It wasn’t built with performance in mind and can be quite resource intensive if you are keeping years of backup history in msdb. Use procedures msdb.dbo.sp_delete_database_backuphistory or msdb.dbo.sp_delete_backuphistory to tidy history beyond what is required.

Known issues:

  • The @WithMoveDataFiles override for database files only offers 1 destination folder. If a database has multiple .ndf’s in different folders, they can only be move to 1 folder on the standby. If folder structures match on the primary and standby servers then no override is required and database files will restore on the standby to the same drive/folder names as on the primary.
  • If a database was upgraded from SQL Server 2000 then additional WITH DATA PURITY checks may be required but are not included in the DBCC CHECKDB command generated.
USE [master]
GO
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = 'sp_LogShippingLight')
EXEC ('CREATE PROC dbo.sp_LogShippingLight AS SELECT ''stub version, to be replaced''')
GO
 
 
/*********************************************************************************************
Log Shipping Light v2.00 (2013-10-15)
(C) 2012, Paul Brewer
 
Feedback: <a href="mailto:paulbrewer@yahoo.co.uk">mailto:paulbrewer@yahoo.co.uk</a>
Updates: <a href="http://paulbrewer.wordpress.com/2013/10/12/database-restore-automation/">http://paulbrewer.wordpress.com/2013/10/12/database-restore-automation/</a>
 
This procedure has a companion PowerShell driver script called ps_LogShippingLight
It is designed to gather backup information and return restore commands which are then executed on a standby server by the PoSh script.
 
Usage examples:
 
sp_LogShippingLight
No parameters = Generates RESTORE commands for all USER databases, from actual backup files to existing file locations to most current time, consistency checks, CHECKSUM where possible
 
sp_LogShippingLight @Database = 'db_workspace', @StopAt = '2012-12-23 12:01:00.000', @StandbyMode = 1
Generates RESTORE commands for a specific database from the most recent full backup + most recent differential + transaction log backups before to STOPAT.
Databases left in STANDBY
Ignores COPY_ONLY backups, restores to default file locations from default backup file.
 
sp_LogShippingLight @Database = 'db_workspace', @StopAt = '2012-12-23 12:31:00.000', @WithMoveDataFiles = 'c:\temp\', @WithMoveLogFile  = 'c:\temp\' , @FromFileFullUNC = 'c:\backup\'
Overrides data file folder, log file folder and backup file folder.
Generates RESTORE commands for a specific database from most recent full backup, most recent differential + transaction log backups before STOPAT.
Ignores COPY_ONLY backups, includes WITH MOVE to simulate a restore to a test environment with different folder mapping.
 
CHANGE LOG:
December 23, 2012   - V1.01 - Release
January 4,2013      - V1.02 - LSN Checks + Bug fix to STOPAT date format
January 11,2013     - V1.03 - SQL Server 2005 compatibility (backup compression problem) & @StandbyMode for stepping through log restores with a readable database
January 14, 2013    - V1.04 - Cope with up to 10 striped backup files
January 15, 2013    - V1.05 - Format of constructed restore script, enclose database name in [ ]
February 7, 2013    - V1.06 - Andrew Guerin feedback, modified WHERE Device_Type IN (102,2)
May 26, 2013        - V1.07 - Various changes for PoSh Driver Script compatibility
October 14, 2013    - V1.08 - Rename parameters, more meaningful names
October 15, 2013    - V2.00 - Add 2nd CTE for striped backup files and remove repeating calls to CTE
*********************************************************************************************/ 
ALTER PROC [dbo].[sp_LogShippingLight]
(
    @Database SYSNAME = NULL,
    @WithMoveDataFiles VARCHAR(2000) = NULL,
    @WithMoveLogFile  VARCHAR(2000) = NULL,
    @FromFileFullUNC VARCHAR(2000) = NULL,
    @FromFileDiffUNC VARCHAR(2000) = NULL,
    @FromFileLogUNC VARCHAR(2000) = NULL,
    @StopAt DATETIME = NULL,
    @StandbyMode BIT = 0,
    @IncludeSystemDBs BIT = 0,
    @WithRecovery BIT = 0,
    @WithCHECKDB BIT = 0
)
AS
BEGIN
 
SET NOCOUNT ON;
 
IF ISNULL(@StopAt,'') = ''
SET @StopAt = GETDATE();
 
--------------------------------------------------------------
-- CTE1 Full backup UNION Differential Backup UNION Log Backup
--------------------------------------------------------------
WITH CTE
(
    database_name
    ,current_compatibility_level
    ,Last_LSN
    ,current_is_read_only
    ,current_state_desc
    ,current_recovery_model_desc
    ,has_backup_checksums
    ,backup_size
    ,[type]
    ,backupmediasetid
    ,family_sequence_number
    ,backupfinishdate
    ,physical_device_name
    ,position
)
AS
(
--------------------------------------------------------------
-- CTE1 Full backup (most current or immediately before @StopAt if supplied)
--------------------------------------------------------------
 
SELECT
    bs.database_name
    ,d.[compatibility_level] AS current_compatibility_level
    ,bs.last_lsn
    ,d.[is_read_only] AS current_is_read_only
    ,d.[state_desc] AS current_state_desc
    ,d.[recovery_model_desc] current_recovery_model_desc
    ,bs.has_backup_checksums
    ,bs.backup_size AS backup_size
    ,'D' AS [type]
    ,bs.media_set_id AS backupmediasetid
    ,mf.family_sequence_number
    ,x.backup_finish_date AS backupfinishdate
    ,mf.physical_device_name
    ,bs.position
FROM msdb.dbo.backupset bs
 
INNER JOIN sys.databases d
ON bs.database_name = d.name
 
INNER JOIN
(
SELECT
    database_name
    ,MAX(backup_finish_date) backup_finish_date
FROM msdb.dbo.backupset a
JOIN msdb.dbo.backupmediafamily b
ON a.media_set_id = b.media_set_id
WHERE a.[type] = 'D'
--  AND b.[Device_Type] = 2
AND Device_Type IN (102,2)
AND a.is_copy_only = 0
AND a.backup_finish_date <= ISNULL(@StopAt,a.backup_finish_date)
GROUP BY database_name
) x
ON x.database_name = bs.database_name
AND x.backup_finish_date = bs.backup_finish_date
 
JOIN msdb.dbo.backupmediafamily mf
ON mf.media_set_id = bs.media_set_id
AND mf.family_sequence_number Between bs.first_family_number And bs.last_family_number
 
WHERE bs.type = 'D'
AND mf.physical_device_name NOT IN ('Nul', 'Nul:')
 
--------------------------------------------------------------
-- CTE1 Differential backup, most current immediately before @StopAt
--------------------------------------------------------------
UNION
 
SELECT
    bs.database_name
    ,d.[compatibility_level] AS current_compatibility_level
    ,bs.last_lsn
    ,d.[is_read_only] AS current_is_read_only
    ,d.[state_desc] AS current_state_desc
    ,d.[recovery_model_desc] current_recovery_model_desc
    ,bs.has_backup_checksums
    ,bs.backup_size AS backup_size
    ,'I' AS [type]
    ,bs.media_set_id AS backupmediasetid
    ,mf.family_sequence_number
    ,x.backup_finish_date AS backupfinishdate
    ,mf.physical_device_name
    ,bs.position
FROM msdb.dbo.backupset bs
 
INNER JOIN sys.databases d
ON bs.database_name = d.name
 
INNER JOIN
(
SELECT
    database_name
    ,MAX(backup_finish_date) backup_finish_date
FROM msdb.dbo.backupset a
JOIN msdb.dbo.backupmediafamily b
ON a.media_set_id = b.media_set_id
WHERE a.[type] = 'I'
--  AND b.[Device_Type] = 2
AND Device_Type IN (102,2)
AND a.is_copy_only = 0
AND a.backup_finish_date <= ISNULL(@StopAt,GETDATE())
GROUP BY database_name
) x
ON x.database_name = bs.database_name
AND x.backup_finish_date = bs.backup_finish_date
 
JOIN msdb.dbo.backupmediafamily mf
ON mf.media_set_id = bs.media_set_id
AND mf.family_sequence_number Between bs.first_family_number And bs.last_family_number
 
WHERE bs.type = 'I'
AND mf.physical_device_name NOT IN ('Nul', 'Nul:')
AND bs.backup_finish_date <= ISNULL(@StopAt,GETDATE())
 
--------------------------------------------------------------
-- CTE1 Log file backups after 1st full backup before @STOPAT
--------------------------------------------------------------
UNION
 
SELECT
    bs.database_name
    ,d.[compatibility_level] AS current_compatibility_level
    ,bs.last_lsn
    ,d.[is_read_only] AS current_is_read_only
    ,d.[state_desc] AS current_state_desc
    ,d.[recovery_model_desc] current_recovery_model_desc
    ,bs.has_backup_checksums
    ,bs.backup_size AS backup_size
    ,'L' AS [type]
    ,bs.media_set_id AS backupmediasetid
    ,mf.family_sequence_number
    ,bs.backup_finish_date as backupfinishdate
    ,mf.physical_device_name
    ,bs.position
 
FROM msdb.dbo.backupset bs
 
INNER JOIN sys.databases d
ON bs.database_name = d.name
 
JOIN msdb.dbo.backupmediafamily mf
ON mf.media_set_id = bs.media_set_id
AND mf.family_sequence_number Between bs.first_family_number And bs.last_family_number
 
LEFT OUTER JOIN
(
    SELECT
    database_name
    ,MAX(backup_finish_date) backup_finish_date
    FROM msdb.dbo.backupset a
    JOIN msdb.dbo.backupmediafamily b
    ON a.media_set_id = b.media_set_id
    WHERE a.[type] = 'D'
    --  AND b.[Device_Type] = 2
    AND Device_Type IN (102,2)
    AND a.is_copy_only = 0
    AND a.backup_finish_date <= ISNULL(@StopAt,a.backup_finish_date)
    GROUP BY database_name
) y
ON bs.database_name = y.Database_name
 
LEFT OUTER JOIN
(
SELECT
    database_name
    ,MIN(backup_finish_date) backup_finish_date
FROM msdb.dbo.backupset a
JOIN msdb.dbo.backupmediafamily b
ON a.media_set_id = b.media_set_id
WHERE a.[type] = 'D'
--  AND b.[Device_Type] = 2
AND Device_Type IN (102,2)
 
AND a.is_copy_only = 0
AND a.backup_finish_date > ISNULL(@StopAt,'1 Jan, 1900')
GROUP BY database_name
) z
ON bs.database_name = z.database_name
 
WHERE bs.backup_finish_date > y.backup_finish_date
AND bs.backup_finish_date < ISNULL(z.backup_finish_date,GETDATE())
AND mf.physical_device_name NOT IN ('Nul', 'Nul:')
AND bs.type = 'L'
--  AND b.[Device_Type] = 2
AND Device_Type IN (102,2)
),
 
--------------------------------------------------------------
-- CTE2 Optionally, striped backup file details
--------------------------------------------------------------
 
Stripes
(
    database_name,
    backupmediasetid,
    family_sequence_number,
    last_lsn,
    S2_pdn,
    S3_pdn,
    S4_pdn,
    S5_pdn,
    S6_pdn,
    S7_pdn,
    S8_pdn,
    S9_pdn,
    S10_pdn
)
AS
(
SELECT
    Stripe1.database_name,
    Stripe1.backupmediasetid,
    Stripe1.family_sequence_number,
    Stripe1.Last_LSN,
    Stripe2.physical_device_name AS S2_pdn,
    Stripe3.physical_device_name AS S3_pdn,
    Stripe4.physical_device_name AS S4_pdn,
    Stripe5.physical_device_name AS S5_pdn,
    Stripe6.physical_device_name AS S6_pdn,
    Stripe7.physical_device_name AS S7_pdn,
    Stripe8.physical_device_name AS S8_pdn,
    Stripe9.physical_device_name AS S9_pdn,
    Stripe10.physical_device_name  AS S10_pdn
        
FROM CTE AS Stripe1
LEFT OUTER JOIN CTE AS Stripe2
ON Stripe2.database_name = Stripe1.Database_name
AND Stripe2.backupmediasetid = Stripe1.backupmediasetid
AND Stripe2.family_sequence_number = 2
 
LEFT OUTER JOIN CTE AS Stripe3
ON Stripe3.database_name = Stripe1.Database_name
AND Stripe3.backupmediasetid = Stripe1.backupmediasetid
AND Stripe3.family_sequence_number = 3
 
LEFT OUTER JOIN CTE AS Stripe4
ON Stripe4.database_name = Stripe1.Database_name
AND Stripe4.backupmediasetid = Stripe1.backupmediasetid
AND Stripe4.family_sequence_number = 4
 
LEFT OUTER JOIN CTE AS Stripe5
ON Stripe5.database_name = Stripe1.Database_name
AND Stripe5.backupmediasetid = Stripe1.backupmediasetid
AND Stripe5.family_sequence_number = 5
 
LEFT OUTER JOIN CTE AS Stripe6
ON Stripe6.database_name = Stripe1.Database_name
AND Stripe6.backupmediasetid = Stripe1.backupmediasetid
AND Stripe6.family_sequence_number = 6
 
LEFT OUTER JOIN CTE AS Stripe7
ON Stripe7.database_name = Stripe1.Database_name
AND Stripe7.backupmediasetid = Stripe1.backupmediasetid
AND Stripe7.family_sequence_number = 7
 
LEFT OUTER JOIN CTE AS Stripe8
ON Stripe8.database_name = Stripe1.Database_name
AND Stripe8.backupmediasetid = Stripe1.backupmediasetid
AND Stripe8.family_sequence_number = 8
 
LEFT OUTER JOIN CTE AS Stripe9
ON Stripe9.database_name = Stripe1.Database_name
AND Stripe9.backupmediasetid = Stripe1.backupmediasetid
AND Stripe9.family_sequence_number = 9
 
LEFT OUTER JOIN CTE AS Stripe10
ON Stripe10.database_name = Stripe1.Database_name
AND Stripe10.backupmediasetid = Stripe1.backupmediasetid
AND Stripe10.family_sequence_number = 10
)
 
--------------------------------------------------------------
-- Results, T-SQL RESTORE commands, below are based on CTE's above
--------------------------------------------------------------
 
SELECT
    a.Command AS TSQL,
    CONVERT(nvarchar(30), a.backupfinishdate, 126)
    AS BackupDate,
    a.BackupDevice,
    a.Last_LSN
FROM
(
 
--------------------------------------------------------------
-- Most recent full backup
--------------------------------------------------------------
 
SELECT
    ';SELECT ''' + 'RESTORE_FULL'' AS STEP' + ';RESTORE DATABASE [' + d.[name] + ']' + SPACE(1) +
    'FROM DISK = N' + '''' +
    CASE ISNULL(@FromFileFullUNC,'Actual')
    WHEN 'Actual' THEN CTE.physical_device_name
    ELSE @FromFileFullUNC + SUBSTRING(CTE.physical_device_name,LEN(CTE.physical_device_name) - CHARINDEX('\',REVERSE(CTE.physical_device_name),1) + 2,CHARINDEX('\',REVERSE(CTE.physical_device_name),1) + 1)
    END + '''' + SPACE(1) +
     
    -- Striped backup files
    CASE ISNULL(Stripes.S2_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S2_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S2_pdn,LEN(Stripes.S2_pdn) - CHARINDEX('\',REVERSE(Stripes.S2_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S2_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S3_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S3_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S3_pdn,LEN(Stripes.S3_pdn) - CHARINDEX('\',REVERSE(Stripes.S3_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S3_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S4_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S4_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S4_pdn,LEN(Stripes.S4_pdn) - CHARINDEX('\',REVERSE(Stripes.S4_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S4_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S5_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S5_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S5_pdn,LEN(Stripes.S5_pdn) - CHARINDEX('\',REVERSE(Stripes.S5_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S5_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S6_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S6_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S6_pdn,LEN(Stripes.S6_pdn) - CHARINDEX('\',REVERSE(Stripes.S6_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S6_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S7_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S7_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S7_pdn,LEN(Stripes.S7_pdn) - CHARINDEX('\',REVERSE(Stripes.S7_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S7_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S8_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S8_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S8_pdn,LEN(Stripes.S8_pdn) - CHARINDEX('\',REVERSE(Stripes.S8_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S8_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S9_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S9_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S9_pdn,LEN(Stripes.S9_pdn) - CHARINDEX('\',REVERSE(Stripes.S9_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S9_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S10_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileFullUNC,'Actual') WHEN 'Actual' THEN Stripes.S10_pdn ELSE @FromFileFullUNC + SUBSTRING(Stripes.S10_pdn,LEN(Stripes.S10_pdn) - CHARINDEX('\',REVERSE(Stripes.S10_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S10_pdn),1) + 1) END + ''''
    END +
 
    'WITH REPLACE, FILE = ' + CAST(CTE.Position AS VARCHAR(5)) + ',' +
    CASE CTE.has_backup_checksums WHEN 1 THEN 'CHECKSUM, ' ELSE ' ' END +
     
    CASE @StandbyMode WHEN 0 THEN 'NORECOVERY,' ELSE 'STANDBY =N' + '''' + ISNULL(@FromFileFullUNC,SUBSTRING(CTE.physical_device_name,1,LEN(CTE.physical_device_name) - CHARINDEX('\',REVERSE(CTE.physical_device_name)))) + '\' + d.name + '_ROLLBACK_UNDO.bak ' + '''' + ',' END + SPACE(1) +
     
    'STATS=10,' + SPACE(1) +
    'MOVE N' + '''' + x.LogicalName + '''' + ' TO ' +
    '''' +
    CASE ISNULL(@WithMoveDataFiles,'Actual')
    WHEN 'Actual' THEN x.PhysicalName
    ELSE @WithMoveDataFiles + SUBSTRING(x.PhysicalName,LEN(x.PhysicalName) - CHARINDEX('\',REVERSE(x.PhysicalName),1) + 2,CHARINDEX('\',REVERSE(x.PhysicalName),1) + 1)
    END + '''' + ',' + SPACE(1) +
     
    'MOVE N' + '''' + y.LogicalName + '''' + ' TO ' +
    '''' +
    CASE ISNULL(@WithMoveLogFile ,'Actual')
    WHEN 'Actual' THEN y.PhysicalName
    ELSE @WithMoveLogFile  + SUBSTRING(y.PhysicalName,LEN(y.PhysicalName) - CHARINDEX('\',REVERSE(y.PhysicalName),1) + 2,CHARINDEX('\',REVERSE(y.PhysicalName),1) + 1)
    END + '''' AS Command,
    1 AS Sequence,
    d.name AS database_name,
    CTE.physical_device_name AS BackupDevice,
    CTE.backupfinishdate,
    CTE.backup_size,
    CTE.Last_LSN
 
FROM sys.databases d
JOIN
(
SELECT
    DB_NAME(mf.database_id) AS name
    ,mf.Physical_Name AS PhysicalName
    ,mf.Name AS LogicalName
FROM sys.master_files mf
WHERE type_desc = 'ROWS'
AND mf.file_id = 1
) x
ON d.name = x.name
 
JOIN
(
SELECT
    DB_NAME(mf.database_id) AS name, type_desc
    ,mf.Physical_Name PhysicalName
    ,mf.Name AS LogicalName
FROM sys.master_files mf
WHERE type_desc = 'LOG'
) y
ON d.name = y.name
 
LEFT OUTER JOIN CTE
ON CTE.database_name = d.name
AND CTE.family_sequence_number = 1
 
JOIN Stripes
ON Stripes.database_name = d.name
AND Stripes.backupmediasetid = CTE.backupmediasetid
AND Stripes.last_lsn = CTE.Last_LSN
 
WHERE CTE.[type] = 'D'
AND CTE.family_sequence_number = 1
 
--------------------------------------------------------------
-- Most recent differential backup
--------------------------------------------------------------
UNION
 
    SELECT
    ';SELECT ''' + 'RESTORE_DIFF'' AS STEP' + ';RESTORE DATABASE [' + d.[name] + ']' + SPACE(1) +
    'FROM DISK = N' + '''' +
    CASE ISNULL(@FromFileDiffUNC,'Actual')
    WHEN 'Actual' THEN CTE.physical_device_name
    ELSE @FromFileDiffUNC + SUBSTRING(CTE.physical_device_name,LEN(CTE.physical_device_name) - CHARINDEX('\',REVERSE(CTE.physical_device_name),1) + 2,CHARINDEX('\',REVERSE(CTE.physical_device_name),1) + 1)
    END + '''' + SPACE(1) +
     
    -- Striped backup files
    CASE ISNULL(Stripes.S2_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S2_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S2_pdn,LEN(Stripes.S2_pdn) - CHARINDEX('\',REVERSE(Stripes.S2_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S2_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S3_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S3_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S3_pdn,LEN(Stripes.S3_pdn) - CHARINDEX('\',REVERSE(Stripes.S3_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S3_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S4_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S4_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S4_pdn,LEN(Stripes.S4_pdn) - CHARINDEX('\',REVERSE(Stripes.S4_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S4_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S5_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S5_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S5_pdn,LEN(Stripes.S5_pdn) - CHARINDEX('\',REVERSE(Stripes.S5_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S5_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S6_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S6_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S6_pdn,LEN(Stripes.S6_pdn) - CHARINDEX('\',REVERSE(Stripes.S6_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S6_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S7_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S7_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S7_pdn,LEN(Stripes.S7_pdn) - CHARINDEX('\',REVERSE(Stripes.S7_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S7_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S8_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S8_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S8_pdn,LEN(Stripes.S8_pdn) - CHARINDEX('\',REVERSE(Stripes.S8_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S8_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S9_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S9_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S9_pdn,LEN(Stripes.S9_pdn) - CHARINDEX('\',REVERSE(Stripes.S9_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S9_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S10_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileDiffUNC,'Actual') WHEN 'Actual' THEN Stripes.S10_pdn ELSE @FromFileDiffUNC + SUBSTRING(Stripes.S10_pdn,LEN(Stripes.S10_pdn) - CHARINDEX('\',REVERSE(Stripes.S10_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S10_pdn),1) + 1) END + ''''
    END +
 
     
    'WITH REPLACE, FILE = ' + CAST(CTE.Position AS VARCHAR(5)) + ',' +
    CASE CTE.has_backup_checksums WHEN 1 THEN 'CHECKSUM, ' ELSE ' ' END +
     
    CASE @StandbyMode WHEN 0 THEN 'NORECOVERY,' ELSE 'STANDBY =N' + '''' + ISNULL(@FromFileFullUNC,SUBSTRING(CTE.physical_device_name,1,LEN(CTE.physical_device_name) - CHARINDEX('\',REVERSE(CTE.physical_device_name)))) + '\' + d.name + '_ROLLBACK_UNDO.bak ' + ''''  + ',' END + SPACE(1) +
     
    'STATS=10,' + SPACE(1) +
    'MOVE N' + '''' + x.LogicalName + '''' + ' TO ' +
    '''' +
    CASE ISNULL(@WithMoveDataFiles,'Actual')
    WHEN 'Actual' THEN x.PhysicalName
    ELSE @WithMoveDataFiles + SUBSTRING(x.PhysicalName,LEN(x.PhysicalName) - CHARINDEX('\',REVERSE(x.PhysicalName),1) + 2,CHARINDEX('\',REVERSE(x.PhysicalName),1) + 1)
    END + '''' + ',' + SPACE(1) +
     
    'MOVE N' + '''' + y.LogicalName + '''' + ' TO ' +
    '''' +
    CASE ISNULL(@WithMoveLogFile ,'Actual')
    WHEN 'Actual' THEN y.PhysicalName
    ELSE @WithMoveLogFile  + SUBSTRING(y.PhysicalName,LEN(y.PhysicalName) - CHARINDEX('\',REVERSE(y.PhysicalName),1) + 2,CHARINDEX('\',REVERSE(y.PhysicalName),1) + 1)
    END + '''' AS Command,
    32769/2 AS Sequence,
    d.name AS database_name,
    CTE.physical_device_name AS BackupDevice,
    CTE.backupfinishdate,
    CTE.backup_size,
    CTE.Last_LSN
 
FROM sys.databases d
 
JOIN CTE
ON CTE.database_name = d.name
AND CTE.family_sequence_number = 1
 
LEFT OUTER JOIN Stripes
ON Stripes.database_name = d.name
AND Stripes.backupmediasetid = CTE.backupmediasetid
AND Stripes.last_lsn = CTE.Last_LSN
 
JOIN
(
SELECT
    DB_NAME(mf.database_id) AS name
    ,mf.Physical_Name AS PhysicalName
    ,mf.Name AS LogicalName
FROM sys.master_files mf
WHERE type_desc = 'ROWS'
AND mf.file_id = 1
) x
ON d.name = x.name
 
JOIN
(
SELECT
    DB_NAME(mf.database_id) AS name, type_desc
    ,mf.Physical_Name PhysicalName
    ,mf.Name AS LogicalName
FROM sys.master_files mf
WHERE type_desc = 'LOG'
) y
ON d.name = y.name
 
JOIN
(
SELECT
    database_name,
    Last_LSN,
    backupfinishdate
FROM CTE
WHERE [Type] = 'D'
) z
ON CTE.database_name = z.database_name
 
WHERE CTE.[type] = 'I'
AND CTE.backupfinishdate > z.backupfinishdate -- Differential backup was after selected full backup
AND CTE.Last_LSN > z.Last_LSN -- Differential Last LSN > Full Last LSN
AND CTE.backupfinishdate < @StopAt
AND CTE.family_sequence_number = 1
 
--------------------------------------------------------------
UNION -- Log backups taken since most recent full or diff
--------------------------------------------------------------
 
SELECT
    ';SELECT ''' + 'RESTORE_LOG'' AS STEP' + ';RESTORE LOG [' + d.[name] + ']' + SPACE(1) +
    'FROM DISK = N' + '''' + --CTE.physical_device_name + '''' + SPACE(1) +
    CASE ISNULL(@FromFileLogUNC,'Actual')
    WHEN 'Actual' THEN CTE.physical_device_name
    ELSE @FromFileLogUNC + SUBSTRING(CTE.physical_device_name,LEN(CTE.physical_device_name) - CHARINDEX('\',REVERSE(CTE.physical_device_name),1) + 2,CHARINDEX('\',REVERSE(CTE.physical_device_name),1) + 1)
    END + '''' +
     
    -- Striped backup files
    CASE ISNULL(Stripes.S2_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S2_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S2_pdn,LEN(Stripes.S2_pdn) - CHARINDEX('\',REVERSE(Stripes.S2_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S2_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S3_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S3_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S3_pdn,LEN(Stripes.S3_pdn) - CHARINDEX('\',REVERSE(Stripes.S3_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S3_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S4_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S4_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S4_pdn,LEN(Stripes.S4_pdn) - CHARINDEX('\',REVERSE(Stripes.S4_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S4_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S5_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S5_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S5_pdn,LEN(Stripes.S5_pdn) - CHARINDEX('\',REVERSE(Stripes.S5_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S5_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S6_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S6_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S6_pdn,LEN(Stripes.S6_pdn) - CHARINDEX('\',REVERSE(Stripes.S6_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S6_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S7_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S7_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S7_pdn,LEN(Stripes.S7_pdn) - CHARINDEX('\',REVERSE(Stripes.S7_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S7_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S8_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S8_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S8_pdn,LEN(Stripes.S8_pdn) - CHARINDEX('\',REVERSE(Stripes.S8_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S8_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S9_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S9_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S9_pdn,LEN(Stripes.S9_pdn) - CHARINDEX('\',REVERSE(Stripes.S9_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S9_pdn),1) + 1) END + ''''
    END +
     
    CASE ISNULL(Stripes.S10_pdn,'')
    WHEN '' THEN ''
    ELSE  ', DISK = N' + '''' + CASE ISNULL(@FromFileLogUNC,'Actual') WHEN 'Actual' THEN Stripes.S10_pdn ELSE @FromFileLogUNC + SUBSTRING(Stripes.S10_pdn,LEN(Stripes.S10_pdn) - CHARINDEX('\',REVERSE(Stripes.S10_pdn),1) + 2,CHARINDEX('\',REVERSE(Stripes.S10_pdn),1) + 1) END + ''''
    END +
     
    CASE @StandbyMode WHEN 0 THEN ' WITH NORECOVERY,' ELSE ' WITH STANDBY =N' + '''' + ISNULL(@FromFileFullUNC,SUBSTRING(CTE.physical_device_name,1,LEN(CTE.physical_device_name) - CHARINDEX('\',REVERSE(CTE.physical_device_name)))) + '\' + d.name + '_ROLLBACK_UNDO.bak ' + ''''  + ',' END + SPACE(1) +
     
    CASE CTE.has_backup_checksums WHEN 1 THEN ' CHECKSUM, ' ELSE ' ' END +
     
    + 'FILE = ' + CAST(CTE.Position AS VARCHAR(5)) +
    ' ,STOPAT = ' + '''' + CONVERT(VARCHAR(21),@StopAt,120) + '''' +
    ',MOVE N' + '''' + x2.LogicalName + '''' + ' TO ' +
    '''' +
    CASE ISNULL(@WithMoveDataFiles,'Actual')
    WHEN 'Actual' THEN x2.PhysicalName
    ELSE @WithMoveDataFiles + SUBSTRING(x2.PhysicalName,LEN(x2.PhysicalName) - CHARINDEX('\',REVERSE(x2.PhysicalName),1) + 2,CHARINDEX('\',REVERSE(x2.PhysicalName),1) + 1)
    END + '''' + ',' + SPACE(1) +
     
    'MOVE N' + '''' + y1.LogicalName + '''' + ' TO ' +
    '''' +
    CASE ISNULL(@WithMoveLogFile ,'Actual')
    WHEN 'Actual' THEN y1.PhysicalName
    ELSE @WithMoveLogFile  + SUBSTRING(y1.PhysicalName,LEN(y1.PhysicalName) - CHARINDEX('\',REVERSE(y1.PhysicalName),1) + 2,CHARINDEX('\',REVERSE(y1.PhysicalName),1) + 1)
    END + ''''
    AS Command,
    32769 AS Sequence,
    d.name AS database_name,
    CTE.physical_device_name AS BackupDevice,
    CTE.backupfinishdate,
    CTE.backup_size,
    CTE.Last_LSN
 
FROM sys.databases d
 
JOIN CTE
ON CTE.database_name = d.name
AND CTE.family_sequence_number = 1
 
LEFT OUTER JOIN Stripes
ON Stripes.database_name = d.name
AND Stripes.backupmediasetid = CTE.backupmediasetid
AND Stripes.last_lsn = CTE.Last_LSN
 
LEFT OUTER JOIN  -- Next full backup after STOPAT
(
SELECT
    database_name,
    MIN(BackupFinishDate) AS backup_finish_date
FROM CTE
WHERE type = 'D'
AND backupfinishdate > @StopAt
GROUP BY database_name
 
) x
ON x.database_name = CTE.database_name
 
LEFT OUTER JOIN -- Highest differential backup date
(
SELECT
    database_name,
    max(backupfinishdate) AS backupfinishdate
FROM CTE
WHERE CTE.type = 'I'
AND CTE.backupfinishdate < @StopAt
GROUP BY database_name
) y
ON y.database_name = CTE.database_name
 
LEFT OUTER JOIN -- First log file after STOPAT
(
SELECT
    database_name,
    min(backupfinishdate) AS backupfinishdate
FROM CTE
WHERE CTE.type = 'L'
AND backupfinishdate > @StopAt
GROUP BY database_name
) z
ON z.database_name = CTE.database_name
 
JOIN
(
SELECT
    database_name,
    MAX(Last_LSN) AS Last_LSN
FROM CTE
WHERE CTE.backupfinishdate < ISNULL(@StopAt,GETDATE())
AND CTE.Type IN ('D','I')
GROUP BY database_name
) x1
ON CTE.database_name = x1.database_name
 
JOIN
(
SELECT
    DB_NAME(mf.database_id) AS name
    ,mf.Physical_Name AS PhysicalName
    ,mf.Name AS LogicalName
FROM sys.master_files mf
WHERE type_desc = 'ROWS'
AND mf.file_id = 1
) x2
ON d.name = x2.name
 
JOIN
(
SELECT
    DB_NAME(mf.database_id) AS name, type_desc
    ,mf.Physical_Name PhysicalName
    ,mf.Name AS LogicalName
FROM sys.master_files mf
WHERE type_desc = 'LOG'
) y1
ON d.name = y1.name
 
WHERE CTE.[type] = 'L'
AND CTE.backupfinishdate <= ISNULL(x.backup_finish_date,'31 Dec, 2199') -- Less than next full backup
AND CTE.backupfinishdate >= ISNULL(y.backupfinishdate, CTE.backupfinishdate) --Great than or equal to last differential backup
AND CTE.backupfinishdate <= ISNULL(z.backupfinishdate, CTE.backupfinishdate) -- Less than or equal to last file file in recovery chain (IE Log Backup datetime might be after STOPAT)
AND CTE.family_sequence_number = 1
 
 
--------------------------------------------------------------
UNION -- Restore WITH RECOVERY
--------------------------------------------------------------
SELECT
    ';SELECT ''' + 'RESTORE_RECOVERY'' AS STEP' + ';RESTORE DATABASE [' + d.[name] + ']' + SPACE(1) + 'WITH RECOVERY' AS Command,
    32771 AS Sequence,
    d.name AS database_name,
    '' AS BackupDevice,
    GETDATE() AS backupfinishdate,
    CTE.backup_size,
    '99999999999999998' AS Last_LSN
 
FROM sys.databases d
 
JOIN CTE
ON CTE.database_name = d.name
 
WHERE CTE.[type] = 'D'
AND @WithRecovery = 1
 
--------------------------------------------------------------
UNION -- CHECKDB
--------------------------------------------------------------
SELECT
    ';SELECT ''' + 'DBCC_CHECKDB'' AS STEP' + ';DBCC CHECKDB(' + '''' + d.[name] + '''' + ') WITH NO_INFOMSGS IF @@ERROR > 0 PRINT N''CONSISTENCY PROBLEMS IN DATABASE : ' + d.name + ''' ELSE PRINT N''CONSISTENCY GOOD IN DATABASE : ' + d.name + '''' AS Command,
    32772 AS Sequence,
    d.name AS database_name,
    '' AS BackupDevice,
    DATEADD(minute,1,GETDATE()) AS backupfinishdate,
    CTE.backup_size,
    '99999999999999999' AS Last_LSN
 
FROM sys.databases d
 
JOIN CTE
ON CTE.database_name = d.name
 
WHERE CTE.[type] = 'D'
AND @WithCHECKDB = 1
AND @WithRecovery = 1
 
--------------------------------------------------------------
UNION -- WITH MOVE secondary data files, allows for up to 32769/2 file groups
--------------------------------------------------------------
SELECT
    ', MOVE N' + '''' + b.name + '''' + ' TO N' +
    '''' +
    CASE ISNULL(@WithMoveDataFiles,'Actual')
    WHEN 'Actual' THEN b.physical_name
    ELSE @WithMoveDataFiles + SUBSTRING(b.Physical_Name,LEN(b.Physical_Name) - CHARINDEX('\',REVERSE(b.Physical_Name),1) + 2,CHARINDEX('\',REVERSE(b.Physical_Name),1) + 1)
    END + '''',
    b.file_id AS Sequence,
    DB_NAME(b.database_id) AS database_name,
    'SECONDARY FULL' AS BackupDevice,
    CTE.backupfinishdate,
    CTE.backup_size,
    CTE.Last_LSN
     
FROM sys.master_files b
INNER JOIN CTE
ON CTE.database_name = DB_NAME(b.database_id)
 
WHERE CTE.[type] = 'D'
AND b.type_desc = 'ROWS'
AND b.file_id > 2
--------------------------------------------------------------
) a
--------------------------------------------------------------
 
WHERE a.database_name = ISNULL(@database,a.database_name)
AND (@IncludeSystemDBs = 1 OR a.database_name NOT IN('master','model','msdb'))
 
ORDER BY
    database_name,
    sequence
 
 
END

ps_LogShippingLight

The PowerShell script below should be saved as a .PS1 file then called passing parameters as necessary. It is the driver script for stored procedure sp_LogShippingLight, it calls the procedure on a primary server to get a restore script which it executes on the standby server. If Initialize = No , it checks a csv file it maintains to see which backups have already been restored and only restores any unprocessed new differential/log backups.

The SQLPS module is a dependency, it’s installed automatically with SQL Server 2012 when the remote server administration feature is enabled but if you are using SQL Server 2008 then this module needs to be made available for import. Another prerequisite is for Execution Policy to be set to Unrestricted.

# Updates: http://paulbrewer.wordpress.com/2013/10/12/database-restore-automation/
# Version 2.0
# ==============================================================================
# PARAMETERS AND DEFAULTS
# ==============================================================================
Param($DBName `
    ,$WithMoveDataFiles `
    ,$WithMoveLogFile `
    ,$FromFileFullUNC `
    ,$FromFileDiffUNC `
    ,$FromFileLogUNC`
    ,$StopAt `
    ,$StandBy `
    ,$WithRecovery `
    ,$WithCHECKDB `
    ,$PrimaryServer `
    ,$StandbyServer `
    ,$RestoreLog `
    ,$Initialize `
    ,$KillConnections `
    )
 
  
# DEFAULTS
if ($FromFileDiffUNC -eq $null) {$FromFileDiffUNC = $FromFileFullUNC}
if ($FromFileLogUNC-eq $null) {$FromFileLogUNC= $FromFileFullUNC}
if ($StopAt -eq $null) {$StopAt = Get-Date -Format s}
if ($StandBy -eq $null){$StandBy = 1}
if ($WithRecovery -eq $null){$WithRecovery = 0}
if ($WithCHECKDB -eq $null){$WithCHECKDB = 0}
  
#PoSh VARIABLES
if ($Initialize -eq $null) {$Initialize = 1}
if ($KillConnections -eq $null) {$KillConnections = 1}
if ($RestoreLog -eq $null) {$RestoreLog = $FromFileFullUNC + $DBName + ".csv"}
  
# REPORT RUNTIME VALUES
"EXECUTION - RUNTIME PARAMETERS" | Out-Default
"DBName  = " + $DBName | Out-Default
"WithMoveDataFiles = " + $WithMoveDataFiles | Out-Default
"WithMoveLogFile = " + $WithMoveLogFile | Out-Default
"FromFileFullUNC = " + $FromFileFullUNC | Out-Default
"FromFileDiffUNC = " + $FromFileDiffUNC | Out-Default
"FromFileLogUNC= " + $FromFileLogUNC | Out-Default
"StopAt = " + $StopAt  | Out-Default
"StandBy = " + $StandBy  | Out-Default
"WithRecovery = " + $WithRecovery  | Out-Default
"WithCHECKDB = " + $WithCHECKDB  | Out-Default
"PrimaryServer = " + $PrimaryServer  | Out-Default
"StandbyServer = " + $StandbyServer  | Out-Default
"RestoreLog = " + $RestoreLog  | Out-Default
"Initialize = " + $Initialize  | Out-Default
"KillConnections = " + $KillConnections  | Out-Default
" " | Out-Default
  
# ==============================================================================
# INITIALIZE AND VALIDATE PARAMETERS
# ==============================================================================
#Snapin for the Invoke-SQLCmd cmdlet
Add-PSSnapin SqlServerCmdletSnapin100 -ErrorAction SilentlyContinue
Import-Module SQLPS -DisableNameChecking
Set-Location -Path Alias:
#
if ($PrimaryServer -eq $null -or $StandbyServer -eq $null -or $DBName -eq $null)
{throw "BOTH PRIMARY AND STANDBY SERVER NAMES ARE REQUIRED PARAMETERS AS IS DATABASE NAME"}
  
if ($KillConnections -eq 0)
{
  
    $activeconnections = "SELECT * FROM sys.sysprocesses WHERE dbid = DB_ID('" + $DBName + "')"
    $check = Invoke-Sqlcmd -Query $activeconnections -Database "master" -ServerInstance $StandbyServer
    if ($check.spid -ne $null)
    {throw "ACTIVE CONNECTIONS ARE BLOCKING RESTORE COMMANDS ON THE STANDBY SERVER"}
}
  
if ($FromFileFullUNC -eq $null)
{throw "UNC PATH TO PRIMARY BACKUP FOLDER IS REQUIRED"}
  
# ==============================================================================
# GENERATES T-SQL RESTORE COMMANDS DESERIALIZING SECONDARY FILES
# ==============================================================================
# With Overrides of data log file locations during restore
if ($WithMoveDataFiles -ne $null -and $WithMoveLogFile -ne $null)
{
    $LogShippingLight_EXEC = `
    "EXEC dbo.sp_LogShippingLight `
    @Database = '" + $DBName + "', `
    @WithMoveDataFiles = '" + $WithMoveDataFiles + "', `
    @WithMoveLogFile = '" + $WithMoveLogFile + "', `
    @FromFileFullUNC = '" + $FromFileFullUNC + "', `
    @FromFileDiffUNC = '" + $FromFileDiffUNC + "', `
    @FromFileLogUNC= '" + $FromFileLogUNC+ "', `
    @StopAt = '" + $StopAt + "', `
    @StandbyMode = '" + $Standby +"', `
    @WithRecovery = '" + $WithRecovery +"', `
    @WithCHECKDB = '" + $WithCHECKDB +"'"
}
else
{
    # Without Overrides of data log file locations during restore
    $LogShippingLight_EXEC = `
    "EXEC dbo.sp_LogShippingLight `
    @Database = '" + $DBName + "', `
    @FromFileFullUNC = '" + $FromFileFullUNC + "', `
    @FromFileDiffUNC = '" + $FromFileDiffUNC + "', `
    @FromFileLogUNC= '" + $FromFileLogUNC+ "', `
    @StopAt = '" + $StopAt + "', `
    @StandbyMode = '" + $Standby +"', `
    @WithRecovery = '" + $WithRecovery +"', `
    @WithCHECKDB = '" + $WithCHECKDB +"'"
}
 
$LogShippingLight_Results = Invoke-SQLCmd -Query $LogShippingLight_EXEC -QueryTimeout 6000 -Database "msdb" -ServerInstance $PrimaryServer
#$LogShippingLight_Results | FL
 
if ($LogShippingLight_Results -ne $null)
{
  
    foreach ($ndffile in $LogShippingLight_Results)
    {
        if ($ndffile.BackupDevice -eq "SECONDARY FULL")
        {if ($ndffiles -notcontains $ndffile.TSQL) {$ndffiles = $ndffiles + $ndffile.TSQL}}
    }
  
    #$ndffiles | Out-Default
  
    $LogShippingLight_Results = $LogShippingLight_Results | Where-Object {$_.BackupDevice -ne "SECONDARY FULL" -and $_.BackupDevice -ne "SECONDARY DIFF"}
    $hash=@{}
    foreach ($command in $LogShippingLight_Results)
    {
        if ({$command.TSQL -contains "*RESTORE*"} -and {$command.TSQL -notcontains "*WITH RECOVERY*"}) {$hash.Add($command.Last_LSN,($command.TSQL + $ndffiles))}
 #       else {$hash.Add($command.BackupDate,$command.TSQL)}
    }
    $pendingcmds = $hash.GetEnumerator() | Sort-Object -Property key
}
else #database has no backups
{throw "NO BACKUP FILES FOUND FOR INSTANCE " + $PrimaryServer + ", DATABASE - " + $DBNAME + ", CANNOT RESTORE TO INSTANCE " +  $StandbyServer}
  
# ==============================================================================
#  FUNCTION TO KILL BLOCKING CONNECTIONS
# ==============================================================================
function f_killconnections($DBName)
{
    $KillQueryConstructor = "SELECT ';KILL ' + CAST(spid AS VARCHAR(4)) + '' FROM sys.sysprocesses WHERE dbid = DB_ID('" + $DBName + "')"
    $KillCommands = Invoke-Sqlcmd -QueryTimeout 6000 -Query $KillQueryConstructor -Database "master" -ServerInstance $StandbyServer
    foreach ($KillCommand in $KillCommands)
    {
        if ($KillCommand -ne $NULL)
        {
            $KillCommand[0] | Out-Default
            $Result = Invoke-Sqlcmd -Query $KillCommand[0] -Database "master" -ServerInstance $StandbyServer
            $Result | Out-Default
        }
    }
}
  
# ==============================================================================
# IF INITIALIZE = YES RESTORE LAST FULL, MOST RECENT DIFFERENTIAL AND LOG BACKUPS
# ==============================================================================
 
If ($Initialize -eq 1)
{
    $hash1=@{}
    foreach ($pendingcmd in $pendingcmds)
    {
        "RESTORE IN PROGESS FOR LSN - " + $pendingcmd.key + $pendingcmd.value | Out-Default
        # Run RESTORE
        if ($KillConnections -eq 1) {f_killconnections -DBName $DBName}
        try {Invoke-SQLCmd -QueryTimeout 6000 -Query $pendingcmd.value -Database "master" -ServerInstance $StandbyServer -verbose}
        catch
        {throw "EXECUTION RUNTIME ERROR "}
        "-------------------------------------------------------------------------------"  | Out-Default
  
        # hash table log for csv
        sleep -Seconds 1
        $hash1.Add($Pendingcmd.name,$pendingcmd.value)
    }
  
    # Log restore commands to csv
    If (Test-Path $RestoreLog) {Remove-Item $RestoreLog}
    foreach ($Restore in ($hash1.GetEnumerator() | Sort-Object Name))
    {
        # Definiton of a new 'Log Record' object
        $RestoreLogObject = @{
        Last_LSN = $Restore.Name
        RestoreCommand = $Restore.value
        RestoreStopAt = $StopAt}
  
        If (Test-Path $RestoreLog)
        {
            $JustData = New-Object PSObject -Property $RestoreLogObject | ConvertTo-Csv -NoTypeInformation
            $JustData[1] | Add-Content -Path Microsoft.PowerShell.Core\FileSystem::$RestoreLog -Encoding UTF8
        }
        else {New-Object PSObject -Property $RestoreLogObject | Export-Csv -Path Microsoft.PowerShell.Core\FileSystem::$RestoreLog -Encoding UTF8 -NoTypeInformation}
    }
}
  
# ==============================================================================
# IF INITIALIZE = NO RESTORE RECENT DIFFERENTIAL AND LOG BACKUPS
# ==============================================================================
If ($Initialize -eq 0)
{
    $RestoreCmdsAlreadyRun = Import-Csv -Path Microsoft.PowerShell.Core\FileSystem::$RestoreLog
 
    if ($RestoreCmdsAlreadyRun.RestoreStopAt -gt $StopAt)
    {
        throw "STOPAT PARAMETER IS EARLIER THAN A PREVIOUS RUN " | Out-Default
        break
    }
  
    if (($RestoreCmdsAlreadyRun.RestoreCommand -like "*DBCC*") -or ($RestoreCmdsAlreadyRun.RestoreCommand -like "*RESTORE_RECOVERY*"))
    {
        throw "RECOVERY HAS RUN, NO FURTH RESTORES PERMITTED" | Out-Default
        break
    }
 
    $hash2=@{}
    foreach ($pendingcmd in $pendingcmds)
    {
        if (($pendingcmd.key -ge $RestoreCmdsAlreadyRun.Last_LSN) -and  ($pendingcmd.value -ne $RestoreCmdsAlreadyRun.RestoreCommand))
        {
            "RESTORE IN PROGESS FOR LSN - " + $pendingcmd.key + $pendingcmd.value | Out-Default
            # Run RESTORE
            if ($KillConnections -eq 1) {f_killconnections -DBName $DBName}
            try {Invoke-SQLCmd -QueryTimeout 6000 -Query $pendingcmd.value -Database "master" -ServerInstance $StandbyServer -verbose}
            catch
            {
                throw "ERROR RUNNING QUERY "
            }
            "-------------------------------------------------------------------------------"  | Out-Default
  
            # hash table log for csv
            sleep -Seconds 1
            $hash2.Add($Pendingcmd.name,$pendingcmd.value)
        }
    }
 
#    clutters output, unnecessary
#    if ($pendingcmd.value = $RestoreCmdsAlreadyRun.Last_LSN)
#    {
#        "NO NEW LOG OR DIFFERENTIAL BACKUPS" | Out-Default
#        "-------------------------------------------------------------------------------"  | Out-Default
#    }   
 
  
    # Log restore commands to csv
    If (Test-Path $RestoreLog) {Remove-Item $RestoreLog}
    foreach ($Restore in ($hash2.GetEnumerator() | Sort-Object Name))
    {
        # Definiton of a new 'Log Record' object
        $RestoreLogObject = @{
        Last_LSN = $Restore.Name
        RestoreCommand = $Restore.value
        RestoreStopAt = $StopAt}
  
        If (Test-Path $RestoreLog)
        {
            $JustData = New-Object PSObject -Property $RestoreLogObject | ConvertTo-Csv -NoTypeInformation
            $JustData[1] | Add-Content -Path Microsoft.PowerShell.Core\FileSystem::$RestoreLog -Encoding UTF8
        }
        else {New-Object PSObject -Property $RestoreLogObject | Export-Csv -Path Microsoft.PowerShell.Core\FileSystem::$RestoreLog -Encoding UTF8 -NoTypeInformation}
    }
}

Log Shipping Light Example

The test lab consists of 2 SQL Server 2008 R2 instances on the same virtual server. This is obviously not a perquisite and the Posh script could be run from anywhere.

Server = PBV01

Instance = I08A

  • Database = workspace (primary)
  • Data File = x:\SQL08\I08A\Data
  • Log File = x:\SQL08\I08A\Logs
  • Full Backup = X:\SQL08\I08A\Backups\FULL
  • Differential Backup = X:\SQL08\I08A\Backups\DIFF
  • Log Backup = X:\SQL08\I08A\Backups\LOG

Instance = I08B

  • Database = workspace (standby)
  • Data File = x:\SQL08\I08B\Data
  • Log File = x:\SQL08\I08B\Logs

Full Recovery with CHECKDB The output below shows an invocation of the ps_LogShippingLight.ps1 script and the commands it executed on the standby server. The restores start from the last full backup (-Initialize “1″), the most recent differential then outstanding log backups and includes recovery and CHECKDB.

PS C:\Users\Administrator> X:\PS\ps_LogShippingLight.ps1 -DBName "workspace" -PrimaryServer "PBV02\I08A" -StandbyServer "PBV02\I08B" -WithMoveDataFiles "x:\SQL08\I08B\Data\" -WithMoveLogFile  "x:\SQL08\I08B\Logs\" -FromFileFullUNC "\\pbv02\Backups\FULL\" -FromFileDiffUNC "\\pbv02\Backups\DIFF\" -FromFileLogUNC "\\pbv02\Backups\LOG\" -Initialize "1" -WithRecovery "1" -WithCHECKDB "1"
 
EXECUTION - RUNTIME PARAMETERS
DBName  = workspace
WithMoveDataFiles = x:\SQL08\I08B\Data\
WithMoveLogFile = x:\SQL08\I08B\Logs\
FromFileFullUNC = \\pbv02\Backups\FULL\
FromFileDiffUNC = \\pbv02\Backups\DIFF\
FromFileLogUNC= \\pbv02\Backups\LOG\
StopAt = 2013-10-18T22:33:58
StandBy = 1
WithRecovery = 1
WithCHECKDB = 1
PrimaryServer = PBV02\I08A
StandbyServer = PBV02\I08B
RestoreLog = \\pbv02\Backups\FULL\workspace.csv
Initialize = 1
KillConnections = 1
 
RESTORE IN PROGESS FOR LSN - 26000000048000001;SELECT 'RESTORE_FULL' AS STEP;RESTORE DATABASE [workspace] FROM DISK = N
'\\pbv02\Backups\FULL\workspace_1.bak' , DISK = N'\\pbv02\Backups\FULL\workspace_2.bak'WITH REPLACE, FILE = 2,CHECKSUM,
 STANDBY =N'\\pbv02\Backups\FULL\\workspace_ROLLBACK_UNDO.bak ', STATS=10, MOVE N'workspace_data' TO 'x:\SQL08\I08B\Dat
a\workspace_data.mdf', MOVE N'workspace_log' TO 'x:\SQL08\I08B\Logs\workspace.ldf'
 
STEP
----
RESTORE_FULL
VERBOSE: 71 percent processed.
VERBOSE: 100 percent processed.
VERBOSE: Processed 176 pages for database 'workspace', file 'workspace_data' on file 2.
VERBOSE: Processed 2 pages for database 'workspace', file 'workspace_log' on file 2.
VERBOSE: RESTORE DATABASE successfully processed 178 pages in 0.469 seconds (2.965 MB/sec).
-------------------------------------------------------------------------------
RESTORE IN PROGESS FOR LSN - 26000000061600001;SELECT 'RESTORE_DIFF' AS STEP;RESTORE DATABASE [workspace] FROM DISK = N
'\\pbv02\Backups\DIFF\workspace_Diff15.bak' , DISK = N'\\pbv02\Backups\DIFF\workspace_Diff25.bak'WITH REPLACE, FILE = 2
,CHECKSUM, STANDBY =N'\\pbv02\Backups\FULL\\workspace_ROLLBACK_UNDO.bak ', STATS=10, MOVE N'workspace_data' TO 'x:\SQL0
8\I08B\Data\workspace_data.mdf', MOVE N'workspace_log' TO 'x:\SQL08\I08B\Logs\workspace.ldf'
RESTORE_DIFF
VERBOSE: 100 percent processed.
VERBOSE: Processed 48 pages for database 'workspace', file 'workspace_data' on file 2.
VERBOSE: Processed 2 pages for database 'workspace', file 'workspace_log' on file 2.
VERBOSE: RESTORE DATABASE successfully processed 50 pages in 0.365 seconds (1.070 MB/sec).
-------------------------------------------------------------------------------
RESTORE IN PROGESS FOR LSN - 26000000062400001;SELECT 'RESTORE_LOG' AS STEP;RESTORE LOG [workspace] FROM DISK = N'\\pbv
02\Backups\LOG\workspace_LogA_6.trn', DISK = N'\\pbv02\Backups\LOG\workspace_LogB_6.trn' WITH STANDBY =N'\\pbv02\Backup
s\FULL\\workspace_ROLLBACK_UNDO.bak ',  CHECKSUM, FILE = 2 ,STOPAT = '2013-10-18 22:33:58',MOVE N'workspace_data' TO 'x
:\SQL08\I08B\Data\workspace_data.mdf', MOVE N'workspace_log' TO 'x:\SQL08\I08B\Logs\workspace.ldf'
RESTORE_LOG
VERBOSE: Processed 0 pages for database 'workspace', file 'workspace_data' on file 2.
VERBOSE: Processed 3 pages for database 'workspace', file 'workspace_log' on file 2.
VERBOSE: This backup set contains records that were logged before the designated point in time. The database is being le
ft in the restoring state so that more roll forward can be performed.
VERBOSE: RESTORE LOG successfully processed 3 pages in 0.215 seconds (0.109 MB/sec).
-------------------------------------------------------------------------------
RESTORE IN PROGESS FOR LSN - 26000000064000001;SELECT 'RESTORE_LOG' AS STEP;RESTORE LOG [workspace] FROM DISK = N'\\pbv
02\Backups\LOG\workspace_LogA_7.trn', DISK = N'\\pbv02\Backups\LOG\workspace_LogB_7.trn' WITH STANDBY =N'\\pbv02\Backup
s\FULL\\workspace_ROLLBACK_UNDO.bak ',  CHECKSUM, FILE = 2 ,STOPAT = '2013-10-18 22:33:58',MOVE N'workspace_data' TO 'x
:\SQL08\I08B\Data\workspace_data.mdf', MOVE N'workspace_log' TO 'x:\SQL08\I08B\Logs\workspace.ldf'
RESTORE_LOG
VERBOSE: Processed 0 pages for database 'workspace', file 'workspace_data' on file 2.
VERBOSE: Processed 1 pages for database 'workspace', file 'workspace_log' on file 2.
VERBOSE: This backup set contains records that were logged before the designated point in time. The database is being le
ft in the restoring state so that more roll forward can be performed.
VERBOSE: RESTORE LOG successfully processed 1 pages in 0.119 seconds (0.065 MB/sec).
-------------------------------------------------------------------------------
RESTORE IN PROGESS FOR LSN - 26000000065600001;SELECT 'RESTORE_LOG' AS STEP;RESTORE LOG [workspace] FROM DISK = N'\\pbv
02\Backups\LOG\workspace_LogA_8.trn', DISK = N'\\pbv02\Backups\LOG\workspace_LogB_8.trn' WITH STANDBY =N'\\pbv02\Backup
s\FULL\\workspace_ROLLBACK_UNDO.bak ',  CHECKSUM, FILE = 2 ,STOPAT = '2013-10-18 22:33:58',MOVE N'workspace_data' TO 'x
:\SQL08\I08B\Data\workspace_data.mdf', MOVE N'workspace_log' TO 'x:\SQL08\I08B\Logs\workspace.ldf'
RESTORE_LOG
VERBOSE: Processed 0 pages for database 'workspace', file 'workspace_data' on file 2.
VERBOSE: Processed 1 pages for database 'workspace', file 'workspace_log' on file 2.
VERBOSE: This backup set contains records that were logged before the designated point in time. The database is being le
ft in the restoring state so that more roll forward can be performed.
VERBOSE: RESTORE LOG successfully processed 1 pages in 0.122 seconds (0.064 MB/sec).
-------------------------------------------------------------------------------
RESTORE IN PROGESS FOR LSN - 99999999999999998;SELECT 'RESTORE_RECOVERY' AS STEP;RESTORE DATABASE [workspace] WITH RECO
VERY
RESTORE_RECOVERY
VERBOSE: RESTORE DATABASE successfully processed 0 pages in 3.543 seconds (0.000 MB/sec).
-------------------------------------------------------------------------------
RESTORE IN PROGESS FOR LSN - 99999999999999999;SELECT 'DBCC_CHECKDB' AS STEP;DBCC CHECKDB('workspace') WITH NO_INFOMSGS
 IF @@ERROR > 0 PRINT N'CONSISTENCY PROBLEMS IN DATABASE : workspace' ELSE PRINT N'CONSISTENCY GOOD IN DATABASE : works
pace'
DBCC_CHECKDB
VERBOSE: CONSISTENCY GOOD IN DATABASE : workspace
-------------------------------------------------------------------------------
 

The PoSh script above was run after the T-SQL Lab Setup script below had completed on the primary server. It’s deliberately obtuse and complex to test the constructed restore script integrity, multiple generations of stripped backup files are created.

-------------------------------------------------
-- Log Shipping Light Example
-- Create Primary Database
-------------------------------------------------
:CONNECT VCTSDBA05
:SETVAR DataDrive "C:\Share\"
:SETVAR LogDrive "C:\Share\"
:SETVAR DatabaseName "workspace"
:SETVAR BackupFull "C:\Share"
:SETVAR BackupDiff "C:\Share"
:SETVAR BackupLog "C:\Share"
 
IF DATABASEPROPERTYEX(N'$(databasename)','Status') IS NOT NULL
BEGIN
    ALTER DATABASE $(DatabaseName) SET SINGLE_USER WITH ROLLBACK IMMEDIATE
    DROP DATABASE $(DatabaseName)
END;
 
CREATE DATABASE $(DatabaseName)
ON
(
    NAME = $(DatabaseName)_data,
    FILENAME = N'$(DataDrive)\$(DatabaseName)_data.mdf',
    SIZE = 10,
    MAXSIZE = 500,
    FILEGROWTH = 5
)
LOG ON
(
    NAME = $(DatabaseName)_log,
    FILENAME = N'$(LogDrive)\$(DatabaseName).ldf',
    SIZE = 5MB,
    MAXSIZE = 5000MB,
    FILEGROWTH = 5MB
) ;
GO
 
-------------------------------------------------
-- Create Table
USE $(DatabaseName);
 
IF OBJECT_ID('dbo.HeartBeat','U') IS NOT NULL
    DROP TABLE dbo.HeartBeat;
 
CREATE TABLE [dbo].[HeartBeat](
[GUID_PK] [uniqueidentifier] NOT NULL,
[CreateDate] [datetime] NULL,
[CreateServer] [nvarchar](50) NULL,
[RandomNbr] [int] NULL,
CONSTRAINT [PK_HeartBeat] PRIMARY KEY CLUSTERED
(
[GUID_PK] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];
 
ALTER TABLE [dbo].[HeartBeat] ADD CONSTRAINT [DF_HeartBeat_GUID_PK] DEFAULT (newsequentialid()) FOR [GUID_PK];
 
ALTER TABLE [dbo].[HeartBeat] ADD CONSTRAINT [DF_HeartBeat_CreateDate] DEFAULT (getdate()) FOR [CreateDate];
 
ALTER TABLE [dbo].[HeartBeat] ADD CONSTRAINT [DF_HeartBeat_CreateServer] DEFAULT (@@servername) FOR [CreateServer];
 
-------------------------------------------------
-- Easily tracked activity and backups
--:SETVAR DatabaseName "workspace"
--:SETVAR BackupFull "X:\SQL08\I08A\Backups\FULL\"
--:SETVAR BackupDiff "X:\SQL08\I08A\Backups\DIFF\"
--:SETVAR BackupLog "X:\SQL08\I08A\Backups\LOG\"
 
DECLARE @FileGeneration INT =1
WHILE @FileGeneration < 3 -- ((@FileGeneration - 1) * @ExecutionTime) + Restore Runtime) = Total Test Runtime
BEGIN  -------------------------------------------------   -- Backup database to 2 striped files. First generation FILE = 1   
DECLARE @nSQL NVARCHAR(1000);  
SELECT @nSQL = N'
   BACKUP DATABASE $(DatabaseName)    
   TO DISK = ' + '''' + '$(BackupFull)\$(DatabaseName)_1.bak' + '''' + ', 
   DISK = ' + '''' + '$(BackupFull)\$(DatabaseName)_2.bak' + '''' + '     
   WITH CHECKSUM,COMPRESSION' + CASE @FileGeneration WHEN 1 THEN ',INIT;' ELSE ';' END    
   EXEC sp_executesql @nSQL;  
   --PRINT @nSQL  
   -------------------------------------------------  
   -- Insert data, log & differential backups. To 2 striped files, file gen# will be @FileGeneration  
   SET NOCOUNT ON;    
   DECLARE @Now DATETIME = GETDATE(); 
   DECLARE @ExecutionTime INT = 2 -- Minutes  
   DECLARE @x INT = 0;    
   DECLARE @y AS DATETIME2;   
   DECLARE @z VARCHAR(200);   
   WHILE @Now > DATEADD(minute,-2,GETDATE())
    BEGIN
        WAITFOR DELAY '00:00:010'; --10 Second Delay
        INSERT INTO [dbo].[HeartBeat]  ([RandomNbr]) VALUES (ROUND((RAND() * 100),0));
 
        SET @x = @x + 1
        SELECT @nSQL = N'
        BACKUP LOG $(DatabaseName)
        TO DISK = ' + '''' + '$(BackupLog)\$(DatabaseName)_LogA_' + CAST(@x AS VARCHAR(3)) + '.trn' + '''' + ',
        DISK = ' + '''' + '$(BackupLog)\$(DatabaseName)_LogB_' + CAST(@x AS VARCHAR(3)) + '.trn' + '''' + '
        WITH CHECKSUM,COMPRESSION' + CASE @FileGeneration WHEN 1 THEN ', INIT;' ELSE ';' END
        EXEC sp_executesql @nSQL;
        --PRINT @nSQL
 
        WAITFOR DELAY '00:00:005';
        INSERT INTO [dbo].[HeartBeat]  ([RandomNbr]) VALUES (ROUND((RAND() * 100),0));
 
        IF @x = 5 -- Random differential backup to test restore script optimization
        BEGIN
 
            SELECT @nSQL = N'
            BACKUP DATABASE $(DatabaseName)
            TO DISK = ' + '''' + '$(BackupDiff)\$(DatabaseName)_Diff1' + CAST(@x AS VARCHAR(3)) + '.bak' + '''' + ',
            DISK = ' + '''' + '$(BackupDiff)\$(DatabaseName)_Diff2' + CAST(@x AS VARCHAR(3)) + '.bak' + '''' + '
            WITH DIFFERENTIAL, CHECKSUM,COMPRESSION' + CASE @FileGeneration WHEN 1 THEN ', INIT;' ELSE ';' END
            EXEC sp_executesql @nSQL;
            --PRINT @nSQL
 
        END
 
        SET @y = GETDATE();
        SET @z = 'Log = ' + CAST(@x AS VARCHAR(3)) + ' - STOPAT = ' + CAST(@y AS VARCHAR(19));
        RAISERROR (@z,0,0);
    END;
 
    SET @FileGeneration = @FileGeneration + 1
END
 
GO

In a log shipping, rather than recovery situation, the PoSh script might be executed as below:

X:\PS\ps_LogShippingLight.ps1 -DBName "workspace" -PrimaryServer "PBV02\I08A" -StandbyServer "PBV02\I08B" -WithMoveDataFiles "x:\SQL08\I08B\Data\" -WithMoveLogFile  "x:\SQL08\I08B\Logs\" -FromFileFullUNC "\\pbv02\Backups\FULL\" -FromFileDiffUNC "\\pbv02\Backups\DIFF\" -FromFileLogUNC "\\pbv02\Backups\LOG\" -Initialize "1"
while (1 -eq 1)
{
    X:\PS\ps_LogShippingLight.ps1 -DBName "workspace" -PrimaryServer "PBV02\I08A" -StandbyServer "PBV02\I08B" -WithMoveDataFiles "x:\SQL08\I08B\Data\" -WithMoveLogFile  "x:\SQL08\I08B\Logs\" -FromFileFullUNC "\\pbv02\Backups\FULL\" -FromFileDiffUNC "\\pbv02\Backups\DIFF\" -FromFileLogUNC "\\pbv02\Backups\LOG\" -Initialize "0"
    Start-Sleep -s 1800
}

Conclusion

This PoSh script and SQL stored procedure might meet certain business requirements as they are now. Log ship type incremental restores to a standby for ETL source data is possible as is the integrity validation of production backups, restoring with recovery to a standby.   They might also form part of a larger overall solution such as reinitializing Availability Groups, database mirroring, replication. They have limitations and challenges, such as those listed below, but they do offer more SQL Server restore automation possibilities than anything else available free (but with no warranty) on the internet that I can find.

  • Some of the features available in the RESTORE DATABASE command, such as those related to Service Broker, Replication and Change Data Capture, are not available in this framework. Additional commands may have to be run after WITH RECOVERY to bring the database into the desired state.
  • SQL Agent security permissions to run the PoSh script, the log shipping light stored procedure on the primary and the restore commands on standby have been the most common problems. Also, making the SQLPS. module available to the script at runtime can be a challenge.
  • The logging in the PoSh script isn’t great, ‘out-default’ & ‘invoke-sql  -verbose’
  • The log shipping Light PoSh script doesn’t copy backup files to the standby server like the default SQL Server Log Shipping feature so it isn’t a good idea for DR purposes.
  • If multiple database backups finish in the same second, the framework errors.

Rate

4.75 (4)

You rated this post out of 5. Change rating

Share

Share

Rate

4.75 (4)

You rated this post out of 5. Change rating