In my last post, I demonstrated how using DBCC PAGE can be automated by using the “WITH TABLERESULTS” option. In this post, we will continue with another look at how this can be done.
On a nice wintry day, your city ended up being covered in several feet of snow. During the course of the night, your own house had several power outages. Being concerned about your databases, you shoveled your way into your office, so that you could check on things. (Okay… actually you would just VPN in, but this is my story after all…)
Once you get into your server, you check the jobs and find that your job that runs DBCC CHECKDB has failed. Let’s assume that a power glitch has caused corruption in your database. In order to find out what all is affected, you run DBCC CHECKDB WITH NO_INFOMSGS, ALL_ERRORMSGS. But, this happens to be on your 2TB database, and it will take a while for CHECKDB to finish so that you can find the scope of corruption.
You’d really like to know what tables are affected without having to wait. Luckily(?), this corruption was recorded in msdb.dbo.suspect_pages, and having just recently read Paul Randal’s post at here, we know we can use DBCC PAGE to determine this information. And, after having read my last blog post, you know that we can automate DBCC PAGE, so we can use our new friend “WITH TABLERESULTS” to find out what objects have been corrupted.
The suspect_pages table, documented here, has three particular columns of interest: database_id, file_id and page_id. These correspond nicely to the first three parameters needed for DBCC PAGE. To automate this, we need to know what information we need to return off of the page – and from Paul’s post, we know that this is the field “METADATA: ObjectId”. For this code example, let’s assume that this corruption is on page 11 of the master database (just change “master” to the name of your 2TB database).
IF OBJECT_ID('tempdb.dbo.#DBCCPAGE') IS NOT NULL DROP TABLE #DBCCPAGE; CREATE TABLE #DBCCPAGE ( ParentObject VARCHAR(255), [OBJECT] VARCHAR(255), Field VARCHAR(255), [VALUE] VARCHAR(255)); INSERT INTO #DBCCPAGE EXECUTE ('DBCC PAGE (''master'', 1, 11, 3) WITH TABLERESULTS;'); SELECT schema_name = OBJECT_SCHEMA_NAME(ca.OBJECT_ID), OBJECT_NAME = OBJECT_NAME(ca.OBJECT_ID) FROM #DBCCPAGE CROSS APPLY (SELECT CONVERT(INTEGER, VALUE)) ca(OBJECT_ID) WHERE Field = 'Metadata: ObjectId';
And there you go… you now know which object it is that has the corruption. In the same fashion, another interesting field that is returned is the IndexId – the Field value is “Metadata: IndexId”. It would be a similar exercise to grab that from this page also, an exercise that I’ll leave to you.
An automated method for getting the object from all suspect pages would entail encapsulating this logic into a cursor to spin through each row in the suspect_pages table (and I’ll even throw in getting the index_id also):
IF OBJECT_ID('tempdb.dbo.#DBCCPAGE') IS NOT NULL DROP TABLE #DBCCPAGE; IF OBJECT_ID('tempdb.dbo.#SuspectObjects') IS NOT NULL DROP TABLE #SuspectObjects; CREATE TABLE #DBCCPAGE ( ParentObject VARCHAR(255), [OBJECT] VARCHAR(255), Field VARCHAR(255), [VALUE] VARCHAR(255)); CREATE TABLE #SuspectObjects ( database_id INTEGER, FILE_ID INTEGER, page_id INTEGER, OBJECT_ID INTEGER, index_id INTEGER); DECLARE @database_id INTEGER, @FILE_ID INTEGER, @page_id INTEGER, @SQLCMD NVARCHAR(MAX); DECLARE cCrackSuspectPages CURSOR LOCAL FAST_FORWARD FOR SELECT 'EXECUTE (''DBCC PAGE (' + CONVERT(VARCHAR(15), database_id) + ', ' + CONVERT(VARCHAR(15), FILE_ID) + ', ' + CONVERT(VARCHAR(15), page_id) + ') WITH TABLERESULTS;'');', database_id, FILE_ID, page_id FROM msdb.dbo.suspect_pages; OPEN cCrackSuspectPages; FETCH NEXT FROM cCrackSuspectPages INTO @SQLCMD, @database_id, @FILE_ID, @page_id; WHILE @@FETCH_STATUS = 0 BEGIN TRUNCATE TABLE #DBCCPAGE; INSERT INTO #DBCCPAGE EXECUTE (@SQLCMD); INSERT INTO #SuspectObjects (database_id, FILE_ID, page_id, OBJECT_ID, index_id ) SELECT @database_id, @FILE_ID, @page_id, (SELECT CONVERT(INTEGER, VALUE) FROM #DBCCPAGE dp WHERE dp.Field = 'Metadata: ObjectId'), (SELECT CONVERT(INTEGER, VALUE) FROM #DBCCPAGE dp WHERE dp.Field = 'Metadata: IndexId'); FETCH NEXT FROM cCrackSuspectPages INTO @SQLCMD, @database_id, @FILE_ID, @page_id; END CLOSE cCrackSuspectPages; DEALLOCATE cCrackSuspectPages; SELECT database_name = DB_NAME(database_id), database_id, FILE_ID, page_id, schema_name = OBJECT_SCHEMA_NAME(OBJECT_ID, database_id), OBJECT_NAME = OBJECT_NAME(OBJECT_ID, database_id), index_id FROM #SuspectObjects;
If you happen to be on SQL 2012 or higher, this can be greatly simplified by using the new (undocumented) DMO function sys.dm_db_database_page_allocations (and it also takes away the need to crack the page using DBCC PAGE).
SELECT database_name = DB_NAME(sp.database_id), sp.database_id, sp.FILE_ID, sp.page_id, schema_name = OBJECT_SCHEMA_NAME(dpa.OBJECT_ID, sp.database_id), OBJECT_NAME = OBJECT_NAME(dpa.OBJECT_ID, sp.database_id), dpa.index_id FROM msdb.dbo.suspect_pages sp CROSS APPLY sys.dm_db_database_page_allocations(sp.database_id, NULL, NULL, NULL, 'LIMITED') dpa WHERE sp.FILE_ID = dpa.allocated_page_file_id AND sp.page_id = dpa.allocated_page_page_id;
And there we go – yet another time when you might want to automate using DBCC PAGE. By now you should be able to see other uses for it – as long as you can get the database_id, file_id and page_id, you can automate the usage of it to retrieve the information that you are looking for.