Por todos es bien conocida la base de datos tempdb así como su criticidad de cara al correcto funcionamiento de SQL Server. Con la llegada de las versiones SQL Server 2005 y 2008 su uso se ha incrementado considerablemente debido a múltiples funcionalidades internas que se apoyan sobre ésta (versionado de filas, reconstrucción de índices online, etc.) En este post vamos a analizar el comportamiento de algunos elementos que residen en tempdb así como una propiedad característica de la base de datos tempdb: escrituras asíncronas en el log de transacciones.
Vamos a comenzar creando algunos de los elementos típicos de tempdb (tabla temporal, variable de tipo tabla y variable de tipo tabla definido por usuario) para analizar cómo se comportan ante pequeñas inserciones. Este es el escenario típico que podríamos tener cuando tratamos de utilizar estos elementos como interfaz entre procedimientos almacenados. Esta es una nueva funcionalidad de SQL Server 2008 (TVP) que nos permite pasar como parámetros tablas directamente, evitando así tener que convertir tablas en XML, CSV, etc. El siguiente script crea estos elementos y realiza una inserción de 5 registros en cada uno de ellos:
USE tempdb
— Creamos una tabla temporal, una variable de tipo tabla y un tipo tabla
CREATE TYPE TipoTabla AS TABLE (id int, data char(3000))
CREATE TABLE #TEMP (id int, data char(3000))
DECLARE @tabla AS TipoTabla
DECLARE @tabla2 AS TABLE (id int, data char(3000))
— Obtenemos el último LSN actual
DECLARE @lastTempdbLSN VARCHAR(MAX) =
(SELECT MAX([CURRENT LSN])
FROM fn_dblog(NULL, NULL))
— Realizamos inserciones en cada elemento
INSERT INTO #TEMP
VALUES (1,‘data1’),(2,‘data2’),(3,‘data3’),(4,‘data4’),(5,‘data5’)
INSERT INTO @tabla
VALUES (1,‘data1’),(2,‘data2’),(3,‘data3’),(4,‘data4’),(5,‘data5’)
INSERT INTO @tabla2
VALUES (1,‘data1’),(2,‘data2’),(3,‘data3’),(4,‘data4’),(5,‘data5’)
— Mostramos los registros desde el anterior LSN
SELECT AllocUnitName, *
FROM fn_dblog(NULL, NULL)
WHERE [CURRENT LSN] > @lastTempdbLSN
ORDER BY [CURRENT LSN] ASC
SELECT allocation_unit_id, type_desc, total_pages, used_pages, data_pages, container_id, hobt_id,rows,data_compression FROM sys.allocation_units
JOIN sys.partitions ON container_id = sys.partitions.hobt_id
DROP TABLE #TEMP
Antes de ejecutar el script, vamos a analizar el plan de ejecución previsto para las inserciones:
Como podemos ver el plan de ejecución para todas las operaciones es idéntico. Si nos fijamos a más bajo nivel vemos que el coste estimado es el mismo para todos los casos:
Estimated CPU Cost | Estimated IO Cost | |
Tabla temporal | 0,000005 | 0,0156606 |
Variable de tipo TipoTabla | 0,000005 | 0,0156606 |
Variable de tipo tabla | 0,000005 | 0,0156606 |
A continuación vamos a ejecutar el script y analizaremos las entradas en el log de transacciones. Tras ejecutarlo, vemos que las tres operaciones generan 92 entradas en el log de transacciones. La primera de ellas (sobre la tabla temporal) genera 32 entradas, la segunda (sobre la variable de TipoTabla) 28 y la tercera (sobre la variable de tipo tabla) genera 32 entradas.
Si comparamos las 32 operaciones de la tabla temporal y de la variable de tipo tabla vemos que las diferencias son mínimas pues el tipo de las operaciones realizadas coincide en orden y número. Los cambios se centran en los nombres de los objetos, pequeños cambios en la longitud de los registros, los LSN (obviamente) y en la descripción de la operación. La descripción de la operación en el log es importante pues nos indica que en el caso de la tabla temporal está considerada una operación INSERT real mientras que en las variables son TVQUERY. Las TVQUERY no participan en la transacción por lo que no se verían afectadas por un rollback de ésta. Las operaciones realizadas, en pseudocódigo, quedarían así:
Operación | Objeto | Descripción |
LOP_MODIFY_ROW | LCX_PFS | Actualizamos la PFS |
LOP_FORMAT_PAGE | LCX_IAM | Formateamos una página en la IAM |
LOP_MODIFY_ROW | LCX_IAM | Actualizamos la página IAM correspondiente |
LOP_FORMAT_PAGE | LCX_HEAP | Formateamos una página de datos |
LOP_INSERT_ROWS | LCX_HEAP | Insertamos el primer registro |
LOP_INSERT_ROWS | LCX_HEAP | Insertamos el segundo registro |
LOP_MODIFY_ROW | LCX_PFS | Actualizamos el espacio libre de la página (ya no caben más) |
LOP_MODIFY_ROW | LCX_IAM | Modificamos la IAM |
LOP_FORMAT_PAGE | LCX_HEAP | Formateamos una nueva página de datos |
LOP_INSERT_ROWS | LCX_HEAP | Insertarmos el tercer registro |
LOP_INSERT_ROWS | LCX_HEAP | Insertamos el cuarto registro |
LOP_MODIFY_ROW | LCX_PFS | Actualizamos el espacio libre de la página (ya no caben más) |
LOP_MODIFY_ROW | LCX_IAM | Modificamos la IAM |
LOP_FORMAT_PAGE | LCX_HEAP | Formateamos una nueva página de datos |
LOP_INSERT_ROWS | LCX_HEAP | Insertarmos el último registro |
Por simplicidad hemos obviado algunas entradas referentes a los cambios en sys.sysallocunits que podréis ver si lanzáis el script en vuestra máquina. Si comparamos estas inserciones con la inserción en la variable de tipo TipoTabla podemos encontrar que una de las pocas diferencias es que en este caso NO existen cuatro entradas que sí aparecen tanto en el caso de tabla temporal como en el caso de variable de tipo tabla. De nuevo en pseudocódigo:
Operación | Objeto | Descripción |
LOP_BEGIN_XACT | LCX_NULL | Inicio de transacción interna |
LOP_SET_BITS | LCX_GAM | Modificamos la GAM |
LOP_SET_BITS | LCX_SGAM | Modificamos la SGAM |
LOP_COMMIT_XACT | LCX_NULL | Fin de transacción interna |
Por tanto, la inserción en una variable de un tipo de tabla aunque genera asignaciones de espacio, no modifica la SGAM ni la GAM. Podemos ver cómo se asigna el espacio obteniendo los allocation_unit_id del log de transacciones consultando la tabla sys.allocation_units:
select allocation_unit_id, type_desc, total_pages, used_pages, data_pages, container_id, hobt_id,rows,data_compression from sys.allocation_units
join sys.partitions on container_id = sys.partitions.hobt_id
Los allocation unit mostrados corresponden con la variable de tipo TipoTabla, con la variable de tipo tabla y con la tabla temporal respectivamente. En general podemos esperar un rendimiento muy similar para pequeñas tablas utilicemos la alternativa que utilicemos pues vemos que la actividad real en el log de transacciones es prácticamente idéntica. Desde el punto de vista del rendimiento la actividad del log de transacciones es vital. Por tanto consideramos importante destacar que la escritura en el log de transacciones ocurre de forma asíncrona en tempdb. Esto incrementa notablemente el rendimiento de las operaciones que requieren de uso intensivo del log de transacciones respecto a una base de datos de usuario. Para comprobar este punto vamos a realizar 10000 veces la misma inserción de los 5 registros sobre una tabla temporal y sobre una tabla no temporal para comparar tiempos. Indicar que todos los ficheros de datos y de log residen sobre el mismo disco por lo que no se esperan diferencias debido a dicho motivo.
USE test
CREATE TABLE #TEMP (id int, data char(3000))
CREATE TABLE NOTEMP (id int, data char(3000))
SELECT GETDATE();
DECLARE @i int = 0
WHILE @i<10000
BEGIN
INSERT INTO #TEMP
VALUES (1,‘data1’),(2,‘data2’),(3,‘data3’),(4,‘data4’),(5,‘data5’)
set @i=@i+1
END
SELECT GETDATE();
SET @i=0
WHILE @i<10000
BEGIN
INSERT INTO NOTEMP
VALUES (1,‘data1’),(2,‘data2’),(3,‘data3’),(4,‘data4’),(5,‘data5’)
set @i=@i+1
END
SELECT GETDATE();
Como podemos ver la diferencia de tiempo necesario para realizar las mismas operaciones sobre una tabla temporal o sobre una tabla no temporal es considerable (14 segundos vs 34 segundos). Obviamente estos resultados tan dispares se producen debido a la no existencia de una caché de escritura suficientemente grande en mi máquina. En un sistema de almacenamiento empresarial con varios gigabytes de caché de escritura las diferencias serían mínimas o no existentes. Deberemos en todo caso hacer mediciones en nuestro sistema particular para determinar el impacto real. No olvidemos que tempdb es un recurso compartido por todas las bases de datos. Por tanto, lo que ganamos con la asincronía lo podemos perder debido a congestión en los discos que soporten tempdb.
Si analizamos la actividad generada en el log de tempdb veremos se caracteriza por un tamaño de registros de log reducido pues únicamente tenemos que almacenar los valores “before” y no los valores “after” pues nunca deberemos echar manos de la “recuperabilidad” en un caso de desastre.
USE test
SELECT TOP 10 AllocUnitName, *
FROM fn_dblog(NULL, NULL)
WHERE AllocUnitName=‘dbo.NOTEMP’ and Operation=‘LOP_INSERT_ROWS’
ORDER BY [CURRENT LSN] DESC
USE tempdb
SELECT TOP 10 AllocUnitName, *
FROM fn_dblog(NULL, NULL)
WHERE AllocUnitName like ‘dbo.#temp%’ and Operation=‘LOP_INSERT_ROWS’
ORDER BY [CURRENT LSN] DESC
Pensemos que cada vez que se inicia SQL Server la base de datos tempdb se inicializa vacía por lo cual no tienen sentido los valores “after” al no tener que hacer nunca un REDO de la operación. Si simultáneamente al script lanzamos una herramienta de monitorización como “Process Monitor” de Sysinternals podemos ver como las escrituras sobre el log de tempdb se realizan de forma asíncrona respecto a las escrituras. Esto lo podemos ver cuando durante el script anterior se intercalan operaciones de escritura del log en tempdb con la escritura en el log de la base de datos test:
En conclusión, vemos que tanto el uso de variables de tipo tabla, como variables creadas con un tipo tabla como tablas temporales implican actividad en el log de transacciones y, por tanto, operaciones de entrada/salida a disco. Sin embargo para su uso como estructuras temporales de pequeño tamaño todas son válidas. Por otra parte, tempdb opera como una base de datos temporal, en modo de recuperación simple, con logging reducido y con escritura asíncrona en el log. Por tanto el uso de tempdb puede traernos ventajas de rendimiento pero debemos tener en cuenta que es una base de datos especialmente sensible a congestión por lo que requiere un análisis caso por caso para evitar empeorar el rendimiento global.
5 comments
ligljk
ghjgjgfjgj
dghjdfhdfgj
hfghghdfgh
xcfgjhfgjdj