¿Se necesitan transacciones explícitas en este ciclo while?

11

SQL Server 2014:

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.

Si dejamos que lo siguiente se ejecute por un momento y luego cancelemos / finalicemos la consulta, ¿se confirmará el trabajo realizado hasta el momento, o necesitamos agregar declaraciones explícitas de COMENZAR TRANSACCIÓN / FINALIZAR TRANSACCIÓN para que podamos cancelar en cualquier momento?

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:

13

Las declaraciones individuales (DML, DDL, etc.) son transacciones en sí mismas. Entonces sí, después de cada iteración del ciclo (técnicamente: después de cada declaración), lo que sea que UPDATEhaya cambiado esa declaración se ha confirmado automáticamente.

Por supuesto, siempre hay una excepción, ¿verdad? Es posible habilitar transacciones implícitas a través SET IMPLICIT_TRANSACTIONS , en cuyo caso la primera UPDATEdeclaración desencadenara una transacción que usted tendría que COMMITo ROLLBACKal final. Esta es una configuración de nivel de sesión que está DESACTIVADA de forma predeterminada en la mayoría de los casos.

¿necesitamos agregar declaraciones explícitas de BEGIN TRANSACTION / END TRANSACTION para que podamos cancelar en cualquier momento?

No. Y de hecho, dado que desea poder detener el proceso y reiniciarlo, agregar una transacción explícita (o habilitar Transacciones implícitas) sería una mala idea, ya que detener el proceso podría atraparlo antes de que lo haga COMMIT. En ese caso, necesitaría emitir manualmente COMMIT(si está en SSMS), o si está ejecutando esto desde un trabajo del Agente SQL, entonces no tiene esa oportunidad y podría terminar con una transacción huérfana.


Además, es posible que desee establecer @CHUNK_SIZEun número menor. La escalada de bloqueo generalmente ocurre en 5000 bloqueos adquiridos en un solo objeto. Dependiendo del tamaño de las filas y si está haciendo bloqueos de fila frente a bloqueos de página, es posible que esté superando ese límite. Si el tamaño de una fila es tal que solo caben 1 o 2 filas por cada página, entonces es posible que siempre esté presionando esto incluso si está bloqueando la página.

Si la tabla está particionada, tiene la opción de configurar la LOCK_ESCALATIONopción (introducida en SQL Server 2008) para que la tabla AUTObloquee solo la partición y no toda la tabla al escalar. O, para cualquier tabla, puede establecer esa misma opción DISABLE, aunque tendría que tener mucho cuidado con eso. Ver ALTER TABLE para más detalles.

Aquí hay alguna documentación que habla sobre Lock Escalation y los umbrales: Lock Escalation (dice que se aplica a "SQL Server 2008 R2 y versiones superiores"). Y aquí hay una publicación de blog que trata de detectar y corregir la escalada de bloqueo: Bloqueo en Microsoft SQL Server (Parte 12 - Escalada de bloqueo) .


Sin relación con la pregunta exacta, pero relacionada con la consulta en la pregunta, hay algunas mejoras que podrían hacerse aquí (o al menos así parece al mirarla):

  1. Para su ciclo, hacerlo WHILE (@@ROWCOUNT = @CHUNK_SIZE)es un poco mejor ya que si el número de filas actualizadas en la última iteración es menor que la cantidad solicitada para ACTUALIZAR, entonces no queda trabajo por hacer.

  2. Si el deletedcampo es un BITtipo de datos, entonces no es ese valor depende de si o no deletedDatees 2000-01-01? ¿Por qué necesitas ambos?

  3. Si estos dos campos son nuevos y los agregó, por NULLlo que podría ser una operación en línea / sin bloqueo y ahora desea actualizarlos a su valor "predeterminado", entonces eso no fue necesario. A partir de SQL Server 2012 (solo Enterprise Edition), agregar NOT NULLcolumnas que tengan una restricción DEFAULT son operaciones sin bloqueo siempre que el valor de DEFAULT sea una constante. Entonces, si aún no está utilizando los campos, simplemente suelte y vuelva a agregar como NOT NULLy con una restricción DEFAULT.

  4. Si ningún otro proceso está actualizando estos campos mientras está haciendo esta ACTUALIZACIÓN, entonces sería más rápido si pusiera en cola los registros que deseaba actualizar y luego simplemente saliera de esa cola. Hay un impacto en el rendimiento en el método actual, ya que debe volver a consultar la tabla cada vez para obtener el conjunto que debe cambiarse. En su lugar, puede hacer lo siguiente, que solo escanea la tabla una vez en esos dos campos y luego emite solo declaraciones de ACTUALIZACIÓN muy específicas. Tampoco hay penalización por detener el proceso en cualquier momento e iniciarlo más tarde, ya que la población inicial de la cola simplemente encontrará los registros que quedan para actualizar.

    1. Cree una tabla temporal (#FullSet) que solo contenga los campos clave del índice agrupado.
    2. Cree una segunda tabla temporal (#CurrentSet) de esa misma estructura.
    3. insertar en #FullSet a través de SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;

      El TOP(n)está allí debido al tamaño de la mesa. Con 100 millones de filas en la tabla, realmente no necesita llenar la tabla de la cola con todo ese conjunto de claves, especialmente si planea detener el proceso de vez en cuando y reiniciarlo más tarde. Por lo tanto, puede establecer nen 1 millón y dejar que se ejecute hasta su finalización. Siempre puede programar esto en un trabajo del Agente SQL que ejecuta el conjunto de 1 millón (o tal vez incluso menos) y luego espera la próxima hora programada para retomar el trabajo. Luego, puede programar que se ejecute cada 20 minutos para que haya un respiro forzado entre series de n, pero aún así terminará todo el proceso sin supervisión. Luego, haga que el trabajo se elimine solo cuando no haya nada más que hacer :-).

    4. en un bucle, hacer:
      1. Rellene el lote actual a través de algo como DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. Haga la ACTUALIZACIÓN usando algo como: UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;
      4. Borrar el conjunto actual: TRUNCATE TABLE #CurrentSet;
  5. 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).

ACTUALIZACIÓN: consulte mi respuesta a una pregunta relacionada con esta pregunta para la implementación completa de lo que se sugiere anteriormente, incluido un mecanismo para rastrear el estado y cancelar de forma limpia: servidor SQL: actualización de campos en una tabla enorme en pequeños fragmentos: cómo obtener ¿status del progreso?

Solomon Rutzky
fuente
Sus sugerencias en el n. ° 4 pueden ser más rápidas en algunos casos, pero parece una complejidad de código significativa para agregar. Estoy a favor de comenzar de manera simple, y luego, si eso no satisface sus necesidades, considere alternativas.
Bacon Bits
@BaconBits De acuerdo en comenzar simple. Para ser justos, estas sugerencias no estaban destinadas a aplicarse a todos los escenarios. La pregunta es sobre tratar con una tabla muy grande (más de 100 millones de filas).
Solomon Rutzky