Corruption Challenge 4 – Alternate Solution

Download PDF

Here is the solution provided by Patrick Flynn. Congratulations Patrick for being one of this weeks winners.

I really liked this solution for a number  of reasons, first it showed how to pull data from DBCC Page and actually convert it into rows and columns that could then be inserted back into a table. Secondly due to its use of Common Table Expressions.

 

This was done using SQL Server 2014.

Approach used was:

(a) Restore and check database – Corruption is related to Allocation Meta Data for dbo.Customers

(b) Use the non-clustered indexes on FirstName and LastName to recover data apart from Middle name – Total of 511740 rows in Table
Steps are in CorruptionChallange Setup.sql (requires disabling Database triggers

(c) Cannot directly select data from pages due to allocation issues but can select raw data vis DBCC Page (and DBCC IND)
Requires using DBCC Ind to get list of Data pages and DBCC Page to extract data
Steps to extract and Parse data are in DBCCPage_Extract

(d) After checking data was match by Id, FirstName and Last Name restore data into Customer table
This requires temporarily disabling FK on Orders table

(e) Check Database Corruption fixed

(f) Cleanup objects created and re-enable FK’s and Triggers
Steps are in CorruptionChallange_Cleanup.sql

Start by restoring the database. Of course Patrick was using different paths, so I had to modify the file paths to work on my test server.

Step 1 restore the database.

USE master;
GO

RESTORE DATABASE [CorruptionChallenge4]
   FROM DISK = N'C:\DBBackups\CorruptionChallenge4_Corrupt.bak'
   WITH FILE = 1,
        MOVE N'CorruptionChallenge4'
		  TO N'C:\SQL_DATA\CorruptionChallenge4.mdf',
        MOVE N'UserObjects'
		  TO N'C:\SQL_DATA\CorruptionChallenge4_UserObjects.ndf',
        MOVE N'CorruptionChallenge4_log'
		  TO N'C:\SQL_DATA\CorruptionChallenge4_log.ldf',
        NOUNLOAD,
		REPLACE,
		STATS = 5;
GO


Patrick1

Step2 DBCC CheckDB().
The next thing to try is DBCC CheckDB, however when I run this, it eventually crashes out and drops the connection to the SQL Server from SSMS.

USE [CorruptionChallenge4]
GO
DBCC CHECKDB('CorruptionChallenge4') WITH ALL_ERRORMSGS, NO_INFOMSGS

If DBCC CheckDB is run, and then cancelled after 6 seconds, it already has returned 78,000 lines of output ending with:
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 249. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 250. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 251. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 252. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 253. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 254. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 255. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 256. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 257. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 258. Column ‘subid’ was created NOT NULL, but is NULL in the row.
Msg 8970, Level 16, State 1, Line 1
Row error: Object ID 29, index ID 1, partition ID 281474978611200, alloc unit ID 1900544 (type In-row data), page ID (3:192), row ID 259. Column ‘subid’ was created NOT NULL, but is NULL in the row.
CHECKDB found 0 allocation errors and 39344 consistency errors in table ‘sys.sysprivs’ (object ID 29).
CHECKDB found 0 allocation errors and 39344 consistency errors in database ‘CorruptionChallenge4’.
repair_allow_data_loss is the minimum repair level for the errors found by DBCC CHECKDB (CorruptionChallenge4).
Query was cancelled by user.

Now on to do something about that corruption.
Step 3 Check Current User Objects – Before == 5

 

SELECT Count(*)
FROM sys.objects
WHERE is_ms_shipped = 0;

Patrick2

Step 4 Errors are related to Clustered Index
Can use the Non clustered indexes to recover all data except MiddleName
Require to disable Database Triggers

DISABLE TRIGGER [noDropTables] ON DATABASE;
GO
DISABLE TRIGGER [noNewTables] ON DATABASE;
GO

CREATE TABLE [dbo].[Customers_Copy](
	[id] [int] NOT NULL,
	[FirstName] [varchar](30) NULL,
	[MiddleName] [varchar](30) NULL,
	[LastName] [varchar](30) NULL,
 CONSTRAINT [PK_Customers_1] PRIMARY KEY CLUSTERED
(
	[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
       IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
       ALLOW_PAGE_LOCKS = ON) ON [UserObjects]
) ON [UserObjects]

GO

— GET what we can from the indexes.

INSERT INTO [dbo].[Customers_Copy]
     (id, FirstName, MiddleName, LastName)
SELECT a.id, a.FirstName, '' as MiddleName, b.LastName
FROM
(
	SELECT [id]
      ,[FirstName]
	FROM [CorruptionChallenge4].[dbo].[Customers]
	WITH (INDEX(ncCustomerFirstname))
) a
INNER JOIN
(
	SELECT [id]
      ,[LastName]
	FROM [CorruptionChallenge4].[dbo].[Customers]
        WITH (INDEX(ncCustomerLastname))
) b
ON a.id = b.id;

511740 rows recovered


SELECT *
FROM [dbo].[Customers_Copy];

Shows that we get back the data, but no middle names.

Patrick3

 

— Using DBCC Page data is intact in page but Allocation Data is incorrect
— Can extract data via DBCC Page and Parse Data to obtain MiddleName
— Create Table to store data from DBCC Page

Use CorruptionChallenge4
GO
CREATE TABLE [dbo].[Customers_Copy1](
	[id] [int] NOT NULL,
	[FirstName] [varchar](30) NULL,
	[MiddleName] [varchar](30) NULL,
	[LastName] [varchar](30) NULL,
 CONSTRAINT [PK_Customers_2] PRIMARY KEY CLUSTERED
(
	[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
       IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
       ALLOW_PAGE_LOCKS = ON) ON [UserObjects]
) ON [UserObjects];

Now create some temp tables, and use a cursor to fill them up using DBCC page.

DECLARE
	@DBName VARCHAR(256) = 'CorruptionChallenge4',
	@FileId INT = 3,
	@PageId INT,
	@DumpStyle int = 1;

DROP TABLE #DBCCIND;
CREATE TABLE #DBCCIND
(
  PageFID INT,
  PagePID INT,
  IAMFID INT,
  IAMPID INT,
  ObjectID INT,
  IndexID INT,
  PartitionNumber INT,
  PartitionID BIGINT,
  iam_chain_type VARCHAR(100),
  PageType INT,
  IndexLevel INT,
  NextPageFID INT,
  NextPagePID INT,
  PrevPageFID INT,
  PrevPagePID INT
);

/* create a temp table to hold dbcc page results */
DROP TABLE #DBCC_PAGE;
CREATE TABLE #DBCC_Page
(
parentObject varchar(200),
object varchar(200),
field varchar(200),
value varchar(200)
)
DROP TABLE #DBCC_Data;

CREATE TABLE #DBCC_Data
(
	PageId int,
	parentObject varchar(200),
	value varchar(100),
	I1 int,
	I2 int,
	I3 int
)

INSERT INTO #DBCCIND
  EXEC ('DBCC IND(''CorruptionChallenge4'', ''dbo.Customers'', 1)');

DECLARE @STR VARCHAR (2000) 

DECLARE db_cursor CURSOR FOR
SELECT PagePID
FROM #DBCCIND
WHERE PageType = 1 

OPEN db_cursor
FETCH NEXT FROM db_cursor INTO @PageId   

WHILE @@FETCH_STATUS = 0
BEGIN
	SET @STR='DBCC PAGE (' + @DBName + ',' + Cast(@FileId as varchar(3)) + ','+
		Cast(@PageId as varchar(100)) + ','+
		Cast(@DumpStyle as varchar(100)) + ') with tableresults'

	/* store the output of dbcc page into the temp table */
	insert into #DBCC_Page exec(@STR) 

	INSERT INTO #DBCC_Data(PageId, parentObject, value, I1, I2, I3)
	Select
		@PageId as PageId,
		parentObject,
		value,
		CHARINDEX('Offset', parentObject, 1) as I1,
		CHARINDEX('Length', parentObject, 1) as I2,
		CHARINDEX('DumpStyle', parentObject, 1) as I3
	from #DBCC_Page
	WHERE object like 'Memory Dump %'

	DELETE FROM #DBCC_Page;

    FETCH NEXT FROM db_cursor INTO @PageId
END   

CLOSE db_cursor
DEALLOCATE db_cursor;

Patrick4

So now all the output from DBCC Page has been stored in the #DBCC_Page table. Let’s take a look at what’s there.

SELECT *
  FROM #DBCC_Data;
 

Patrick5

Now to translate the data and store it into a table called tmpCustomerData.

WITH Translate as
(
SELECT
	PageId,
	SUBSTRING(parentObject, 5, I1-7) as Slot,
	SUBSTRING(parentObject, I2+7, I3-I2-9) as Length,
	SUBSTRING(value,1,16) as LengthHex,
	SUBSTRING(value,20,45) as RawValues,
	SUBSTRING(value,67,20) as StringValues
FROM #DBCC_Data
), OrderedData as
(
	Select
		PageId,
		Slot,
		Length,
		LengthHex,
		RawValues,
		StringValues,
		ROW_NUMBER() Over (Partition By PageId,
Slot Order By LengthHex) as RowNum
	FROM Translate
)
SELECT
	a.PageId,
	a.Slot,
	a.Length,
	a.RawValues + ' ' + b.RawValues as HexValues,
	a.StringValues + b.StringValues as StringValues
INTO tmpCustomerData
FROM
(
	Select PageId, Slot, Length, RawValues, StringValues, RowNum
	from OrderedData
	WHERE RowNum =1
) a
INNER JOIN
(
	Select PageId, Slot, Length, RawValues, StringValues, RowNum
	from OrderedData
	WHERE RowNum = 2
) b
ON a.Slot = b.Slot
AND a.PageId  = b.PageId
ORDER BY a.PageId, a.Slot;
GO

Let’s take a look at the contents of the [tmpCustomerData] table.

SELECT *
  FROM tmpCustomerData;

Patrick6

The next step requires this function HexStrToVarBinary, so we will create it now.

CREATE FUNCTION dbo.HexStrToVarBinary(@hexstr varchar(8000))
RETURNS varbinary(8000)
AS
BEGIN
    DECLARE @hex char(1), @i int, @place bigint, @a bigint
    SET @i = LEN(@hexstr) 

    SET @place = convert(bigint,1)
    SET @a = convert(bigint, 0)

    WHILE (@i > 0 AND (SUBSTRING(@hexstr, @i, 1) like '[0-9A-Fa-f]'))
    BEGIN
        SET @hex = SUBSTRING(@hexstr, @i, 1)
        SET @a = @a +
    convert(bigint, CASE WHEN @hex LIKE '[0-9]'
         THEN CAST(@hex as int)
         ELSE CAST(ASCII(UPPER(@hex))-55 as int) end * @place)
		SET @place = @place * convert(bigint,16)
        SET @i = @i - 1
    END
    RETURN convert(varbinary(8000),@a)
END;
GO

Then using our new function, and another Common Table Expression, the data is extracted and placed into a table called [Customer_Copy1].

WITH Data AS
(
	SELECT
	PageId, Slot, Length, HexValues, StringValues,
	CAST(dbo.HexStrToVarBinary('0x' + SUBSTRING(HexValues, 17, 2) + SUBSTRING(HexValues, 15,2) + SUBSTRING(HexValues, 13,2) + SUBSTRING(HexValues, 11,2)) AS INT) as id,
	CAST(dbo.HexStrToVarBinary('0x' + SUBSTRING(HexValues, 33, 2) + SUBSTRING(HexValues, 31,2)) AS INT) as V1,
	CAST(dbo.HexStrToVarBinary('0x' + SUBSTRING(HexValues, 38, 2) + SUBSTRING(HexValues, 35,2)) AS INT) as V2
	FROM   tmpCustomerData
)
INSERT INTO [dbo].[Customers_Copy1](id, FirstName, MiddleName, LastName)
SELECT Data.id,
	   SUBSTRING(StringValues,20, V1-19),
	   SUBSTRING(StringValues,V1+1, V2-V1),
	   SUBSTRING(StringValues,V2+1, 50)
  FROM Data;

Check the contents of the [Customers_Copy1] table.

SELECT *
  FROM Customers_Copy1;

Patrick7

Compare to data extracted from Non-Clustered, and they all match on the id.

SELECT Count(*)
FROM Customers_Copy1 c1
INNER JOIN Customers_Copy c2
	ON c1.id = c2.id;
 

— 511740 rows matching
Now to see how many names don’t match.

SELECT Count(*)
FROM Customers_Copy1 c1
INNER JOIN Customers_Copy c2
	ON c1.id = c2.id
WHERE c1.FirstName <> c2.FirstName
OR c2.LastName <> c2.LastName;

Patrick8

Time to clean up and put all the data back into the original table.
Cleanup Step1 Restore Data to original Table (need to disableFK)

USE [CorruptionChallenge4]
GO

ALTER TABLE [dbo].[Orders] DROP CONSTRAINT [FK_Orders_People]
GO

SET IDENTITY_INSERT Customers ON

TRUNCATE TABLE [dbo].[Customers]

INSERT INTO [dbo].[Customers](id, FirstName, MiddleName, LastName)
SELECT
	id, FirstName, MiddleName, LastName
FROM [dbo].[Customers_Copy1];

SET IDENTITY_INSERT Customers OFF

Now confirm that the corruption is fixed.

DBCC CHECKDB('CorruptionChallenge4') WITH ALL_ERRORMSGS, NO_INFOMSGS

Put back everything we removed.

ALTER TABLE [dbo].[Orders]  WITH CHECK ADD  CONSTRAINT [FK_Orders_People] FOREIGN KEY([customerId])
REFERENCES [dbo].[Customers] ([id])
GO

ALTER TABLE [dbo].[Orders] CHECK CONSTRAINT [FK_Orders_People]
GO

-- (b) Drop Temporary objects
USE [CorruptionChallenge4];
GO

DROP TABLE [dbo].[Customers_Copy];
DROP TABLE [dbo].[Customers_Copy1];
DROP TABLE [dbo].[tmpCustomerData];
DROP FUNCTION [dbo].[HexStrToVarBinary];
GO

--(c) Restore Triggers
ENABLE TRIGGER [noDropTables] ON DATABASE;
ENABLE TRIGGER [noNewTables] ON DATABASE;

And finally the check needed to submit the challenge this week.

-- (d) Check Object Count restored to 5
SELECT Count(*)
FROM sys.objects
WHERE is_ms_shipped = 0;

-- (e) Check Data
Select * from dbo.Customers
WHERE id in (510900, 510901)

--510900	Steve	M	Stedman
--510901	William	V	STARK

And that’s how it’s done if you want to rip through the data pages with DBCC Page. Nice work Patrick.

 

More from Stedman Solutions:

SteveStedman5
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!

Leave a Reply

Your email address will not be published. Required fields are marked *

*