Querying msdb for job history


I recently had the need to create an SSRS report showing the status of some jobs from the 10 last days, so I wrote this script that I thought could come in handy to others as well. The contents should be pretty self explanatory except maybe for the RowNum column which is there to separate each iteration of the job:

WITH cte_jobhistory AS (
		--> Creating a cte only to make the columns run_date and run_time into a "datetime-friendly" format  
		--  so it can be used for filtering
		SELECT 
			RunDateString = SUBSTRING(CAST(run_date as VARCHAR(20)), 1, 4) + '-' +  SUBSTRING(CAST(run_date as VARCHAR(20)), 5, 2) + '-' + SUBSTRING(CAST(run_date as VARCHAR(20)), 7, 2),
			RunTimeString = SUBSTRING(RIGHT('000000' + CAST(run_time as VARCHAR(20)), 6), 1, 2) + ':' + SUBSTRING(RIGHT('000000' + CAST(run_time as VARCHAR(20)), 6), 3, 2) + ':' + SUBSTRING(RIGHT('000000' + CAST(run_time as VARCHAR(20)), 6), 5, 2),
			*
		FROM msdb.dbo.sysjobhistory
		)
SELECT 
	a.name,
	RowNum = ROW_NUMBER() OVER (PARTITION BY a.name, b.step_id ORDER BY instance_id DESC),
	b.step_id,
	b.step_name,
	b.message,
	RunStatus = CASE b.run_status 
		WHEN 0 THEN 'Failed'
		WHEN 1 THEN 'Succeded' 
		WHEN 2 THEN 'Retry' 
		WHEN 3 THEN 'Cancelled' 
		WHEN 4 THEN 'In Progress' 
		END,
	b.run_duration,
	b.RunDateString,
	b.RunTimeString
FROM msdb.dbo.sysjobs a
	INNER JOIN cte_jobhistory b
		ON a.job_id = b.job_id
WHERE CONVERT(datetime, b.RunDateString) > GETDATE()-10
ORDER BY a.name ASC, b.instance_id DESC
Advertisements

Updated: create point-in-time restore script automatically


The script I created for generating a point-in-time restore t-sql code has been updated and the last version should support sql server 2005, 2008 and 2012 versions.

There is one prerequisite:
– Only backups made to file on disk is supported

Here is the final script:

/*
Generate Point-in-time restore script, given only DBName and Point-in-time timestamp
The script WILL NOT do any changes to your databases, it will only generate sql code 
that you need to manually execute.
 
Created by Henning Frettem, www.thefirstsql.com
*/
SET NOCOUNT ON
GO
 
DECLARE
  @DBName varchar(200),
  @PointInTime datetime,
  @Filename varchar(200),
  @tab varchar(1) = char(9),
  @cr varchar(2) = char(13)+char(10),
  @Full_BackupStartDate datetime,
  @Diff_BackupStartDate datetime,
  @Log_BackupStartDate datetime,
  @SQL varchar(max) = ''
 
SET @DBName  = 'MyDatabase'
SET @PointInTime = '2013-05-21 15:40:00'
 
BEGIN TRY
  --> Performing some checks
  IF NOT EXISTS (SELECT 1 FROM msdb.dbo.backupset WHERE database_name = @DBName)
    RAISERROR(N'The database has never been backed up', 16, 1)
  IF NOT EXISTS (SELECT 1 FROM msdb.dbo.backupset WHERE database_name = @DBName AND type = 'D' AND backup_start_date <= @PointInTime)
    RAISERROR(N'No full backup exists prior to the specified PointInTime', 16, 1)
  ELSE IF NOT EXISTS (SELECT 1 FROM msdb.dbo.backupset WHERE database_name = @DBName AND type = 'L' AND backup_start_date > @PointInTime)
    RAISERROR(N'No backup of the log exist after the specified PointInTime, do a tail log backup first', 16, 1)
 
  --> Getting the filename and the date of the last full backup prior to the PointInTime
  SELECT TOP 1 
      @Filename = b.physical_device_name,
      @Full_BackupStartDate = backup_start_date
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'D'
    AND a.backup_start_date <= @PointInTime
  ORDER BY a.backup_start_date DESC
 
  SET @SQL = 'USE master' + @cr + 
    'GO' + @cr + @cr +
    'ALTER DATABASE ' + QUOTENAME(@DBName) + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE' + @cr + 
    'GO' + @cr + @cr
  
  PRINT @SQL
  SET @SQL = ''
 
  --> Restore the last full backup if it exists
  SELECT TOP 1
    @SQL = @SQL + 'RESTORE DATABASE ' + QUOTENAME(@DBName) + ' FROM DISK = ''' + b.physical_device_name + ''' 
        WITH FILE=' + CAST(a.Position as varchar(10)) + ', REPLACE, STATS=5, NORECOVERY' + @cr + 'GO' + @cr,
    @Diff_BackupStartDate = a.backup_start_date
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'D'
    AND a.backup_start_date = @Full_BackupStartDate
  ORDER BY a.backup_start_date DESC   

  PRINT @SQL
  SET @SQL = ''
   
  --> Restore the last differential backup if it exists
  SELECT TOP 1
    @SQL = @SQL + 'RESTORE DATABASE ' + QUOTENAME(@DBName) + ' FROM DISK = ''' + b.physical_device_name + ''' 
        WITH FILE=' + CAST(a.Position as varchar(10)) + ', NORECOVERY' + @cr + 'GO',
    @Diff_BackupStartDate = a.backup_start_date
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'I'
    AND a.backup_start_date > @Full_BackupStartDate
    AND a.backup_start_date < @PointInTime
  ORDER BY a.backup_start_date DESC
  
  PRINT @SQL
  SET @SQL = ''
   
  IF @Diff_BackupStartDate IS NULL
    SET @Diff_BackupStartDate = @Full_BackupStartDate
     
  --> Generate all log restores except the last one
  SELECT
    @SQL = @SQL + 'RESTORE LOG ' + QUOTENAME(@DBName) + ' FROM DISK = ''' + b.physical_device_name + ''' 
        WITH FILE=' + CAST(a.Position as varchar(10)) + ', NORECOVERY' + @cr + 'GO' + @cr,
    @Log_BackupStartDate = a.backup_start_date   
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'L'
    AND a.backup_start_date > @Diff_BackupStartDate
    AND a.backup_start_date < @PointInTime
  ORDER BY a.backup_start_date
 
  PRINT @SQL
  SET @SQL = ''

  --> Generate last log restore script with the stopat command and recovery
  SELECT TOP 1
    @SQL = @SQL + 'RESTORE LOG ' + QUOTENAME(@DBName) + ' FROM DISK = ''' + b.physical_device_name + ''' 
        WITH FILE=' + CAST(a.Position as varchar(10)) + ', RECOVERY, STOPAT = ''' + CONVERT(varchar(20), @PointInTime, 120) + '''' + @cr + 'GO' + @cr
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'L'
    AND a.backup_start_date > @PointInTime
  ORDER BY a.backup_start_date ASC
 
  PRINT @SQL
 
END TRY
BEGIN CATCH
  PRINT ERROR_MESSAGE()
END CATCH

How to find gaps in identity columns at the speed of light


I was just recently working with a client that was moving a few 100k rows of data on a daily basis from one system to another and all this data was incremental in the sense that the source data was a ticket system and there was an incrementing ID on all the sales records. Now the problem was that for various reasons not all data had been transferred in the past, a few sales records had been deleted because of void transactions (these were not so many), others were simply missing (this was a substantial bunch). All these missing data  were amounting to quite a bit of money in the sales reports so I had to find a way to identify and transfer them on a regular basis so that this problem was kept at a bare minimum.

The tables that needed to be synced had in excess of 6 mill rows and even though it’s not huge it’s more than enough to cause problems if you write some bad code. SO! With some excellent help from all my geek friends over at http://www.sqlteam.com/forums  I was able to come down with a script that did the trick…and mind you; it took less than 30 seconds to execute on the production box identifying over 500 gaps! Now since we are only talking 100k rows here the query will probably have sub-second execution time but it scales very well (the concept behind the query originally created by MVP Steve Kass, http://www.stevekass.com/)

-- Make sure insert runs a little faster
SET NOCOUNT ON
GO

-- Prepare a sample table
CREATE TABLE GapsTest (
	ID int NOT NULL IDENTITY(1, 1) PRIMARY KEY CLUSTERED,
	Name varchar(50) NULL
	)
GO

-- Insert 100k rows of dummy data
INSERT INTO GapsTest (Name) VALUES ('TheFirstSQL')
GO 100000

-- Delete some rows to create gaps in the identity column
DELETE GapsTest WHERE ID IN (263, 264, 37392, 44, 98712, 66219)
DELETE GapsTest WHERE ID BETWEEN 35000 AND 36000
GO

-- Run the "magic" query
;WITH 
cte AS (
	SELECT 
		ID, 
		RowNum = ROW_NUMBER() OVER (ORDER BY ID) 
	FROM dbo.GapsTest),
cte2 AS (
	SELECT *, DENSE_RANK() OVER (ORDER BY ID - RowNum) As Series 
	FROM cte),
cte3 AS (
	SELECT *, COUNT(*) OVER (PARTITION BY Series) AS SCount 
	FROM cte2),
cte4 AS (
	SELECT 
		MinID = MIN(ID), 
		MaxID = MAX(ID), 
		Series
	FROM cte3
	GROUP BY Series)
SELECT GapStart = a.MaxID, GapEnd = b.MinID
FROM cte4 a
	INNER JOIN cte4 b
		ON a.Series+1 = b.Series
ORDER BY GapStart

Regular Expressions, advanced string matching and new Split function SQL Server 2008 R2


NOTE: in order to use the functions discussed below you will need to enable Master Data Services on your server and install a version of the MDS database. You *do not* need to configure the Master Data Manager website or anything else, and since the MDS is not a Windows Service it will not create any additional overhead on your server. Just run the .msi and create the database and how you do this is described here: https://thefirstsql.com/2011/02/08/how-to-enable-master-data-services-mds-in-sql-server-2008-r2/

I’ve only recently begun investigating the new Master Data Services in SQL Server 2008 R2 and I have to say that I was completely baffled at some of the new functionality!! MDS is regarded as a Data Management tool and is usually only used in Business Intelligence projects. And it was for this purpose I started investigating, but I soon realised that some of the functions that come with MDS has a far greater potential than just in BI projects. The following functions comes out-of-the-box when you install an instance of Master Data Services:

  • mdq.NGrams
  • mdq.RegexReplace
  • mdq.RegexExtract
  • mdq.RegexSplit
  • mdq.RegexIsMatch
  • mdq.Similarity
  • mdq.RegexIsValid
  • mdq.SimilarityDate
  • mdq.RegexMask
  • mdq.Split

Now, I have to admit that I can’t see the real usage value for all of these but I find mdq.RegexIsMatch, mdq.Similarity and mdq.Split to be reeeeeaally interesting :) They can solve a whole bunch of problems that previously had no solution at all or that required some cumbersome workarounds.

mdq.RegexIsMatch

There is no doubt about it; I suck at RegEx! Always when I want to do some complex matching I have to search the web for hours to find an example that I can copy and that’s kind of embarrassing. Nevertheless, this new function makes it possible to use RegEx in sql server without having to create your own clr assembly and that really helps! Here is how you use the new function (be aware that you’ll have to install the MDS database first):

--> Usage: mdq.RegexIsMatch (input,pattern,mask)
declare @table table (
	email varchar(200))
insert into @table values ('my.sample@mail.com'),('15.sample@mail.com'), ('my.sample@mail.c')

select email, isvalid = mdq.RegexIsMatch(email, N'^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3} \.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)+$', 0)
from @table
--> Result
email	isvalid
my.sample@mail.com	1
15.sample@mail.com	1
my.sample@mail.c	0

A rediculously long regex expression but it works!

mdq.Similarity

The coolest of the new functions BY FAR is the mdq.Similarity function. It’s not something you’ll use every day, but when you need it you *really* need it! What it basically does is to compare two strings and score their similarity from 0.0 to 1.0 and the higher the number is the more alike the two strings are. This is extremely useful when you for example need to merge two tables together or you need to find closely matching records and you don’t have a full-text index available. The function supports 4 different matching algorithms:

  • Levenshtein edit distance algorithm (0)
  • Jaccard similarity coefficient algorithm (1)
  • A form of the Jaro-Winkler distance algorithm (2)
  • Longest common subsequence algorithm (3)

I will not go in to detail about each algorithm, but to my (limited) experience it seems that the Jaro-Winkler distance algorithm (3) will give the best result when comparing name records which is what I have done so far. Here’s how you would use it:

--> Usage: mdq.Similarity (input1,input2,method,containmentBias , [ minScoreHint ] )
SELECT mdq.Similarity(N'The First SQL', N'TheFirstSQL', 2, 0.0, 0.0);
--> Result:
(No column name)
0,964102564102564

mdq.Split

The Split function is widely used in programming and in SQL Server there are numerous implementations that span from custom clr-functions to all kinds of user defined functions. And the mdq.Split works more or less exactly as you expect it to, except it has some additional functionality. You can for example use regular expressions inside the split function:

--> Usage: mdq.Split (input,separators,removeEmpty,tokenPattern,[mask])
select * from mdq.split('the, first, sql, rules', ', ', 1, '', 0)
--> Result:
Sequence	Token	IsValid
1	the	1
2	first	1
3	sql	1
4	rules	1

Pretty cool, eh?? :)

Mirroring between two stand-alone servers using certificates


Mirroring is usually done between two servers inside the same network where both servers are a part of a domain and have a trusted authentication mechanism provided by Active Directory. But what if this is not the case? What if you have your mirroring partners in separate networks or in separate data centers even, where there is no trusted authentication possible?

There is still hope, and the solution is to use SQL Server certificates. Here are the scripts you need to set up mirroring between a PRINCIPAL and a MIRROR server using certificates.

PLEASE NOTE: you can not simply copy and paste these scripts in to SQL Server Management Studio, press F5 and expect everything to work. You need to run each individual statement by itself, and some times you will need to run scripts on the mirror before progressing on the principal (i.e you need to create and do a backup of the certificate on the mirror before importing it to the principal, obviously)

--> This is the setup of the PRINCIPAL (master) in a mirroring environment

USE master
GO

CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'password';
GO

--> Create a name not using "master" or "principal", remember: the roles can 
--  switch but the certificates remain the same. 
CREATE CERTIFICATE SQLSrv1_mirroring_cert 
   WITH SUBJECT = 'SQLSrv1 certificate',
      START_DATE = '2009-01-01 00:00:00',
      EXPIRY_DATE = '2030-01-01 00:00:00'
GO

CREATE ENDPOINT Mirroring_Endpoint
   STATE = STARTED
   AS TCP (
      LISTENER_PORT = 7024, 
      LISTENER_IP = ALL
   ) 
   FOR DATABASE_MIRRORING ( 
      AUTHENTICATION = CERTIFICATE SQLSrv1_mirroring_cert, 
      ENCRYPTION = REQUIRED ALGORITHM AES, 
      ROLE = ALL
   )
GO

--> Backup certificate and transfer to SQLSrv2
BACKUP CERTIFICATE SQLSrv1_mirroring_cert TO FILE = 'C:\SQLSrv1_mirroring_cert.cer';
GO

--> Create a login for the other server process
CREATE LOGIN SQLSrv2_mirroring_login WITH PASSWORD = 'password';
GO

--> Create a user for the new login 
CREATE USER SQLSrv2_mirroring_user FOR LOGIN SQLSrv2_mirroring_login;
GO

--> Associate the certificate with the user.
CREATE CERTIFICATE SQLSrv2_cert
   AUTHORIZATION SQLSrv2_mirroring_user
   FROM FILE = 'C:\SQLSrv2_mirroring_cert.cer'
GO

--> Grant the user access to the endpoint 
GRANT CONNECT ON ENDPOINT::Mirroring_Endpoint TO [SQLSrv2_mirroring_login]
GO

--> Create a FULL BACKUP of each database to be mirrored
BACKUP DATABASE mojo TO DISK = 'D:\SQLbackup\mojo_20090211.bak'

--> Restore these databases to the MIRROR using NORECOVERY

--> Set up partnering between the mirror and the principal (use a fully qualified name or an IP address)

--> Set up partnering between the principal and the mirror (use a fully qualified name or an IP address)
ALTER DATABASE mojo SET PARTNER ='tcp://sqlsrv2:7024'
GO
--> This is the setup of the MIRROR (slave) in a mirroring environment

USE master
GO

--> Make sure a master key is set 
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'password';
GO

--> 
CREATE CERTIFICATE SQLSrv2_mirroring_cert 
   WITH SUBJECT = 'SQLSrv2 certificate',
      START_DATE = '2009-01-01 00:00:00',
      EXPIRY_DATE = '2030-01-01 00:00:00'
GO

CREATE ENDPOINT Mirroring_Endpoint
   STATE = STARTED
   AS TCP (
      LISTENER_PORT = 7024, 
      LISTENER_IP = ALL
   ) 
   FOR DATABASE_MIRRORING ( 
      AUTHENTICATION = CERTIFICATE SQLSrv2_mirroring_cert, 
      ENCRYPTION = REQUIRED ALGORITHM AES, 
      ROLE = ALL
   )
GO

--> Backup certificate and transfer to SQLSrv1
BACKUP CERTIFICATE SQLSrv2_mirroring_cert TO FILE = 'C:\SQLSrv2_mirroring_cert.cer';
GO

--> Create a login for the other server process
CREATE LOGIN SQLSrv1_mirroring_login WITH PASSWORD = 'password'
GO

--> Create a user for the new login 
CREATE USER SQLSrv1_mirroring_user FOR LOGIN SQLSrv1_mirroring_login;
GO

--> Associate the certificate with the user.
CREATE CERTIFICATE SQLSrv1_mirroring_cert
   AUTHORIZATION SQLSrv1_mirroring_user
   FROM FILE = 'C:\SQLSrv1_mirroring_cert.cer'
GO

--> Grant the user access to the endpoint 
GRANT CONNECT ON ENDPOINT::Mirroring_Endpoint TO [SQLSrv1_mirroring_login]
GO

--> Copy a backup of the database(s) to be mirrored

--> Restore them using NORECOVERY
RESTORE DATABASE mojo
   FROM DISK='d:\SQLBackups\mojo_20090211.bak'
   WITH NORECOVERY
GO

--> Set up partnering between the mirror and the principal (use a fully qualified name or an IP address)
ALTER DATABASE mojo SET PARTNER ='tcp://sqlsrv1:7024'
GO

How to monitor replication latency using code?


I while back I had the need for monitoring quite a few replicated databases, so I had to make a procedure to automate the monitoring process. This procedure will insert a tracer token for each publication and monitor how long it takes for the token to by committed at each subscriber.

CREATE PROCEDURE [dbo].[proc_replication_latency_monitor]
AS

-- Procedure created by Henning Frettem, www.thefirstsql.com

BEGIN

SET NOCOUNT ON

DECLARE 
  @PublishedDBName varchar(200),
  @SQL nvarchar(2000),
  @PublicationPK int, 
  @PublicationName varchar(200),
  @PublicationDatabase varchar(200),
  @TracerTokenID int,
  @Parmameters nvarchar(500)
  
DECLARE @Publications table (
  PublicationPK int IDENTITY(1, 1) PRIMARY KEY CLUSTERED,
  PublicationDatabase varchar(200),
  PublicationName varchar(200),
  TracerTokenID int,
  TracerTokenPostTime datetime
)

DECLARE @Latency table (
  PublicationPK int,
  DistributorLatency bigint,
  Subscriber sysname,
  SubscriberDB sysname,
  SubscriberLatency bigint,
  OverallLatency bigint
)

--> Cursor for fetching all publications in all databases
DECLARE curPublishedDatabases CURSOR LOCAL FAST_FORWARD FOR 
  SELECT [name] FROM sys.databases WHERE is_published > 0
  
OPEN curPublishedDatabases
FETCH curPublishedDatabases INTO @PublishedDBName
WHILE @@FETCH_STATUS = 0
  BEGIN 
    SET @SQL = N'SELECT ''' + @PublishedDBName + ''', [name] FROM ' + @PublishedDBName + '.dbo.syspublications'
    INSERT INTO @Publications (PublicationDatabase, PublicationName)
    EXEC sp_executesql @SQL
    
    FETCH NEXT FROM curPublishedDatabases INTO @PublishedDBName
  END

CLOSE curPublishedDatabases
DEALLOCATE curPublishedDatabases 

--> Cursor for posting tracer tokens
DECLARE curPublications CURSOR LOCAL FAST_FORWARD FOR 
  SELECT PublicationPK, PublicationDatabase, PublicationName FROM @Publications

OPEN curPublications
FETCH curPublications INTO @PublicationPK, @PublicationDatabase, @PublicationName
WHILE @@FETCH_STATUS = 0
  BEGIN 
    SET @Parmameters = N'@PublicationName varchar(200), @TracerTokenID_OUT int OUTPUT';
    SET @SQL = N'EXEC ' + @PublicationDatabase + '.sys.sp_posttracertoken @publication = @PublicationName, @tracer_token_id = @TracerTokenID_OUT OUTPUT'
    EXEC sp_executesql @SQL, @Parmameters, @PublicationName = @PublicationName, @TracerTokenID_OUT = @TracerTokenID OUTPUT
  
    UPDATE @Publications SET 
      TracerTokenID = @TracerTokenID,
      TracerTokenPostTime = GETDATE()
    WHERE PublicationPK = @PublicationPK

    FETCH NEXT FROM curPublications INTO @PublicationPK, @PublicationDatabase, @PublicationName
  END
CLOSE curPublications
DEALLOCATE curPublications

--> Wait two minutes for all tokens to be commited at all subscribers
WAITFOR DELAY '000:02:00.000'

--> Then check the results for each posted token 
DECLARE curTokens CURSOR LOCAL FAST_FORWARD FOR 
  SELECT PublicationPK, PublicationDatabase, PublicationName, TracerTokenID FROM @Publications

OPEN curTokens
FETCH curTokens INTO @PublicationPK, @PublicationDatabase, @PublicationName, @TracerTokenID
WHILE @@FETCH_STATUS = 0
  BEGIN 
    --> Insert token history for each token 
    SET @SQL = N'EXEC ' + @PublicationDatabase + '.dbo.sp_helptracertokenhistory @publication = ''' + @PublicationName + ''', @tracer_id = ' + CAST(@TracerTokenID AS VARCHAR(50)) + ''
    INSERT INTO @Latency
      (DistributorLatency, Subscriber, SubscriberDB, SubscriberLatency, OverallLatency)
    EXEC sp_executesql @SQL

    --> Make sure that the PublicationPK is added to the token history
    UPDATE @Latency SET PublicationPK = @PublicationPK WHERE PublicationPK IS NULL
  
    --> Clean up the tracer token
    SET @SQL = N'EXEC ' + @PublicationDatabase + '.sys.sp_deletetracertokenhistory @publication = ''' + @PublicationName + ''', @tracer_id = ' + CAST(@TracerTokenID AS VARCHAR(50)) + ''
    EXEC sp_executesql @SQL

    FETCH NEXT FROM curTokens INTO @PublicationPK, @PublicationDatabase, @PublicationName, @TracerTokenID
  END
CLOSE curTokens
DEALLOCATE curTokens

SELECT 
  b.PublicationDatabase,
  b.PublicationName,
  a.Subscriber,
  a.SubscriberDB,
  a.OverallLatency,
  b.TracerTokenPostTime 
FROM @Latency a
  INNER JOIN @Publications b
    ON a.PublicationPK = b.PublicationPK

END

Create a full Point-In-Time restore script automatically!


A new version has been created! Check it out here: Updated: create point-in-time restore script automatically

Ok, I was sort of bored today so I figured I’d do something I’ve been wanting to do for a long time now.

It’s not very often you need to do a point-in-time restore but when you do you’re quite often in a big hurry. So I created a nice little script that will generate the entire point-in-time restore code for you automatically by only providing the name of the database and the point in time. All the scripts are generated from data in the backup-tables in the MSDB but for the actual restore scripts to work you’ll of course need the backup files in the same location as they were when you ran the backup.

Please note that the script only works on SQL Server 2008 (because of the format of the header/fillist tables).
Create a point-in-time restore automatically – SQL 2005 version

/*
Generate Point-in-time restore script, given only DBName and Point-in-time timestamp
Works only on SQL2008 databases!!

Script for generating restore script for full backup originally created by Michael
Valentine Jones, SQLTeam.com

The rest is created by Henning Frettem, www.thefirstsql.com
*/
SET NOCOUNT ON
GO

DECLARE
  @DBName varchar(200) = 'MyDB',
  @PointInTime datetime = '2010-08-09 09:10:00',
  @Filename varchar(200),
  @tab varchar(1) = char(9),
  @cr varchar(2) = char(13)+char(10),
  @Full_BackupStartDate datetime,
  @Diff_BackupStartDate datetime,
  @Log_BackupStartDate datetime,
  @SQL nvarchar(max) = ''

BEGIN TRY
  --> Performing some checks
  IF NOT EXISTS (SELECT 1 FROM msdb.dbo.backupset WHERE database_name = @DBName AND type = 'D' AND backup_start_date  @PointInTime)
    RAISERROR(N'No backup of the log exist after the specified PointInTime', 16, 1)

  --> Getting the filename and the date of the last full backup prior to the PointInTime
  SELECT TOP 1
    @Filename = b.physical_device_name,
    @Full_BackupStartDate = backup_start_date
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'D'
    AND a.backup_start_date  Create temp-tables for file header information
  DECLARE @header table (
    BackupName nvarchar(128),
    BackupDescription nvarchar(255),
    BackupType smallint,
    ExpirationDate datetime,
    Compressed bit,
    Position smallint,
    DeviceType tinyint,
    UserName nvarchar(128),
    ServerName nvarchar(128),
    DatabaseName nvarchar(128),
    DatabaseVersion int,
    DatabaseCreationDate datetime,
    BackupSize numeric(20,0),
    FirstLSN numeric(25,0),
    LastLSN numeric(25,0),
    CheckpointLSN numeric(25,0),
    DatabaseBackupLSN numeric(25,0),
    BackupStartDate datetime,
    BackupFinishDate datetime,
    SortOrder smallint,
    CodePage smallint,
    UnicodeLocaleId int,
    UnicodeComparisonStyle int,
    CompatibilityLevel tinyint,
    SoftwareVendorId int,
    SoftwareVersionMajor int,
    SoftwareVersionMinor int,
    SoftwareVersionBuild int,
    MachineName nvarchar(128),
    Flags  int,
    BindingID uniqueidentifier,
    RecoveryForkID uniqueidentifier,
    Collation nvarchar(128),
    FamilyGUID uniqueidentifier,
    HasBulkLoggedData bit,
    IsSnapshot bit,
    IsReadOnly bit,
    IsSingleUser bit,
    HasBackupChecksums bit,
    IsDamaged bit,
    BeginsLogChain bit,
    HasIncompleteMetaData bit,
    IsForceOffline bit,
    IsCopyOnly bit,
    FirstRecoveryForkID uniqueidentifier,
    ForkPointLSN numeric(25,0) NULL,
    RecoveryModel nvarchar(60),
    DifferentialBaseLSN numeric(25,0) NULL,
    DifferentialBaseGUID uniqueidentifier,
    BackupTypeDescription nvarchar(60),
    BackupSetGUID uniqueidentifier NULL,
    CompressedBackupSize bigint,
    Seq int NOT NULL identity(1,1)
    )

  --> Create temp-table for db file information
  DECLARE @filelist TABLE  (
    LogicalName nvarchar(128),
    PhysicalName nvarchar(260),
    Type char(1),
    FileGroupName nvarchar(128),
    Size numeric(20,0),
    MaxSize numeric(20,0),
    FileID bigint,
    CreateLSN numeric(25,0),
    DropLSN numeric(25,0) NULL,
    UniqueID uniqueidentifier,
    ReadOnlyLSN numeric(25,0) NULL,
    ReadWriteLSN numeric(25,0) NULL,
    BackupSizeInBytes bigint,
    SourceBlockSize int,
    FileGroupID int,
    LogGroupGUID uniqueidentifier NULL,
    DifferentialBaseLSN numeric(25,0) NULL,
    DifferentialBaseGUID uniqueidentifier,
    IsReadOnly bit,
    IsPresent bit,
    TDEThumbprint varbinary(32),
    Seq int NOT NULL identity(1,1)
    )

  --> Get header and filelist information from the backup file
  INSERT INTO @header
  EXEC ('RESTORE HeaderOnly FROM DISK = ''' + @Filename + '''')

  INSERT INTO @filelist
  EXEC ('RESTORE FilelistOnly FROM DISK = ''' + @Filename + '''')

  --> Generate the full backup restore script
  SELECT
    @SQL = @SQL +
      CASE
        WHEN a.Seq = 1 THEN
          @cr + 'RESTORE DATABASE [' + c.DatabaseName + ']' +
          @cr + 'FROM DISK =' + @cr + @tab + '''' +
          @Filename + '''' + @cr + 'WITH'
        ELSE ''
      END
      + @cr + @tab + 'MOVE ''' + a.LogicalName + ''' TO ''' + a.PhysicalName + ''','
      +
      CASE
        WHEN a.Seq = b.Seq THEN
          @cr + @tab + 'REPLACE, STATS = 5, NORECOVERY'
        ELSE ''
      END
  FROM
    @filelist a
    CROSS JOIN
      (SELECT Seq = MAX(b1.Seq) FROM @filelist b1 ) b
    CROSS JOIN
      (SELECT DatabaseName = MAX(c1.DatabaseName) FROM @header c1) c
  ORDER BY
    a.Seq

  SELECT @SQL = @SQL + @cr + 'GO' + @cr + @cr

  --> Restore the last differential backup if it exists
  SELECT TOP 1
    @SQL = @SQL + 'RESTORE DATABASE [' + @DBName + '] FROM DISK = ''' + b.physical_device_name + ''' WITH NORECOVERY' + @cr + 'GO',
    @Diff_BackupStartDate = a.backup_start_date
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'I'
    AND a.backup_start_date > @Full_BackupStartDate
    AND a.backup_start_date < @PointInTime   ORDER BY a.backup_start_date DESC      IF @Diff_BackupStartDate IS NULL     SET @Diff_BackupStartDate = @Full_BackupStartDate        --> Generate all log restores except the last one
  SELECT
    @SQL = @SQL + 'RESTORE LOG [' + @DBName + '] FROM DISK = ''' + b.physical_device_name + ''' WITH NORECOVERY' + @cr + 'GO' + @cr,
    @Log_BackupStartDate = a.backup_start_date
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'L'
    AND a.backup_start_date > @Diff_BackupStartDate
    AND a.backup_start_date < @PointInTime   ORDER BY a.backup_start_date      --> Generate last log restore script with the stopat command and recovery
  SELECT TOP 1
    @SQL = @SQL + 'RESTORE LOG [' + @DBName + '] FROM DISK = ''' + b.physical_device_name + '''
      WITH RECOVERY, STOPAT = ''' + CONVERT(varchar(20), @PointInTime, 120) + '''' + @cr + 'GO' + @cr
  FROM msdb.dbo.backupset a
    INNER JOIN msdb.dbo.backupmediafamily b
      ON a.media_set_id = b.media_set_id
  WHERE a.database_name = @DBName
    AND a.type = 'L'
    AND a.backup_start_date > @PointInTime
  ORDER BY a.backup_start_date ASC

  PRINT @SQL

END TRY
BEGIN CATCH
  PRINT ERROR_MESSAGE()
END CATCH