Esta pregunta está relacionada con este hilo del foro .
Ejecuto SQL Server 2008 Developer Edition en mi estación de trabajo y un clúster de máquina virtual de dos nodos Enterprise Edition donde me refiero a "clúster alfa".
El tiempo que lleva eliminar filas con una columna varbinary (max) está directamente relacionado con la longitud de los datos en esa columna. Eso puede sonar intuitivo al principio, pero después de la investigación, choca con mi comprensión de cómo SQL Server realmente elimina filas en general y trata este tipo de datos.
El problema surge de un problema de tiempo de espera de eliminación (> 30 segundos) que estamos viendo en nuestra aplicación web .NET, pero lo he simplificado en aras de esta discusión.
Cuando se elimina un registro, SQL Server lo marca como un fantasma para ser limpiado por una tarea de limpieza de Ghost en un momento posterior después de que se confirme la transacción (vea el blog de Paul Randal ). En una prueba que elimina tres filas con datos de 16 KB, 4 MB y 50 MB en una columna varbinary (max), respectivamente, veo que esto sucede en la página con la parte de fila de los datos, así como en la transacción Iniciar sesión.
Lo que me parece extraño es que los bloqueos X se colocan en todas las páginas de datos LOB durante la eliminación, y las páginas se desasignan en el PFS. Veo esto en el registro de transacciones, así como con sp_lock
y los resultados del dm_db_index_operational_stats
DMV ( page_lock_count
).
Esto crea un cuello de botella de E / S en mi estación de trabajo y nuestro clúster alfa si esas páginas aún no están en la memoria caché del búfer. De hecho, el page_io_latch_wait_in_ms
del mismo DMV es prácticamente la duración total de la eliminación, y page_io_latch_wait_count
corresponde con el número de páginas bloqueadas. Para el archivo de 50 MB en mi estación de trabajo, esto se traduce en más de 3 segundos al comenzar con un caché de búfer vacío ( checkpoint
/ dbcc dropcleanbuffers
), y no tengo dudas de que sería más largo por una gran fragmentación y bajo carga.
Traté de asegurarme de que no se tratara de asignar espacio en el caché para ocupar ese tiempo. Leí en 2 GB de datos de otras filas antes de ejecutar la eliminación en lugar del checkpoint
método, que es más de lo que se asigna al proceso de SQL Server. No estoy seguro de si esa es una prueba válida o no, ya que no sé cómo SQL Server baraja los datos. Asumí que siempre expulsaría lo viejo en favor de lo nuevo.
Además, ni siquiera modifica las páginas. Esto lo puedo ver con dm_os_buffer_descriptors
. Las páginas están limpias después de la eliminación, mientras que el número de páginas modificadas es inferior a 20 para las tres eliminaciones pequeñas, medianas y grandes. También comparé la salida de DBCC PAGE
una muestra de las páginas buscadas, y no hubo cambios (solo ALLOCATED
se eliminó el bit de PFS). Simplemente los desasigna.
Para probar aún más que las búsquedas de páginas / las ubicaciones de negocios están causando el problema, probé la misma prueba usando una columna de flujo de archivos en lugar de varilla binaria (max). Las eliminaciones fueron de tiempo constante, independientemente del tamaño de LOB.
Entonces, primero mis preguntas académicas:
- ¿Por qué SQL Server necesita buscar todas las páginas de datos LOB para bloquearlas con X? ¿Es eso solo un detalle de cómo se representan los bloqueos en la memoria (almacenados de alguna manera con la página)? Esto hace que el impacto de E / S dependa en gran medida del tamaño de los datos si no se almacena en caché por completo.
- ¿Por qué la X se bloquea en absoluto, solo para desasignarlos? ¿No es suficiente bloquear solo la hoja de índice con la parte de la fila, ya que la desasignación no necesita modificar las páginas en sí? ¿Hay alguna otra forma de obtener los datos LOB contra los que protege el bloqueo?
- ¿Por qué desasignar las páginas por adelantado, dado que ya hay una tarea de fondo dedicada a este tipo de trabajo?
Y quizás más importante, mi pregunta práctica:
- ¿Hay alguna forma de hacer que las eliminaciones funcionen de manera diferente? Mi objetivo es la eliminación constante del tiempo, independientemente del tamaño, similar a la secuencia de archivos, donde cualquier limpieza ocurre en segundo plano después del hecho. ¿Es una cosa de configuración? ¿Estoy almacenando cosas de forma extraña?
Aquí se explica cómo reproducir la prueba descrita (ejecutada a través de la ventana de consulta SSMS):
CREATE TABLE [T] (
[ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Data] [varbinary](max) NULL
)
DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier
SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration
INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))
-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN
-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID
-- Do this after test
ROLLBACK
Estos son algunos resultados del perfil de las eliminaciones en mi estación de trabajo:
El | Tipo de columna | Eliminar tamaño | Duración (ms) | Lee | Escribe | CPU | -------------------------------------------------- ------------------ El | VarBinary | 16 KB | 40 | 13 2 | 0 | El | VarBinary | 4 MB | 952 | 2318 | 2 | 0 | El | VarBinary | 50 MB | 2976 | 28594 | 1 | 62 | -------------------------------------------------- ------------------ El | FileStream | 16 KB | 1 | 12 | 1 | 0 | El | FileStream | 4 MB | 0 | 9 | 0 | 0 | El | FileStream | 50 MB | 1 | 9 | 0 | 0 |
No podemos necesariamente usar filestream en su lugar porque:
- Nuestra distribución del tamaño de los datos no lo garantiza.
- En la práctica, agregamos datos en muchos fragmentos, y filestream no admite actualizaciones parciales. Tendríamos que diseñar alrededor de esto.
Actualización 1
Probé una teoría de que los datos se escriben en el registro de transacciones como parte de la eliminación, y este no parece ser el caso. ¿Estoy probando esto incorrectamente? Vea abajo.
SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001
BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID
SELECT
SUM(
DATALENGTH([RowLog Contents 0]) +
DATALENGTH([RowLog Contents 1]) +
DATALENGTH([RowLog Contents 3]) +
DATALENGTH([RowLog Contents 4])
) [RowLog Contents Total],
SUM(
DATALENGTH([Log Record])
) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'
Para un archivo de más de 5 MB de tamaño, esto regresó 1651 | 171860
.
Además, esperaría que las páginas en sí estuvieran sucias si los datos se escribieran en el registro. Parece que solo se registran las asignaciones de negocios, lo que coincide con lo que está sucio después de la eliminación.
Actualización 2
Recibí una respuesta de Paul Randal. Afirmó el hecho de que tiene que leer todas las páginas para atravesar el árbol y encontrar qué páginas desasignar, y afirmó que no hay otra manera de buscar qué páginas. Esta es una media respuesta a 1 y 2 (aunque no explica la necesidad de bloqueos en los datos fuera de la fila, pero eso es poca cosa).
La pregunta 3 todavía está abierta: ¿por qué desasignar las páginas por adelantado si ya hay una tarea en segundo plano para hacer la limpieza de las eliminaciones?
Y, por supuesto, la pregunta más importante: ¿hay alguna forma de mitigar directamente (es decir, no evitar) este comportamiento de eliminación dependiente del tamaño? Creo que este sería un problema más común, a menos que realmente seamos los únicos que almacenemos y eliminemos filas de 50 MB en SQL Server. ¿Todos los demás trabajan alrededor de esto con algún tipo de trabajo de recolección de basura?