En algunas ocasiones puntuales el uso de los comandos DBCC PAGE y DBCC WRITEPAGE nos puede ser de utilidad. El uso habitual de este tipo de comandos no es recomendable pero sí es de gran utilidad con fines educativos. Por ejemplo para entender mejor los distintos tipos de página, poder obtener información de bajo nivel, simular escenarios de corrupción de datos, etc.

En este artículo vamos a exponer una forma de generar comandos DBCC WRITEPAGE de forma bastante sencilla. El objetivo es poder, sin recurrir a editores hexadecimales externos, realizar copias de páginas completas o de fragmentos de éstas, incluso entre bases de datos diferentes. Debemos advertir que un mal uso o error en la ejecución de este tipo de comandos DBCC puede acarrear graves problemas para la base de datos. Es necesario extremar las precauciones y siempre disponer de una copia de la base de datos antes de comenzar.

Una vez dadas las advertencias pertienntes, vamos a crear un procedimiento que a partir de una base de datos (@dbidIN), un fichero (@fileidIN) y una página (@pageidIN) nos genere un comando DBCC WRITEPAGE para escribir en la base de datos de salida (@dbidOUT), en el fichero de salida (@fileidOUT) y en la página de salida (@pageidOUT) la página correspondiente. Por defecto si no indicamos los parámetros OUT se nos generará el comando con los mismos valores de la entrada por lo que se sobreescribiría la página original.

Adicionalmente podremos suministrar un offset (@offset) y el número de bytes a escribir (@bytes) así como si queremos hacer uso de I/O directa a disco (por defecto, @directIO=1) o a través del buffer pool (@directIO=0). Si nuestra intención es modificar el contenido de la página y queremos que se calcule el checksum correctamente deberemos usar la opción directIO=0 ya que el cálculo del checksum se produce al realizar el flush a disco desde el buffer pool.

El código del procedimiento descrito es el siguiente:

 

CREATE PROCEDURE generate_dbcc_writepage (
@dbidIN int, @fileidIN int, @pageidIN int,
@dbidOUT int=null, @fileidOUT int=null, @pageidOUT int =null,
@offset int = null, @bytes int = null, @directIO bit = 1)
AS
BEGIN
DECLARE @dbid int = @dbidIN
DECLARE @fileid int = @fileidIN
DECLARE @pageid int = @pageidIN
-- Tabla temporal para contener los datos de la página origen
CREATE TABLE #dbcc (id int identity(1,1), offset int null, len int null, page int null, parentobject nvarchar(max),object nvarchar(max),field nvarchar(max),value nvarchar(max))
-- Creamos el comando DBCC para leer la página origen
DECLARE @sql varchar(max)= 'DBCC PAGE(' + convert(varchar(max),@dbid) + ',' + convert(varchar(max),@fileid) + ',' + convert(varchar(max),@pageid) + ', 2) WITH TABLERESULTS'
DBCC TRACEON(3604)
INSERT INTO #dbcc (parentobject,object,field,value)
EXEC (@sql)
DBCC TRACEOFF(3604)
-- Eliminamos todo lo que no nos interese de la salida del comando DBCC
delete
from #dbcc
where object not like 'Memory Dump%'
-- Si tenemos parámetros de escritura distintos actualizamos las variables
if @dbidOUT is not null
SET @dbid=@dbidOUT
if @fileidOUT is not null
SET @fileid=@fileidOUT
if @pageidOUT is not null
SET @pageid=@pageidOUT
--Obtenemos el array de bytes de la página concatenando los bytes del 21 al 45 que siguen el formato siguiente: 00000000 00000000 00000000 00000000 00000000
DECLARE @data nvarchar(max) = ''
SELECT @data=@data+replace(substring(value,21,45),' ','')
from #dbcc
-- Si no hemos especificado offset y número de bytes volcamos la página entera
if @offset is null
set @offset=0
if @bytes is null
set @bytes=8192
-- Cortamos los datos en base al offset y los bytes a copiar
SET @data=substring(@data,@offset*2,@bytes*2)
-- Generamos finalmente el comando DBCC WRITEPAGE correspondiente
select 'DBCC WRITEPAGE (' +
convert(varchar(max),@dbid) + ',' +
convert(varchar(max),@fileid) + ',' +
convert(varchar(max),@pageid) + ',' +
convert(varchar(max),@offset) + ',' +
convert(varchar(max),@bytes) + ',' +
'0x' +  convert(varchar(max),@data) + ',' +
convert(varchar(max),@directIO) + ')'  command
drop table #dbcc
END

 

El código del procedimiento es bastente sencillo de seguiry podemos ver que básicamente nos encargamos de manipular el formato de salida del comando DBCC PAGE para poder generar el comando DBCC WRITEPAGE en base a los parámetros suministrados. Se podría haber habilitado la ejecución directa del comando DBCC WRITEPAGE mediante un EXEC dinámico pero, debido al alto riesgo de causar daños en el sistema, hemos preferido que únicamente se genere el comando en modo texto.

Con el uso de este procedimiento es sencillo generar copias de páginas de sistema, críticas para el funcionamiento de nuestra base de datos. Por ejemplo podemos utilizar las páginas 4 y 5 de un fichero de datos, que no tienen uso, para almacenar una copia de otras páginas:

-- Para la base de datos con id 15 y para el fichero 1

-- Copiar página 0 (File header) a la página 4 (sin uso).

exec generate_dbcc_writepage 15,1,0,15,1,4

-- Copiar página 1 (PFS) a la página 5 (sin uso).

exec generate_dbcc_writepage 15,1,1,15,1,5

 

 

Tras ejecutar dichos comandos (en modo single_user) podemos comprobar que si volvemos a extraemos la información de las páginas 4 y 5 coincide con la que originalmente tenían la 0 y 1:

-- Página 4 (sin uso habitualmente).

exec generate_dbcc_writepage 15,1,4

-- Página 5 (sin uso habitualmente).

exec generate_dbcc_writepage 15,1,5

 

image_thumb_1_1AB3929D

Curiosamente estas páginas no son comprobadas en un CHECKDB por lo que, al contrario de lo previsible, no se genere error alguno por haberlas utilizado:

DBCC CHECKDB (15)

CHECKDB found 0 allocation errors and 0 consistency errors in database 'tttt'. 
 DBCC execution completed. If DBCC printed error messages, contact your system administrator.

 

Otro posible uso que podríamos darle a este procedimiento sería para generar un backup a texto de las 10 primeras páginas de cada fichero de filas para cada base de datos de nuestro sistema. Este backup periódico nos podría permitir recuperarnos, mediante un proceso manual, de casos de corrupción que CHECKDB por si solo no sería capaz de solucionar.

Para generar este backup podemos usar generar el conjunto de comandos DBCC WRITEPAGE con el siguiente script:

-- Generar script DBCC WRITEPAGE con las 10 primeras páginas de cada una de las bases de datos de una instancia (excepto tempdb y resourcedb)

DECLARE cur CURSOR READ_ONLY

FOR

select database_id,file_id,pageid

from sys.master_files m,

(select top 10 ROW_NUMBER() OVER (order by object_id)-1 pageid from sys.objects) s

where database_id not in (2,32767) and type_Desc='ROWS'

order by m.database_id,m.file_id,s.pageid

DECLARE @dbid int, @fileid int, @pageid int

OPEN cur

FETCH NEXT FROM cur INTO @dbid,@fileid,@pageid

WHILE (@@fetch_status <> -1)

BEGIN

       IF (@@fetch_status <> -2)

       BEGIN

             EXEC generate_dbcc_writepage @dbid,@fileid,@pageid

       END

       FETCH NEXT FROM cur INTO @dbid,@fileid,@pageid

END

CLOSE cur

DEALLOCATE cur

 

 

También podremos utilizar este procedimiento para copiar solo partes de páginas utilizando el parámetro @offset y @bytes. Esto nos podría servir para reparar por ejemplo un registro corrupto si disponemos de dicho registro en un backup anterior ahorrándonos reparar la base de datos (y perderlo) o restaurar la base de datos completa.

En conclusión, aunque el comando DBCC WRITEPAGE puede ser muy peligroso, también puede sernos muy útil en escenarios especiales como es el tener que recurrir a resolver un problema de corrupción de forma manual. Es muy importante que siempre que vayamos a realizar cualquier tipo de actuación con este tipo de comandos dispongamos de un backup de los ficheros que van a ser modificados para poder realizar una vuelta atrás en el caso que el daño que produzcamos sea aún mayor.

 

Rubén Garrigós