No estaba al tanto de esta pregunta cuando respondí la pregunta relacionada ( ¿Se necesitan transacciones explícitas en este ciclo while? ), Pero en aras de la exhaustividad, abordaré este problema aquí, ya que no era parte de mi sugerencia en esa respuesta vinculada .
Dado que estoy sugiriendo programar esto a través de un trabajo del Agente SQL (después de todo, son 100 millones de filas), no creo que ninguna forma de enviar mensajes de estado al cliente (es decir, SSMS) sea ideal (aunque si eso es así siempre es necesario para otros proyectos, entonces estoy de acuerdo con Vladimir en que usar RAISERROR('', 10, 1) WITH NOWAIT;
es el camino a seguir).
En este caso particular, crearía una tabla de estado que se puede actualizar por cada ciclo con el número de filas actualizadas hasta el momento. Y no está de más aprovechar el momento actual para tener un latido del corazón en el proceso.
Dado que desea poder cancelar y reiniciar el proceso, Estoy cansado de envolver la ACTUALIZACIÓN de la tabla principal con la ACTUALIZACIÓN de la tabla de estado en una transacción explícita. Sin embargo, si cree que la tabla de estado no está sincronizada debido a la cancelación, es fácil actualizar con el valor actual simplemente actualizándolo manualmente con el COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL
.y hay dos mesas para actualizar (es decir, la mesa principal y la tabla de estados), que deberíamos usar una transacción explícita para mantener esas dos tablas en sincronía, sin embargo, no quieren arriesgarse a que una transacción huérfano si cancela el proceso a una punto después de que haya comenzado la transacción pero no la haya confirmado. Esto debería ser seguro siempre que no detenga el trabajo del Agente SQL.
¿Cómo puedes detener el proceso sin, um, bueno, detenerlo? Al pedirle que pare :-). Sí. Al enviar al proceso una "señal" (similar a kill -3
en Unix), puede solicitar que se detenga en el próximo momento conveniente (es decir, cuando no hay una transacción activa) y que se limpie todo de forma ordenada.
¿Cómo puede comunicarse con el proceso en ejecución en otra sesión? Al utilizar el mismo mecanismo que creamos para comunicarle su estado actual: la tabla de estado. Solo necesitamos agregar una columna que el proceso verificará al comienzo de cada ciclo para que sepa si proceder o abortar. Y dado que la intención es programar esto como un trabajo del Agente SQL (se ejecuta cada 10 o 20 minutos), también deberíamos verificarlo desde el principio, ya que no tiene sentido llenar una tabla temporal con 1 millón de filas si el proceso simplemente va para salir un momento después y no usar ninguno de esos datos.
DECLARE @BatchRows INT = 1000000,
@UpdateRows INT = 4995;
IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
CREATE TABLE dbo.HugeTable_TempStatus
(
RowsUpdated INT NOT NULL, -- updated by the process
LastUpdatedOn DATETIME NOT NULL, -- updated by the process
PauseProcess BIT NOT NULL -- read by the process
);
INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
VALUES (0, GETDATE(), 0);
END;
-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
PRINT 'Process is paused. No need to start.';
RETURN;
END;
CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);
INSERT INTO #FullSet (KeyField1, KeyField2)
SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
FROM dbo.HugeTable ht
WHERE ht.deleted IS NULL
OR ht.deletedDate IS NULL
WHILE (1 = 1)
BEGIN
-- Check if process is paused. If yes, just exit cleanly.
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
PRINT 'Process is paused. Exiting.';
BREAK;
END;
-- grab a set of rows to update
DELETE TOP (@UpdateRows)
FROM #FullSet
OUTPUT Deleted.KeyField1, Deleted.KeyField2
INTO #CurrentSet (KeyField1, KeyField2);
IF (@@ROWCOUNT = 0)
BEGIN
RAISERROR(N'All rows have been updated!!', 16, 1);
BREAK;
END;
BEGIN TRY
BEGIN TRAN;
-- do the update of the main table
UPDATE ht
SET ht.deleted = 0,
ht.deletedDate = '2000-01-01'
FROM dbo.HugeTable ht
INNER JOIN #CurrentSet cs
ON cs.KeyField1 = ht.KeyField1
AND cs.KeyField2 = ht.KeyField2;
-- update the current status
UPDATE ts
SET ts.RowsUpdated += @@ROWCOUNT,
ts.LastUpdatedOn = GETDATE()
FROM dbo.HugeTable_TempStatus ts;
COMMIT TRAN;
END TRY
BEGIN CATCH
IF (@@TRANCOUNT > 0)
BEGIN
ROLLBACK TRAN;
END;
THROW; -- raise the error and terminate the process
END CATCH;
-- clear out rows to update for next iteration
TRUNCATE TABLE #CurrentSet;
WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;
-- clean up temp tables when testing
-- DROP TABLE #FullSet;
-- DROP TABLE #CurrentSet;
Luego puede verificar el estado en cualquier momento utilizando la siguiente consulta:
SELECT sp.[rows] AS [TotalRowsInTable],
ts.RowsUpdated,
(sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND sp.[index_id] < 2;
¿Desea pausar el proceso, ya sea que se ejecute en un trabajo del Agente SQL o incluso en SSMS en la computadora de otra persona? Solo corre:
UPDATE ht
SET ht.PauseProcess = 1
FROM dbo.HugeTable_TempStatus ts;
¿Desea que el proceso pueda volver a comenzar de nuevo? Solo corre:
UPDATE ht
SET ht.PauseProcess = 0
FROM dbo.HugeTable_TempStatus ts;
ACTUALIZAR:
Aquí hay algunas cosas adicionales para probar que podrían mejorar el rendimiento de esta operación. Ninguno está garantizado para ayudar, pero probablemente valga la pena probarlo. Y con 100 millones de filas para actualizar, tiene mucho tiempo / oportunidad para probar algunas variaciones ;-).
- Agregue
TOP (@UpdateRows)
a la consulta ACTUALIZACIÓN para que la línea superior se vea así: a
UPDATE TOP (@UpdateRows) ht
veces ayuda al optimizador a saber cuántas filas como máximo se verán afectadas para que no pierda el tiempo buscando más.
Agregue una CLAVE PRIMARIA a la #CurrentSet
tabla temporal. La idea aquí es ayudar al optimizador con JOIN a la tabla de 100 millones de filas.
Y solo para que se indique para que no sea ambiguo, no debería haber ninguna razón para agregar un PK a la #FullSet
tabla temporal, ya que es solo una simple tabla de cola donde el orden es irrelevante.
- En algunos casos, ayuda agregar un índice filtrado para ayudar a
SELECT
que se alimente a la #FullSet
tabla temporal. Aquí hay algunas consideraciones relacionadas con la adición de dicho índice:
- La condición WHERE debe coincidir con la condición WHERE de su consulta, por lo tanto
WHERE deleted is null or deletedDate is null
- Al comienzo del proceso, la mayoría de las filas coincidirán con su condición WHERE, por lo que un índice no es tan útil. Es posible que desee esperar hasta alrededor del 50% antes de agregar esto. Por supuesto, cuánto ayuda y cuándo es mejor agregar el índice varía debido a varios factores, por lo que es un poco de prueba y error.
- Es posible que deba ACTUALIZAR ESTADÍSTICAS manualmente y / o RECONSTRUIR el índice para mantenerlo actualizado ya que los datos base cambian con bastante frecuencia
- Asegúrese de tener en cuenta que el índice, mientras ayuda
SELECT
, dañará el UPDATE
ya que es otro objeto que debe actualizarse durante esa operación, por lo tanto, más E / S. Esto se aplica tanto al uso de un índice filtrado (que se reduce a medida que actualiza las filas, ya que hay menos filas que coinciden con el filtro), y a esperar un poco para agregar el índice (si no va a ser muy útil al principio, entonces no hay razón para incurrir La E / S adicional).
WAITFOR DELAY
a medio segundo o así, pero eso es una compensación con la concurrencia y posiblemente cuánto se envía a través del envío de registros.Respondiendo a la segunda parte: cómo imprimir algunos resultados durante el ciclo.
Tengo algunos procedimientos de mantenimiento de larga duración que el administrador del sistema a veces tiene que ejecutar.
Los ejecuté desde SSMS y también noté que la
PRINT
declaración se muestra en SSMS solo después de que finaliza todo el procedimiento.Entonces, estoy usando
RAISERROR
con baja gravedad:Estoy usando SQL Server 2008 Standard y SSMS 2012 (11.0.3128.0). Aquí hay un ejemplo de trabajo completo para ejecutar en SSMS:
Cuando comento
RAISERROR
y dejo soloPRINT
los mensajes en la pestaña Mensajes en SSMS aparecen solo después de que todo el lote haya terminado, después de 6 segundos.Cuando comento
PRINT
y usoRAISERROR
los mensajes en la pestaña Mensajes en SSMS aparecen sin esperar 6 segundos, pero a medida que avanza el ciclo.Curiosamente, cuando uso ambos
RAISERROR
yPRINT
veo ambos mensajes. Primero viene el mensaje del primeroRAISERROR
, luego se demora 2 segundos, luego el primeroPRINT
y el segundoRAISERROR
, y así sucesivamente.En otros casos, uso una
log
tabla dedicada separada y simplemente inserto una fila en la tabla con información que describe el estado actual y la marca de tiempo del proceso de larga ejecución.Mientras el largo proceso se ejecuta periódicamente
SELECT
desde lalog
tabla para ver qué está pasando.Obviamente, esto tiene cierta sobrecarga, pero deja un registro (o historial de registros) que puedo examinar a mi propio ritmo más adelante.
fuente
Puede monitorearlo desde otra conexión con algo como:
para ver cuánto queda por hacer. Esto podría ser útil si una aplicación está llamando al proceso, en lugar de ejecutarlo manualmente en SSMS o similar, y necesita mostrar progreso: ejecute el proceso principal de forma asincrónica (o en otro hilo) y luego haga un bucle llamando al "cuánto queda "verifique cada tanto hasta que se complete la llamada asincrónica (o subproceso).
Establecer el nivel de aislamiento lo más laxo posible significa que esto debería regresar en un tiempo razonable sin quedar atrapado detrás de la transacción principal debido a problemas de bloqueo. Podría significar que el valor devuelto es un poco impreciso, por supuesto, pero como un simple medidor de progreso esto no debería importar en absoluto.
fuente