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).