servidor sql: actualización de campos en una tabla enorme en pequeños fragmentos: ¿cómo obtener progreso / estado?

10

Tenemos una tabla muy grande (100 millones de filas) y necesitamos actualizar un par de campos en ella.

Para el envío de registros, etc., también, obviamente, queremos mantenerlo en transacciones de tamaño reducido.

  • ¿Lo siguiente hará el truco?
  • ¿Y cómo podemos hacer que imprima algún resultado, para que podamos ver el progreso? (Intentamos agregar una instrucción PRINT allí, pero no se emitió nada durante el ciclo while)

El codigo es:

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END
Jonesome restablecer monica
fuente

Respuestas:

12

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 -3en 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 ;-).

  1. 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.
  2. Agregue una CLAVE PRIMARIA a la #CurrentSettabla 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 #FullSettabla temporal, ya que es solo una simple tabla de cola donde el orden es irrelevante.

  3. En algunos casos, ayuda agregar un índice filtrado para ayudar a SELECTque se alimente a la #FullSettabla temporal. Aquí hay algunas consideraciones relacionadas con la adición de dicho índice:
    1. 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
    2. 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.
    3. 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
    4. Asegúrese de tener en cuenta que el índice, mientras ayuda SELECT, dañará el UPDATEya 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).
Solomon Rutzky
fuente
1
Esto es excelente. Lo estoy ejecutando ahora, y fuma que podemos ejecutarlo en línea, durante el día. ¡Gracias!
Jonesome Reinstate Monica
@samsmith Consulte la sección ACTUALIZACIÓN que acabo de agregar, ya que hay algunas ideas para hacer que el proceso funcione aún más rápido.
Solomon Rutzky
Sin las mejoras de ACTUALIZACIÓN, estamos obteniendo aproximadamente 8 millones de actualizaciones / hora ... con las @BatchRows configuradas en 10000000 (diez millones)
Jonesome Reinstate Monica
@samsmith Eso es genial :) ¿verdad? Tenga en cuenta dos cosas: 1) El proceso se ralentizará ya que cada vez hay menos filas que coinciden con la cláusula WHERE, por lo tanto, sería un buen momento para agregar un índice filtrado, pero ya agregó un índice no filtrado en el empezar así que no estoy seguro de si que le ayudará o daño, pero aún así yo esperaría que el rendimiento disminuya como se pone más cerca de ser hecho, y 2) que podría aumentar el rendimiento al reducir el WAITFOR DELAYa 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.
Solomon Rutzky
Estamos contentos con 8 millones de filas / hora. Sí, podemos ver que se ralentiza. Dudamos de crear más índices (porque la tabla está bloqueada para toda la compilación). Lo que hemos hecho un par de veces es reorganizar el índice existente (porque está en línea).
Jonesome Reinstate Monica
4

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 PRINTdeclaración se muestra en SSMS solo después de que finaliza todo el procedimiento.

Entonces, estoy usando RAISERRORcon baja gravedad:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

Estoy usando SQL Server 2008 Standard y SSMS 2012 (11.0.3128.0). Aquí hay un ejemplo de trabajo completo para ejecutar en SSMS:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

Cuando comento RAISERRORy dejo solo PRINTlos 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 PRINTy uso RAISERRORlos mensajes en la pestaña Mensajes en SSMS aparecen sin esperar 6 segundos, pero a medida que avanza el ciclo.

Curiosamente, cuando uso ambos RAISERRORy PRINTveo ambos mensajes. Primero viene el mensaje del primero RAISERROR, luego se demora 2 segundos, luego el primero PRINTy el segundo RAISERROR, y así sucesivamente.


En otros casos, uso una logtabla 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 SELECTdesde la logtabla 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.

Vladimir Baranov
fuente
En SQL 2008/2014, no podemos ver los resultados de raiseerror ... ¿qué nos estamos perdiendo?
Jonesome Reinstate Monica
@samsmith, agregué un ejemplo completo. Intentalo. ¿Qué comportamiento obtienes en este simple ejemplo?
Vladimir Baranov
2

Puede monitorearlo desde otra conexión con algo como:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

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.

David Spillett
fuente