Database Corruption Challenge Week 7 – Alternate Solution
The alternate solution to the Database Corruption Challenge this week was created by Patrick Flynn. This solution is the only solution to successfully recover all the data without using any of the backups. If the challenge had been structured differently and all you had was just the database files and no backups, this solution would work just fine.
As you read through this solution, you will notice the use of Common Table Expressions throughout the solution. Patrick is the first person that I have seen write TSQL code that uses Common Table Expressions more than I do. (I mean that as a compliment Patrick)
To start with let’s add the UDF_Convert_Hex_to_Binary function into the master database to match what Patrick had in his system.
All code provided by Patrick, comments and screenshots by me.
-- Step 0 - Create Supporting function USE [master] GO CREATE FUNCTION [dbo].[UDF_Convert_Hex_to_Binary] ( @HEX VARBINARY(MAX) ) RETURNS VARCHAR(MAX) BEGIN DECLARE @BINARY VARCHAR(MAX) ;WITH N1 (n) AS (SELECT 1 UNION ALL SELECT 1), N2 (n) AS (SELECT 1 FROM N1 AS X, N1 AS Y), N3 (n) AS (SELECT 1 FROM N2 AS X, N2 AS Y), N4 (n) AS (SELECT ROW_NUMBER() OVER(ORDER BY X.n) FROM N3 AS X, N3 AS Y) SELECT @BINARY=ISNULL(@BINARY,'') + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 128) % 2) + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 64) % 2) + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 32) % 2) + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 16) % 2) + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 8) % 2) + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 4) % 2) + CONVERT(NVARCHAR(1), (SUBSTRING(@HEX, Nums.n, 1) / 2) % 2) + CONVERT(NVARCHAR(1), SUBSTRING(@HEX, Nums.n, 1) % 2) FROM N4 Nums WHERE Nums.n<=LEN(@HEX) RETURN @BINARY END GO
Once that function is created, let’s drop the previous CorruptionChallenge7 database, because this is probably not our first time running through the solution. If you have a different way of checking for a database and dropping it, please see my blog post related to this topic earlier today.
-- Step 1 Create database using supplied files USE master; GO IF DB_ID ('CorruptionChallenge7') IS NOT NULL BEGIN ALTER DATABASE CorruptionChallenge7 SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE CorruptionChallenge7 END
Now we will use the MDF, LDF, and NDF files provided in the solution, and attach them as the CorruptionChallenge7 database.
-- Attached Supplied Files (Will upgrade database to current version) CREATE DATABASE [CorruptionChallenge7] ON ( FILENAME = N'C:\SQL_DATA\CorruptionChallenge7.mdf' ), ( FILENAME = N'C:\SQL_DATA\CorruptionChallenge7_log.ldf' ), ( FILENAME = N'C:\SQL_DATA\UserObjects.ndf' ) FOR ATTACH GO
Now we have a database that we are ready to work on figuring out where the rows in the [Record1000] table went. So let’s see what we have in the current database log that hasn’t yet been backed up.
-- Step 2 Extract and Parse data from log file Use CorruptionChallenge7 GO Declare @TransactionID varchar(30); Declare @AllocUnitId BigInt; Select @TransactionID = TransactionID from ( SELECT TOP 1 [TRANSACTION ID] as TransactionID, [Begin Time] as BeginTime FROM sys.fn_dblog(NULL, NULL) WHERE Context IN ('LCX_NULL') AND Operation in ('LOP_BEGIN_XACT') And [Transaction Name] In ('DELETE') ORDER BY [Begin Time] Desc ) a; CREATE TABLE #RowData( [RowID] INT IDENTITY(1,1), [RowLogContents] [varbinary](8000) NULL, [AllocUnitID] [bigint] NULL, [TransactionID] [varchar](14) NOT NULL, [SlotId] [int] NULL, [FixedLengthData] [smallint] NULL, [TotalNoOfCols] [smallint], [NullBitMapLength] [smallint], [NullBytes] VARBINARY(8000), [TotalNoofVarCols] [smallint], [ColumnOffsetArray] VARBINARY(8000), [VarColumnStart] [smallint], [NullBitMap] varchar(128) ); WITH RowData(RowLogContents, AllocUnitID, TransactionID, SlotId, FixedLengthData) AS ( SELECT [RowLog Contents 0] ,[AllocUnitID] ,[Transaction ID] ,[Slot ID] ,CONVERT(SMALLINT,CONVERT(BINARY(2), REVERSE(SUBSTRING([RowLog Contents 0], 3, 2)))) FROM sys.fn_dblog(NULL, NULL) WHERE SUBSTRING([RowLog Contents 0], 1, 1)In (0x10,0x30,0x70) AND [Transaction ID] = @TransactionID ) INSERT INTO #RowData(RowLogContents, AllocUnitID, TransactionID, SlotId, FixedLengthData) Select RowLogContents ,AllocUnitID ,TransactionID ,SlotId ,FixedLengthData from RowData; SELECT * FROM #RowData;
Now there is a lot there to sort out.
Now to calculate the number of columns in each row deleted.
Update #RowData Set TotalNoOfCols = CONVERT(INT, CONVERT(BINARY(2), REVERSE(SUBSTRING(RowLogContents, FixedLengthData + 1, 2)))) from #RowData SELECT * FROM #RowData;
Now that the number of columns has been calculated we calculate the NullBitMapLength. All of these will be used later to extract the actual rows from the log data.
Update #RowData Set NullBitMapLength = -- [NullBitMapLength]=ceiling([Total No of Columns] /8.0) CONVERT(INT, ceiling(TotalNoOfCols/8.0)) from #RowData; SELECT * FROM #RowData;
Next the NullBytes and TotalNoofVarColumsn fields are calculated and filled in.
Update #RowData Set NullBytes = --[Null Bytes] = Substring (RowLog content 0, Status Bit A+ Status Bit B + [Fixed Length Data] +1, [NullBitMapLength] ) SUBSTRING(RowLogContents, FixedLengthData + 3, NullBitMapLength), TotalNoofVarCols = --[TotalNoofVarCols] = Substring (RowLog content 0, Status Bit A+ Status Bit B + [Fixed Length Data] +1, [Null Bitmap length] + 2 ) CASE WHEN SUBSTRING(RowLogContents, 1, 1) In (0x10,0x30,0x70) THEN CONVERT(INT, CONVERT(BINARY(2), REVERSE(SUBSTRING(RowLogContents, FixedLengthData + 3 + NullBitMapLength, 2)))) ELSE null END from #RowData; SELECT * FROM #RowData;
Three more fields to calculate, the ColumnOffsetArray, the VarColumnStart, and the NullBitmap.
Update #RowData Set ColumnOffsetArray = --[ColumnOffsetArray]= Substring (RowLog content 0, Status Bit A+ Status Bit B + [Fixed Length Data] +1, [Null Bitmap length] + 2 , [TotalNoofVarCols]*2 ) CASE WHEN SUBSTRING(RowLogContents, 1, 1) In (0x10,0x30,0x70) THEN SUBSTRING(RowLogContents, FixedLengthData + 3 + NullBitMapLength + 2, (CASE WHEN SUBSTRING(RowLogContents, 1, 1) In (0x10,0x30,0x70) THEN CONVERT(INT, CONVERT(BINARY(2), REVERSE(SUBSTRING(RowLogContents, FixedLengthData + 3 + NullBitMapLength, 2)))) ELSE null END) * 2) ELSE null END, VarColumnStart = -- Variable column Start = Status Bit A+ Status Bit B + [Fixed Length Data] + [Null Bitmap length] + 2+([TotalNoofVarCols]*2) CASE WHEN SUBSTRING(RowLogContents, 1, 1)In (0x10,0x30,0x70) THEN (FixedLengthData + 4 + NullBitMapLength + ((CASE WHEN SUBSTRING(RowLogContents, 1, 1) In (0x10,0x30,0x70) THEN CONVERT(INT, CONVERT(BINARY(2), REVERSE(SUBSTRING(RowLogContents, FixedLengthData + 3 + NullBitMapLength, 2)))) ELSE null END) * 2)) ELSE null End, NullBitMap = master.dbo.UDF_Convert_Hex_to_Binary(NullBytes) from #RowData; SELECT * FROM #RowData;
Now we need to figure out the columns for this table.
/* Parse Table */ CREATE TABLE #ColumnNameAndData ( [RowID] int, [Rowlogcontents] varbinary(Max), [ColumnName] sysname, [nullbit] smallint, [leaf_offset] smallint, [length] smallint, [system_type_id] tinyint, [bitpos] tinyint, [xprec] tinyint, [xscale] tinyint, [is_null] int, [ColumnValueSize] int, [ColumnLength] int, [hex_Value] varbinary(max), [SlotID] int, [Updated] int ); --Create common table expression and join it with the rowdata table -- to get each column details -- varlength data With ColumnNameInfo(RowID, Rowlogcontents, ColumnName, nullbit, leaf_offset, length, system_type_id, bitpos, xprec, xscale, is_null, VarColOffsetEnd, VarColOffsetStart, VarColumnStart, SlotId) as ( SELECT [RowID], Rowlogcontents, [NAME] , cols.leaf_null_bit AS nullbit, leaf_offset, ISNULL(syscolumns.length, cols.max_length) AS [length], cols.system_type_id, cols.leaf_bit_position AS bitpos, ISNULL(syscolumns.xprec, cols.precision) AS xprec, ISNULL(syscolumns.xscale, cols.scale) AS xscale, SUBSTRING([nullBitMap], cols.leaf_null_bit, 1) AS is_null, CONVERT(INT, CONVERT(BINARY(2), REVERSE (SUBSTRING ([ColumnOffsetArray], (2 * leaf_offset*-1) - 1, 2)))) as VarColOffsetEnd, CONVERT(INT, CONVERT(BINARY(2), REVERSE (SUBSTRING ([ColumnOffsetArray], (2 * ((leaf_offset*-1) - 1)) - 1, 2)))) as VarColOffsetStart, VarColumnStart, SlotId FROM #RowData A Inner Join sys.allocation_units allocunits On A.[AllocUnitId]=allocunits.[Allocation_Unit_Id] INNER JOIN sys.partitions partitions ON (allocunits.type IN (1, 3) AND partitions.hobt_id = allocunits.container_id) OR (allocunits.type = 2 AND partitions.partition_id = allocunits.container_id) INNER JOIN sys.system_internals_partition_columns cols ON cols.partition_id = partitions.partition_id LEFT OUTER JOIN syscolumns ON syscolumns.id = partitions.object_id AND syscolumns.colid = cols.partition_column_id WHERE leaf_offset<0 ), VarLengthData as ( Select c.*, [Column value Size] = (CASE WHEN leaf_offset<1 and is_null=0 THEN (Case When VarColOffsetEnd > 30000 THEN VarColOffsetEnd - POWER(2, 15) ELSE VarColOffsetEnd END) END), VarColOffsetStartAdj = ISNULL(NULLIF(VarColOffsetStart, 0), [VarColumnStart]) from ColumnNameInfo c ), HexDataInfo as ( Select vd.*, [Column Length] = (CASE WHEN leaf_offset<1 and is_null=0 THEN (Case When VarColOffsetEnd > 30000 And VarColOffsetStartAdj < 30000 THEN (Case When [system_type_id] In (35,34,99) Then 16 else 24 end) When VarColOffsetEnd >30000 And VarColOffsetStartAdj>30000 THEN (Case When [system_type_id] In (35,34,99) Then 16 else 24 end) --24 When VarColOffsetEnd <30000 And VarColOffsetStartAdj<30000 THEN (VarColOffsetEnd - VarColOffsetStartAdj) When VarColOffsetEnd <30000 And VarColOffsetStartAdj>30000 THEN POWER(2, 15) +VarColOffsetEnd - VarColOffsetStartAdj END) END), [VarColStart] = (Case When VarColOffsetEnd > 30000 THEN VarColOffsetEnd - POWER(2, 15) ELSE VarColOffsetEnd END) - (Case When VarColOffsetEnd > 30000 And VarColOffsetStartAdj < 30000 THEN (Case When [system_type_id] In (35,34,99) Then 16 else 24 end) When VarColOffsetEnd > 30000 And VarColOffsetStartAdj>30000 THEN (Case When [system_type_id] In (35,34,99) Then 16 else 24 end) When VarColOffsetEnd < 30000 And VarColOffsetStartAdj < 30000 THEN VarColOffsetEnd - VarColOffsetStartAdj When VarColOffsetEnd < 30000 And VarColOffsetStartAdj > 30000 THEN POWER(2, 15) +VarColOffsetEnd - VarColOffsetStartAdj END) + 1, VarDataLength = (Case When VarColOffsetEnd > 30000 And VarColOffsetStartAdj < 30000 THEN (Case When [system_type_id] In (35,34,99) Then 16 else 24 end) When VarColOffsetEnd > 30000 And VarColOffsetStartAdj > 30000 THEN (Case When [system_type_id] In (35,34,99) Then 16 else 24 end) When VarColOffsetEnd < 30000 And VarColOffsetStartAdj < 30000 THEN ABS(VarColOffsetEnd - VarColOffsetStartAdj) When VarColOffsetEnd < 30000 And VarColOffsetStartAdj > 30000 THEN POWER(2, 15) + VarColOffsetEnd - VarColOffsetStartAdj END) FROM VarLengthData vd ) INSERT INTO #ColumnNameAndData(RowID, Rowlogcontents, ColumnName, nullbit, leaf_offset, [length],system_type_id, bitpos, xprec, xscale, is_null, ColumnValueSize, ColumnLength, hex_Value, SlotID, Updated) Select hd.RowID, hd.Rowlogcontents, hd.ColumnName, hd.nullbit, hd.leaf_offset, hd.[length], hd.system_type_id, hd.bitpos, hd.xprec, hd.xscale, hd.is_null, hd.[Column value Size], hd.[Column Length], hex_Value = (CASE WHEN is_null=1 AND VarDataLength = 0 THEN NULL ELSE SUBSTRING(Rowlogcontents, VarColStart, VarDataLength) END), hd.SlotId, 0 as Updated FROM HexDataInfo hd; SELECT * FROM #ColumnNameAndData;
Now to add some more column information.
INSERT INTO #ColumnNameAndData(RowID, Rowlogcontents, ColumnName, nullbit, leaf_offset, [length],system_type_id, bitpos, xprec, xscale, is_null, ColumnValueSize, ColumnLength, hex_Value, SlotID, Updated) SELECT [RowID], Rowlogcontents, [NAME] , cols.leaf_null_bit AS nullbit, leaf_offset, ISNULL(syscolumns.length, cols.max_length) AS [length], cols.system_type_id, cols.leaf_bit_position AS bitpos, ISNULL(syscolumns.xprec, cols.precision) AS xprec, ISNULL(syscolumns.xscale, cols.scale) AS xscale, SUBSTRING([nullBitMap], cols.leaf_null_bit, 1) AS is_null, ( SELECT TOP 1 ISNULL(SUM(CASE WHEN C.leaf_offset >1 THEN max_length ELSE 0 END),0) FROM sys.system_internals_partition_columns C WHERE cols.partition_id =C.partition_id And C.leaf_null_bit<cols.leaf_null_bit )+5 AS [Column value Size], syscolumns.length AS [Column Length] ,CASE WHEN SUBSTRING([nullBitMap], cols.leaf_null_bit, 1) =1 THEN NULL ELSE SUBSTRING(Rowlogcontents,(SELECT TOP 1 ISNULL(SUM(CASE WHEN C.leaf_offset >1 And C.leaf_bit_position=0 THEN max_length ELSE 0 END),0) FROM sys.system_internals_partition_columns C where cols.partition_id =C.partition_id And C.leaf_null_bit<cols.leaf_null_bit)+5 ,syscolumns.length) END AS hex_Value ,[SlotID] ,0 FROM #RowData A Inner Join sys.allocation_units allocunits ON A.[AllocUnitId]=allocunits.[Allocation_Unit_Id] INNER JOIN sys.partitions partitions ON (allocunits.type IN (1, 3) AND partitions.hobt_id = allocunits.container_id) OR (allocunits.type = 2 AND partitions.partition_id = allocunits.container_id) INNER JOIN sys.system_internals_partition_columns cols ON cols.partition_id = partitions.partition_id LEFT OUTER JOIN syscolumns ON syscolumns.id = partitions.object_id AND syscolumns.colid = cols.partition_column_id WHERE leaf_offset>0; SELECT * FROM #ColumnNameAndData;
Next we create a place to store the recovered data and fill it in.
CREATE TABLE [#RecoverdData] ( [ColumnName] VARCHAR(MAX), [ColOrder] SMALLINT, [FieldValue] NVARCHAR(MAX), [Rowlogcontents] VARBINARY(8000), [RowID] int ) INSERT INTO #RecoverdData(ColumnName, ColOrder, FieldValue, Rowlogcontents, RowID) SELECT ColumnName, nullbit, CASE WHEN system_type_id IN (231, 239) THEN LTRIM(RTRIM(CONVERT(NVARCHAR(max),hex_Value))) --NVARCHAR ,NCHAR WHEN system_type_id IN (167,175) THEN LTRIM(RTRIM(CONVERT(VARCHAR(max),hex_Value))) --VARCHAR,CHAR WHEN system_type_id IN (35) THEN LTRIM(RTRIM(CONVERT(VARCHAR(max),hex_Value))) --Text WHEN system_type_id IN (99) THEN LTRIM(RTRIM(CONVERT(NVARCHAR(max),hex_Value))) --nText WHEN system_type_id = 48 THEN CONVERT(VARCHAR(MAX), CONVERT(TINYINT, CONVERT(BINARY(1), REVERSE (hex_Value)))) --TINY INTEGER WHEN system_type_id = 52 THEN CONVERT(VARCHAR(MAX), CONVERT(SMALLINT, CONVERT(BINARY(2), REVERSE (hex_Value)))) --SMALL INTEGER WHEN system_type_id = 56 THEN CONVERT(VARCHAR(MAX), CONVERT(INT, CONVERT(BINARY(4), REVERSE(hex_Value)))) -- INTEGER WHEN system_type_id = 127 THEN CONVERT(VARCHAR(MAX), CONVERT(BIGINT, CONVERT(BINARY(8), REVERSE(hex_Value))))-- BIG INTEGER WHEN system_type_id = 61 Then CONVERT(VARCHAR(MAX),CONVERT(DATETIME,CONVERT(VARBINARY(8000),REVERSE (hex_Value))),100) --DATETIME WHEN system_type_id =58 Then CONVERT(VARCHAR(MAX),CONVERT(SMALLDATETIME,CONVERT(VARBINARY(8000),REVERSE(hex_Value))),100) --SMALL DATETIME WHEN system_type_id = 108 THEN CONVERT(VARCHAR(MAX),CONVERT(NUMERIC(38,20), CONVERT(VARBINARY,CONVERT(VARBINARY(1),xprec)+CONVERT(VARBINARY(1),xscale))+CONVERT(VARBINARY(1),0) + hex_Value)) --- NUMERIC WHEN system_type_id =106 THEN CONVERT(VARCHAR(MAX), CONVERT(DECIMAL(38,20), CONVERT(VARBINARY,Convert(VARBINARY(1),xprec)+CONVERT(VARBINARY(1),xscale))+CONVERT(VARBINARY(1),0) + hex_Value)) --- DECIMAL WHEN system_type_id In(60,122) THEN CONVERT(VARCHAR(MAX),Convert(MONEY,Convert(VARBINARY(8000),Reverse(hex_Value))),2) --MONEY,SMALLMONEY WHEN system_type_id = 104 THEN CONVERT(VARCHAR(MAX),CONVERT (BIT,CONVERT(BINARY(1), hex_Value)%2)) -- BIT WHEN system_type_id =62 THEN RTRIM(LTRIM(STR(CONVERT(FLOAT,SIGN(CAST(CONVERT(VARBINARY(8000),Reverse(hex_Value)) AS BIGINT)) * (1.0 + (CAST(CONVERT(VARBINARY(8000),Reverse(hex_Value)) AS BIGINT) & 0x000FFFFFFFFFFFFF) * POWER(CAST(2 AS FLOAT), -52)) * POWER(CAST(2 AS FLOAT),((CAST(CONVERT(VARBINARY(8000),Reverse(hex_Value)) AS BIGINT) & 0x7ff0000000000000) / EXP(52 * LOG(2))-1023))),53,LEN(hex_Value)))) --- FLOAT When system_type_id =59 THEN Left(LTRIM(STR(CAST(SIGN(CAST(Convert(VARBINARY(8000),REVERSE(hex_Value)) AS BIGINT))* (1.0 + (CAST(CONVERT(VARBINARY(8000),Reverse(hex_Value)) AS BIGINT) & 0x007FFFFF) * POWER(CAST(2 AS Real), -23)) * POWER(CAST(2 AS Real),(((CAST(CONVERT(VARBINARY(8000),Reverse(hex_Value)) AS INT) )& 0x7f800000)/ EXP(23 * LOG(2))-127))AS REAL),23,23)),8) --Real WHEN system_type_id In (165,173) THEN (CASE WHEN CHARINDEX(0x,cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'VARBINARY(8000)')) = 0 THEN '0x' ELSE '' END) +cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'varchar(max)') -- BINARY,VARBINARY WHEN system_type_id =34 THEN (CASE WHEN CHARINDEX(0x,cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'VARBINARY(8000)')) = 0 THEN '0x' ELSE '' END) +cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'varchar(max)') --IMAGE WHEN system_type_id =36 THEN CONVERT(VARCHAR(MAX),CONVERT(UNIQUEIDENTIFIER,hex_Value)) --UNIQUEIDENTIFIER WHEN system_type_id =231 THEN CONVERT(VARCHAR(MAX),CONVERT(sysname,hex_Value)) --SYSNAME WHEN system_type_id =241 THEN CONVERT(VARCHAR(MAX),CONVERT(xml,hex_Value)) --XML WHEN system_type_id =189 THEN (CASE WHEN CHARINDEX(0x,cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'VARBINARY(8000)')) = 0 THEN '0x' ELSE '' END) +cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'varchar(max)') --TIMESTAMP WHEN system_type_id=98 THEN (CASE WHEN CONVERT(INT,SUBSTRING(hex_Value,1,1))=56 THEN CONVERT(VARCHAR(MAX), CONVERT(INT, CONVERT(BINARY(4), REVERSE(Substring(hex_Value,3,Len(hex_Value)))))) -- INTEGER WHEN CONVERT(INT,SUBSTRING(hex_Value,1,1))=108 THEN CONVERT(VARCHAR(MAX),CONVERT(numeric(38,20),CONVERT(VARBINARY(1),Substring(hex_Value,3,1)) +CONVERT(VARBINARY(1),Substring(hex_Value,4,1))+CONVERT(VARBINARY(1),0) + Substring(hex_Value,5,Len(hex_Value)))) --- NUMERIC WHEN CONVERT(INT,SUBSTRING(hex_Value,1,1))=167 THEN LTRIM(RTRIM(CONVERT(VARCHAR(max),Substring(hex_Value,9,Len(hex_Value))))) --VARCHAR,CHAR WHEN CONVERT(INT,SUBSTRING(hex_Value,1,1))=36 THEN CONVERT(VARCHAR(MAX),CONVERT(UNIQUEIDENTIFIER,Substring((hex_Value),3,20))) --UNIQUEIDENTIFIER WHEN CONVERT(INT,SUBSTRING(hex_Value,1,1))=61 THEN CONVERT(VARCHAR(MAX),CONVERT(DATETIME,CONVERT(VARBINARY(8000),REVERSE (Substring(hex_Value,3,LEN(hex_Value)) ))),100) --DATETIME WHEN CONVERT(INT,SUBSTRING(hex_Value,1,1))=165 THEN '0x'+ SUBSTRING((CASE WHEN CHARINDEX(0x,cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'VARBINARY(8000)')) = 0 THEN '0x' ELSE '' END) +cast('' AS XML).value('xs:hexBinary(sql:column("hex_Value"))', 'varchar(max)'),11,LEN(hex_Value)) -- BINARY,VARBINARY END) END AS FieldValue ,[Rowlogcontents] ,[RowID] FROM #ColumnNameAndData ORDER BY RowID, nullbit SELECT * FROM #RecoverdData;
Now let’s test the query to pull data out of the #RecoverdData table and PIVOT it to something we can use.
SELECT [Record1000Id],[FireDeptID],[FireDeptState],[AlarmDate],[IncidentNumber],[ExposureNumberZeroBased],[RecordType],[TransactionType],[FireDepartmentStation] FROM ( Select ColumnName, FieldValue, RowId FROM #RecoverdData ) a PIVOT (Min([FieldValue]) FOR ColumnName IN ([Record1000Id],[FireDeptID],[FireDeptState],[AlarmDate],[IncidentNumber],[ExposureNumberZeroBased],[RecordType],[TransactionType],[FireDepartmentStation])) AS pvt
Now its starting to look like something we can use.
Since the data looks good, we now put it back into the table it was originally deleted from.
-- Step 3 Insert data back into table -- Insert into table SET IDENTITY_INSERT [OPEN_NFIRS].[Record1000] ON; GO INSERT into [OPEN_NFIRS].[Record1000]([Record1000Id],[FireDeptID],[FireDeptState],[AlarmDate],[IncidentNumber],[ExposureNumberZeroBased],[RecordType],[TransactionType],[FireDepartmentStation]) SELECT [Record1000Id],[FireDeptID],[FireDeptState],[AlarmDate],[IncidentNumber],[ExposureNumberZeroBased],[RecordType],[TransactionType],[FireDepartmentStation] FROM ( Select ColumnName, FieldValue, RowId FROM #RecoverdData ) a PIVOT (Min([FieldValue]) FOR ColumnName IN ([Record1000Id],[FireDeptID],[FireDeptState],[AlarmDate],[IncidentNumber],[ExposureNumberZeroBased],[RecordType],[TransactionType],[FireDepartmentStation])) AS pvt SET IDENTITY_INSERT [OPEN_NFIRS].[Record1000] OFF;
Data restored, and all is good, now we run the checksum query to confirm, and … we get the right results.
SELECT COUNT([Record1000Id]) as check1 ,CHECKSUM_AGG(CHECKSUM([FireDeptID])) as check2 ,CHECKSUM_AGG(CHECKSUM([FireDeptState])) as check3 ,CHECKSUM_AGG(CHECKSUM([AlarmDate])) as check4 ,CHECKSUM_AGG(CHECKSUM([IncidentNumber])) as check5 ,CHECKSUM_AGG(CHECKSUM([ExposureNumberZeroBased])) as check6 ,CHECKSUM_AGG(CHECKSUM([RecordType])) as check7 ,CHECKSUM_AGG(CHECKSUM([TransactionType])) as check8 ,CHECKSUM_AGG(CHECKSUM([FireDepartmentStation])) as check9 FROM [OPEN_NFIRS].[Record1000];
Clean things up.
-- Step 4 Cleanup Drop Table #RowData Drop Table #ColumnNameAndData Drop Table #RecoverdData
And there you have it, recovering 3010 delete rows without going to a backup. In fact this solution, assuming the database already existed and didn’t need to be attached, could be done with no downtime, and without bumping any users out of the system.
You can download the PatrickFlynn2NoRestore.zip.
Nice solution Patrick!
More from Stedman Solutions:
Steve and the team at Stedman Solutions are here for all your SQL Server needs.
Contact us today for your free 30 minute consultation..
We are ready to help!
Hi Steve.
I would like to add a clarification to the post.
Most of this solution is based around code and posts done by Muhammad Imran (raresql.com)
In particular: http://raresql.com/2011/10/22/how-to-recover-deleted-data-from-sql-sever/
The credit for developing this code belongs with him.
My main efforts were to
(a) Modify the code for the particular situation
(b) Split out much of the repeated calculations in order to make the code more readable and understandable
Really great solution. This week there are a lot of things to learn … thanks Steve for challenge and thanks Patrick for sharing your solution … ad finally thanks to Muhammad Imran too
enjoy your SQL
paSQuaLe