ACTUALIZAR el rendimiento donde no hay cambios de datos


31

Si tengo una UPDATEdeclaración que en realidad no cambia ningún dato (porque los datos ya están en el estado actualizado). ¿Hay algún beneficio de rendimiento al marcar la WHEREcláusula para evitar la actualización?

Por ejemplo, habría alguna diferencia en la velocidad de ejecución entre ACTUALIZACIÓN 1 y ACTUALIZACIÓN 2 en lo siguiente:

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;

La razón por la que pregunto es que necesito el recuento de filas para incluir la fila sin cambios, así sé si hacer una inserción si la ID no existe. Como tal, utilicé el formulario ACTUALIZAR 2. Si hay un beneficio de rendimiento al usar el formulario ACTUALIZAR 1, ¿es posible obtener el recuento de filas que necesito de alguna manera?


Ver sqlperformance.com/2012/10/t-sql-queries/conditional-updates (aunque no hice un perfil del caso donde no cambian los valores).
Aaron Bertrand

Respuestas:


24

Si tengo una declaración de ACTUALIZACIÓN que en realidad no cambia ningún dato (porque los datos ya están en el estado actualizado), ¿hay algún beneficio de rendimiento al marcar la cláusula where para evitar la actualización?

Ciertamente, podría haber una ligera diferencia de rendimiento debido a la ACTUALIZACIÓN 1 :

  • en realidad no actualiza ninguna fila (por lo tanto, no hay nada que escribir en el disco, ni siquiera una mínima actividad de registro), y
  • sacar bloqueos menos restrictivos que los necesarios para realizar la actualización real (por lo tanto, mejor para la concurrencia) ( consulte la sección Actualización hacia el final )

Sin embargo, la diferencia que hay que medir en su sistema con su esquema, datos y carga del sistema. Hay varios factores que influyen en el impacto que tiene una ACTUALIZACIÓN que no se actualiza:

  • la cantidad de contención sobre la tabla que se actualiza
  • el número de filas que se actualizan
  • si hay ACTUALIZADOR Disparadores en la tabla que se está actualizando (como señaló Mark en un comentario sobre la Pregunta). Si ejecuta UPDATE TableName SET Field1 = Field1, se activará un desencadenador de actualización e indicará que el campo se actualizó (si marca usando las funciones UPDATE () o COLUMNS_UPDATED ), y que el campo en ambas tablas INSERTEDy en las DELETEDtablas tiene el mismo valor.

Además, la siguiente sección de resumen se encuentra en el artículo de Paul White, El impacto de las actualizaciones no actualizadas (como lo señaló @spaghettidba en un comentario sobre su respuesta):

SQL Server contiene una serie de optimizaciones para evitar el registro innecesario o el vaciado de páginas al procesar una operación de ACTUALIZACIÓN que no dará como resultado ningún cambio en la base de datos persistente.

  • Las actualizaciones que no se actualizan en una tabla en clúster generalmente evitan el registro adicional y el vaciado de páginas, a menos que la operación de actualización afecte a una columna que forma (parte de) la clave del clúster.
  • Si alguna parte de la clave del clúster se 'actualiza' al mismo valor, la operación se registra como si los datos hubieran cambiado y las páginas afectadas se marcan como sucias en el grupo de búferes. Esto es una consecuencia de la conversión de la ACTUALIZACIÓN a una operación de eliminar y luego insertar.
  • Las tablas de montón se comportan de la misma manera que las tablas en clúster, excepto que no tienen una clave de clúster que provoque un registro adicional o un vaciado de página. Este sigue siendo el caso incluso cuando existe una clave primaria no agrupada en el montón. Por lo tanto, las actualizaciones que no se actualizan en un montón generalmente evitan el registro y el vaciado adicionales (pero vea a continuación).
  • Tanto los montones como las tablas en clúster sufrirán el registro y el vaciado adicionales para cualquier fila donde una columna LOB que contiene más de 8000 bytes de datos se actualice al mismo valor usando cualquier sintaxis que no sea 'SET column_name = column_name'.
  • Simplemente habilitar cualquier tipo de nivel de aislamiento de versiones de fila en una base de datos siempre provoca el registro y el vaciado adicionales. Esto ocurre independientemente del nivel de aislamiento vigente para la transacción de actualización.

Tenga en cuenta (especialmente si no sigue el enlace para ver el artículo completo de Paul), los siguientes dos elementos:

  1. Las actualizaciones que no se actualizan todavía tienen alguna actividad de registro, lo que muestra que una transacción está comenzando y terminando. Es solo que no ocurre ninguna modificación de datos (lo que sigue siendo un buen ahorro).

  2. Como dije anteriormente, debe probar en su sistema. Use las mismas consultas de investigación que Paul está usando y vea si obtiene los mismos resultados. Estoy viendo resultados ligeramente diferentes en mi sistema de lo que se muestra en el artículo. Todavía no hay páginas sucias para escribir, pero un poco más de actividad de registro.


... Necesito que el recuento de filas incluya la fila sin cambios, así que sé si hacer una inserción si la ID no existe. ... ¿es posible obtener el recuento de filas que necesito de alguna manera?

Simplísticamente, si solo está tratando con una sola fila, puede hacer lo siguiente:

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;

Para varias filas, puede obtener la información necesaria para tomar esa decisión utilizando la OUTPUTcláusula Al capturar exactamente qué filas se actualizaron, puede reducir los elementos para buscar y conocer la diferencia entre no actualizar filas que no existen en lugar de no actualizar filas que existen pero que no necesitan la actualización.

Muestro la implementación básica en la siguiente respuesta:

¿Cómo evitar el uso de la consulta de combinación al insertar múltiples datos usando el parámetro xml?

El método que se muestra en esa respuesta no filtra las filas que existen, pero no es necesario actualizarlas. Esa parte podría agregarse, pero primero debería mostrar exactamente dónde está obteniendo su conjunto de datos en el que se está fusionando MyTable. ¿Vienen de una mesa temporal? ¿Un parámetro con valores de tabla (TVP)?


ACTUALIZACIÓN 1:

Finalmente pude hacer algunas pruebas y esto es lo que encontré con respecto al registro de transacciones y el bloqueo. Primero, el esquema de la tabla:

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);

A continuación, la prueba actualiza el campo al valor que ya tiene:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117

Resultados:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY

Finalmente, la prueba que filtra la actualización debido a que el valor no cambia:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';

Resultados:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY

Como puede ver, no se escribe nada en el Registro de transacciones al filtrar la fila, a diferencia de las dos entradas que marcan el comienzo y el final de la Transacción. Y si bien es cierto que esas dos entradas son casi nada, siguen siendo algo.

Además, el bloqueo de los recursos PAGE y KEY es menos restrictivo cuando se filtran las filas que no han cambiado. Si ningún otro proceso está interactuando con esta tabla, entonces es probable que no sea un problema (pero ¿qué tan probable es eso realmente?). Tenga en cuenta que las pruebas que se muestran en cualquiera de los blogs vinculados (e incluso mis pruebas) suponen implícitamente que no hay contención en la tabla ya que nunca forma parte de las pruebas. Al decir que las actualizaciones que no se actualizan son tan livianas que no vale la pena realizar el filtrado, deben tomarse con un grano de sal ya que las pruebas se han realizado, más o menos, en el vacío. Pero en Producción, esta tabla probablemente no esté aislada. Por supuesto, bien podría ser que el poco registro y los bloqueos más restrictivos no se traduzcan en menos eficiencia. Entonces, ¿la fuente de información más confiable para responder esta pregunta? Servidor SQL. Específicamente:su servidor SQL Le mostrará qué método es mejor para su sistema :-).


ACTUALIZACIÓN 2:

Si las operaciones en las que el nuevo valor es el mismo que el valor actual (es decir, sin actualización), enumere las operaciones en las que el nuevo valor es diferente y la actualización es necesaria, entonces el siguiente patrón podría ser aún mejor, especialmente si Hay mucha discusión sobre la mesa. La idea es hacer un simple SELECTprimero para obtener el valor actual. Si no obtiene un valor, entonces tiene su respuesta con respecto al INSERT. Si tiene un valor, puede hacer un simple IFy emitir el UPDATE único si es necesario.

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;

Resultados:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE

Por lo tanto, solo hay 2 bloqueos adquiridos en lugar de 3, y ambos bloqueos son Intent Shared, no Intent eXclusive o Intent Update ( Compatibilidad de bloqueo ). Teniendo en cuenta que cada bloqueo adquirido también se liberará, cada bloqueo es realmente 2 operaciones, por lo que este nuevo método es un total de 4 operaciones en lugar de las 6 operaciones en el método propuesto originalmente. Teniendo en cuenta que esta operación se ejecuta una vez cada 15 ms (aproximadamente, como lo indica el OP), eso es aproximadamente 66 veces por segundo. Entonces, la propuesta original asciende a 396 operaciones de bloqueo / desbloqueo por segundo, mientras que este nuevo método equivale a solo 264 operaciones de bloqueo / desbloqueo por segundo de bloqueos aún más livianos. Esto no es garantía de un rendimiento increíble, pero ciertamente vale la pena probarlo :-).


14

Aleja un poco y piensa en la imagen más grande. En el mundo real, su declaración de actualización realmente se verá así:

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;

¿O se verá más así?

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))

Porque en el mundo real, las tablas tienen muchas columnas. Eso significa que tendrá que generar una gran cantidad de lógica de aplicación dinámica compleja para crear cadenas dinámicas, O tendrá que especificar cada contenido de campo antes y después, cada vez.

Si construye estas declaraciones de actualización dinámicamente para cada tabla, solo pasando los campos que se están actualizando, puede encontrarse rápidamente con un problema de contaminación de caché del plan similar al problema de tamaños de parámetros de NHibernate desde hace unos años. Peor aún, si construye las declaraciones de actualización en SQL Server (como en los procedimientos almacenados), entonces quemará preciosos ciclos de CPU porque SQL Server no es terriblemente eficiente para concatenar cadenas juntas a escala.

Debido a esas complejidades, generalmente no tiene sentido hacer este tipo de comparación fila por fila, campo por campo, mientras realiza las actualizaciones. Piense en operaciones basadas en conjuntos en su lugar.


1
Mi ejemplo del mundo real es tan simple como eso, pero se llama mucho. Mi estimación es una vez cada 15 ms en las horas punta. Me preguntaba si SQL Server es lo suficientemente inteligente como para no escribir en el disco cuando no es necesario.
Martin Brown

3

Podría ver una ganancia de rendimiento al omitir filas que no necesitan actualizarse solo cuando el número de filas es grande (menos registro, menos páginas sucias para escribir en el disco).

Cuando se trata de actualizaciones de una sola fila como en su caso, la diferencia de rendimiento es completamente insignificante. Si actualizar las filas en todos los casos lo hace más fácil para usted, hágalo.

Para obtener más información sobre el tema, consulte Actualizaciones sin actualización de Paul White



1

En lugar de verificar los valores de todos los campos, ¿no puede obtener un valor hash utilizando las columnas que le interesan y luego compararlo con el hash almacenado en la fila de la tabla?

IF EXISTS (Select 1 from Table where ID =@ID AND HashValue=Sha256(column1+column2))
GOTO EXIT
ELSE
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.