Sí, parece un problema muy genérico, pero todavía no he podido reducirlo demasiado.
Entonces tengo una instrucción UPDATE en un archivo por lotes sql:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
B tiene 40k registros, A tiene 4M registros y están relacionados de 1 a n a través de A.B_ID, aunque no hay FK entre los dos.
Básicamente, estoy precalculando un campo para fines de minería de datos. Aunque cambié el nombre de las tablas para esta pregunta, no cambié la declaración, es realmente así de simple.
Esto demora horas en ejecutarse, así que decidí cancelar todo. La base de datos se corrompió, así que la eliminé, restauré una copia de seguridad que hice justo antes de ejecutar la declaración y decidí entrar en detalles con un cursor:
DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
RAISERROR(@Msg, 10, 1) WITH NOWAIT
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = @Id
FETCH NEXT FROM CursorB INTO @Id
END
Ahora puedo verlo ejecutándose con un mensaje con la identificación descendente. Lo que sucede es que toma alrededor de 5 minutos ir de id = 40k a id = 13
Y luego, en la identificación 13, por alguna razón, parece colgar. El DB no tiene ninguna conexión además del SSMS, pero en realidad no está colgado:
- el disco duro se ejecuta continuamente, por lo que definitivamente está haciendo algo (verifiqué en Process Explorer que de hecho es el proceso sqlserver.exe que lo usa)
Ejecuté sp_who2, encontré el SPID (70) de la sesión SUSPENDIDA y luego ejecuté el siguiente script:
seleccione * de sys.dm_exec_requests r únase a sys.dm_os_tasks t en r.session_id = t.session_id donde r.session_id = 70
Esto me da el wait_type, que es PAGEIOLATCH_SH la mayor parte del tiempo, pero en realidad cambia a WRITE_COMPLETION a veces, lo que supongo que sucede cuando está vaciando el registro
- el archivo de registro, que era de 1.6 GB cuando restauré la base de datos (y cuando llegó a la identificación 13), ahora es de 3.5 GB
Otra información quizás útil:
- el número de registros en la tabla A para B_ID 13 no es grande (14)
- Mi colega no tiene el mismo problema en su máquina, con una copia de este DB (de hace un par de meses) con la misma estructura.
- la tabla A es, con mucho, la tabla más grande en el DB
- Tiene varios índices y varias vistas indizadas lo usan.
- No hay otro usuario en la base de datos, es local y ninguna aplicación lo está utilizando.
- El archivo LDF no tiene un tamaño limitado.
- El modelo de recuperación es SIMPLE, el nivel de compatibilidad es 100
- Procmon no me da mucha información: sqlserver.exe está leyendo y escribiendo mucho de los archivos MDF y LDF.
Todavía estoy esperando que termine (han pasado 1h30) pero esperaba que tal vez alguien me diera alguna otra acción que pudiera intentar solucionar este problema.
Editado: agregar extracto del registro procmon
15:24:02.0506105 sqlservr.exe 1760 ReadFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal
Al usar DBCC PAGE, parece estar leyendo y escribiendo en campos que se parecen a la tabla A (o uno de sus índices), pero para diferentes B_ID que 13. ¿Reconstruir índices tal vez?
Editado 2: plan de ejecución
Así que cancelé la consulta (en realidad eliminé la base de datos y sus archivos y luego la restauré), y verifiqué el plan de ejecución para:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13
El plan de ejecución (estimado) es el mismo que para cualquier B.ID, y parece bastante sencillo. La cláusula WHERE usa una búsqueda de índice en un índice no agrupado de B, JOIN usa una búsqueda de índice agrupado en ambas PK de las tablas. La búsqueda de índice agrupado en A usa paralelismo (x7) y representa el 90% del tiempo de CPU.
Más importante aún, la ejecución real de la consulta con ID 13 es inmediata.
Editado 3: fragmentación del índice
La estructura de los índices es la siguiente:
B tiene una PK agrupada (no el campo ID) y un índice único no agrupado, cuyo primer campo es B.ID; este segundo índice parece usarse siempre.
A tiene una PK agrupada (campo no relacionado).
También hay 7 vistas en A (todas incluyen el campo AX), cada una con su propia PK agrupada y otro índice que también incluye el campo AX
Las vistas se filtran (con campos que no están en esta ecuación), por lo que dudo que haya alguna forma de que la ACTUALIZACIÓN A use las vistas mismas. Pero tienen un índice que incluye AX, por lo que cambiar AX significa escribir las 7 vistas y los 7 índices que tienen que incluyen el campo.
Aunque se espera que la ACTUALIZACIÓN sea más lenta para esto, no hay ninguna razón por la cual una identificación específica sea mucho más larga que las demás.
Verifiqué la fragmentación de todos los índices, todos estaban en <0.1%, excepto los índices secundarios de las vistas , todos entre 25% y 50%. Los factores de relleno para todos los índices parecen estar bien, entre 90% y 95%.
Reorganicé todos los índices secundarios y volví a ordenar mi script.
Todavía está colgado, pero en un punto diferente:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Mientras que anteriormente, el registro de mensajes se veía así:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Updating A for B_ID=13
Esto es extraño, porque significa que ni siquiera está colgado en el mismo punto del WHILE
bucle. El resto se ve igual: la misma línea de ACTUALIZACIÓN esperando en sp_who2, el mismo tipo de espera PAGEIOLATCH_EX y el mismo uso pesado de HD de sqlserver.exe.
El siguiente paso es eliminar todos los índices y vistas y recrearlos, creo.
Editado 4: eliminación y reconstrucción de índices
Entonces, eliminé todas las vistas indexadas que tenía en la tabla (7 de ellas, 2 índices por vista, incluida la agrupada). Ejecuté el script inicial (sin cursor), y en realidad se ejecutó en 5 minutos.
Entonces mi problema se origina en la existencia de estos índices.
Volví a crear mis índices después de ejecutar la actualización, y me tomó 16 minutos.
Ahora entiendo que los índices tardan en reconstruirse, y estoy bien con la tarea completa que toma 20 minutos.
Lo que todavía no entiendo es por qué cuando ejecuto la actualización sin eliminar los índices primero, lleva varias horas, pero cuando los elimino primero y luego los vuelvo a crear, demoran 20 minutos. ¿No debería tomar el mismo tiempo de cualquier manera?
DBCC PAGE
para ver en qué se está escribiendo.