2010-02-16 32 views
33

A menudo se produce una situación en la que necesita ejecutar una instrucción INSERT, UPDATE o DELETE basada en alguna condición. Y mi pregunta es si el efecto en el rendimiento de la consulta agrega IF EXISTS antes del comando.SI EXISTE antes de INSERTAR, ACTUALIZAR, ELIMINAR para la optimización

Ejemplo

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1) 
    UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1 

¿Qué hay de añadir o suprimir?

+0

¿Por qué no mira el plan de ejecución? – RichardOD

Respuesta

65

No estoy completamente seguro, pero me da la impresión de que esta pregunta es realmente acerca de upsert, que es la siguiente operación atómica:

  • Si la fila existe tanto en el origen y el destino, la UPDATE objetivo;
  • Si la fila solo existe en la fuente, INSERT la fila en el destino;
  • (Opcional) Si la fila existe en el destino pero no la fuente, DELETE la fila del destino.

Desarrolladores convertido en administradores de bases menudo ingenuamente escriben fila por fila, de esta manera:

-- For each row in source 
IF EXISTS(<target_expression>) 
    IF @delete_flag = 1 
     DELETE <target_expression> 
    ELSE 
     UPDATE target 
     SET <target_columns> = <source_values> 
     WHERE <target_expression> 
ELSE 
    INSERT target (<target_columns>) 
    VALUES (<source_values>) 

Esto es casi lo peor que se puede hacer, por varias razones:

  • Tiene una condición de carrera. La fila puede desaparecer entre IF EXISTS y DELETE o UPDATE.

  • Es un desperdicio. Para cada transacción tiene una operación adicional que se realiza; tal vez es trivial, pero eso depende completamente de lo bien que hayas indexado.

  • Lo peor de todo, es seguir un modelo iterativo, pensando en estos problemas en el nivel de una sola fila. Esto tendrá el mayor (peor) impacto de todos en el rendimiento general.

Una muy pequeña (y subrayo menor) de optimización es simplemente intentar la UPDATE de todos modos; si la fila no existe, @@ROWCOUNT será 0 y se puede entonces "segura" de inserción:

-- For each row in source 
BEGIN TRAN 

UPDATE target 
SET <target_columns> = <source_values> 
WHERE <target_expression> 

IF (@@ROWCOUNT = 0) 
    INSERT target (<target_columns>) 
    VALUES (<source_values>) 

COMMIT 

peor de los casos, esto va a seguir realizando dos operaciones para cada transacción, pero al menos hay una posibilidad de de solo realizar uno, y también elimina la condición de raza (tipo de).

Pero el problema real es que esto todavía se está haciendo para cada fila en la fuente.

Antes de SQL Server 2008, se tenía que utilizar un modelo 3-etapa difícil hacer frente a este en el nivel establecido (aún mejor que la fila por fila): El rendimiento

BEGIN TRAN 

INSERT target (<target_columns>) 
SELECT <source_columns> FROM source s 
WHERE s.id NOT IN (SELECT id FROM target) 

UPDATE t SET <target_columns> = <source_columns> 
FROM target t 
INNER JOIN source s ON t.d = s.id 

DELETE t 
FROM target t 
WHERE t.id NOT IN (SELECT id FROM source) 

COMMIT 

Como ya he dicho, fue bastante pésimo en esto, pero aún mucho mejor que el enfoque de una fila a la vez.SQL Server 2008, sin embargo, finalmente se introdujo MERGE sintaxis, por lo que ahora todo lo que tiene que hacer es lo siguiente:

MERGE target 
USING source ON target.id = source.id 
WHEN MATCHED THEN UPDATE <target_columns> = <source_columns> 
WHEN NOT MATCHED THEN INSERT (<target_columns>) VALUES (<source_columns>) 
WHEN NOT MATCHED BY SOURCE THEN DELETE; 

eso es todo. Una declaración. Si está utilizando SQL Server 2008 y la necesidad de llevar a cabo cualquier secuencia de INSERT, UPDATE y DELETE dependiendo de si o no la fila ya existe - incluso si es sólo una fila - hay sin excusa para no estar usando MERGE .

Puede incluso OUTPUT las filas afectadas por un MERGE en una variable de tabla si necesita saber más tarde lo que se hizo. Simple, rápido y libre de riesgos. Hazlo.

+2

Gracias por presentarme a MERGE. Es realmente genial. – jwarzech

+7

MERGE es genial, me gustaría señalar que hace poco descubrí esto: MERGE no evita las condiciones de carrera a menos que se agregue una pista adecuada ('WITH (HOLDLOCK)'). Ver [el trabajo de Dan Guzman sobre señalar la Condición de Carrera UPSERT con MERGE] (http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx) –

+1

+ 1 para "¡Desarrolladores convertidos en DBA a menudo lo escriben ingenuamente fila por fila"! –

2

El rendimiento de un comunicado IF EXISTS:

IF EXISTS(SELECT 1 FROM mytable WHERE someColumn = someValue) 

depende de los índices actuales para satisfacer la consulta.

3

No debe hacer esto en la mayoría de los casos. Dependiendo de su nivel de transacción, ha creado una condición de carrera, ahora en su ejemplo aquí no tendría demasiada importancia, pero los datos pueden cambiarse desde la primera selección hasta la actualización. Y todo lo que ha hecho es obligar a SQL a hacer más trabajo

La mejor manera de saberlo con certeza es probar las dos diferencias y ver cuál le ofrece el rendimiento adecuado.

2

Hay un ligero efecto, ya que está haciendo el mismo cheque dos veces, al menos en su ejemplo:

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1) 

tiene que consultar, ver si hay alguno, de ser cierto, entonces:

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1 

Tiene que consultar, ver cuáles ... mismo verificar dos veces sin motivo. Ahora bien, si la condición que estás buscando está indexada, debería ser rápida, pero para las tablas grandes podrías ver un poco de retraso solo porque estés ejecutando la selección.

1

Sí, esto afectará el rendimiento (el grado en que el rendimiento se verá afectado se verá afectado por una serie de factores). De hecho, está haciendo la misma consulta "dos veces" (en su ejemplo). Pregúntese si necesita o no ser tan defensivo en su consulta y en qué situaciones la fila no estaría allí. Además, con una declaración de actualización, las filas afectadas probablemente sean una mejor manera de determinar si algo se ha actualizado.

3

IF EXISTS básicamente hará un SELECCIONAR - el mismo que ACTUALIZARÍA.

Como tal, será rendimiento disminución - si no hay nada para actualizar, lo hizo la misma cantidad de trabajo (ACTUALIZACIÓN habría preguntado misma falta de filas como su selecto) y si hay algo de actualizar, que Juet hizo una selección no necesaria.

+0

Una alternativa es la respuesta de burnall: simplemente haga primero la actualización y luego marque @@ rowcount. Si es cero, entonces no se actualizó nada y usted llama a un inserto. –

+0

'IF EXISTS' no es ** ** un buen diseño para una operación de inserción o actualización. Tiene una condición de carrera, incluso dentro de un bloque 'BEGIN TRAN' /' COMMIT TRAN'. – Aaronaught

+0

@Aaronaught - buen punto. Eliminado esa pieza. – DVK

8

Eso no es útil para una sola actualización/eliminación/inserción.
Posiblemente agrega rendimiento si varios operadores después de la condición if.
En último caso, es mejor escribir

update a set .. where .. 
if @@rowcount > 0 
begin 
    .. 
end 
+0

+1. Debo admitir que en la primera lectura de la pregunta, no era obvio (al menos para mí) que esto era a lo que se refería el OP. Debe ser tarde ... –

+0

Exactamente. Haga lo que * normalmente * será la acción necesaria, verifique los resultados, luego haga la alternativa si es necesario. –

+0

+1 Iba a ir por este camino también, pero no estaba claro por su pregunta ... Buena respuesta – JoshBerke

4

No debe hacerlo por UPDATE y DELETE, como si no hubiera impacto en el rendimiento, es no es un positivo uno.

Para INSERT puede haber situaciones donde su INSERT lanzará una excepción (UNIQUE CONSTRAINT violación, etc.), en cuyo caso es posible que desee evitar que con el IF EXISTS y manejarlo con más gracia.

2

Esto se repite en gran parte el precedente (por tiempo) de cinco (no, seis) (no, siete) respuestas, pero:

Sí, el SI existe una estructura que tiene por lo general se duplicará el trabajo realizado por la base de datos. Si bien IF EXISTS se "detendrá" cuando encuentre la primera fila coincidente (no es necesario que los encuentre a todos), se trata de un esfuerzo adicional y en última instancia inútil: para las actualizaciones y eliminaciones.

  • Si no existe tal fila (s), IF EXISTS hará un escaneo completo (tabla o índice) para determinar esto.
  • Si existe una o más filas, IF EXISTS leerá suficiente de la tabla/índice para encontrar la primera, y luego ACTUALIZAR o ELIMINAR volverá a leer la tabla para encontrarla nuevamente y procesarla - y leerá "el resto de" la tabla para ver si hay algo más que procesar también. (Lo suficientemente rápido si está indexado correctamente, pero aún así.)

De todos modos, terminará leyendo toda la tabla o índice al menos una vez. Pero, ¿para qué preocuparse con IF EXISTS en primer lugar?

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1 

o DELETE parecido funcionará bien si hay alguna fila se encuentran para procesar. Sin filas, tabla escaneada, nada modificado, listo; 1+ filas, tabla escaneada, todo lo que debería ser se modifica, se vuelve a hacer. Un pase, sin problemas, sin complicaciones, sin tener que preocuparse por "la base de datos cambió por otro usuario entre mi primera consulta y mi segunda consulta".

INSERT es una situación en la que podría ser útil: compruebe si la fila está presente antes de agregarla, para evitar infracciones de clave primaria o única. Por supuesto, debe preocuparse por la simultaneidad: ¿qué ocurre si alguien más intenta agregar esta fila al mismo tiempo que usted? Envolviendo todo esto en un solo inserto manejaría todo en una transacción implícita (recuerde sus propiedades ACID!):

INSERT Contacs (col1, col2, etc) values (val1, val2, etc) where not exists (select 1 from Contacs where col1 = val1) 
IF @@rowcount = 0 then <didn't insert, process accordingly> 
4

Ni

UPDATE … IF (@@ROWCOUNT = 0) INSERT 

ni

IF EXISTS(...) UPDATE ELSE INSERT 

patrones funcionan como se esperaba bajo alta concurrencia. Ambos pueden fallar. Ambos pueden fallar con mucha frecuencia. MERGE es el rey, se sostiene mucho mejor. Hagamos algunas pruebas de estrés y compruébalo por nosotros mismos.

Aquí está la tabla estaremos usando:

CREATE TABLE dbo.TwoINTs 
    (
     ID INT NOT NULL PRIMARY KEY, 
     i1 INT NOT NULL , 
     i2 INT NOT NULL , 
     version ROWVERSION 
    ) ; 
GO 

INSERT INTO dbo.TwoINTs 
     (ID, i1, i2) 
VALUES (1, 0, 0) ;  

Si existe (...), entonces patrón de falla con frecuencia a alta concurrencia.

Permítanos insertar o actualizar filas en un bucle usando la siguiente lógica simple: si existe una fila con una ID dada, actualícela y de lo contrario inserte una nueva. El siguiente ciclo implementa esta lógica. Córtela y péguela en dos pestañas, cambie al modo de texto en ambas pestañas y ejecútelas simultáneamente.

-- hit Ctrl+T to execute in text mode 

SET NOCOUNT ON ; 

DECLARE @ID INT ; 

SET @ID = 0 ; 
WHILE @ID > -100000 
    BEGIN ; 
     SET @ID = (SELECT MIN(ID) 
        FROM dbo.TwoINTs 
       ) - 1 ; 
     BEGIN TRY ; 

      BEGIN TRANSACTION ; 
      IF EXISTS (SELECT * 
         FROM dbo.TwoINTs 
         WHERE ID = @ID) 
       BEGIN ; 
        UPDATE dbo.TwoINTs 
        SET  i1 = 1 
        WHERE ID = @ID ; 
       END ; 
      ELSE 
       BEGIN ; 
        INSERT INTO dbo.TwoINTs 
          (ID, i1, i2) 
        VALUES (@ID, 0, 0) ; 
       END ; 
      COMMIT ; 
     END TRY 
     BEGIN CATCH ; 
      ROLLBACK ; 
      SELECT error_message() ; 
     END CATCH ; 
    END ; 

Cuando ejecutamos este script simultáneamente en dos pestañas, vamos a obtener de inmediato una gran cantidad de violaciónes de clave principal en ambas pestañas. Esto demuestra cuán poco confiable es el patrón IF EXISTS cuando se ejecuta bajo alta concurrencia.

Nota: este ejemplo también demuestra que no es seguro utilizar SELECT MAX (ID) +1 o SELECT MIN (ID) -1 como el siguiente valor único disponible si lo hacemos bajo concurrencia.

0
IF EXISTS....UPDATE 

No practico. Fuerza dos escaneos/búsquedas en lugar de uno.

Si la actualización no encuentra una coincidencia en la cláusula WHERE, el costo de la declaración de actualización es solo una búsqueda/exploración.

Si encuentra una coincidencia, y si la antepone con IF EXISTS, tiene que encontrar la misma coincidencia dos veces. Y en un entorno concurrente, lo que era cierto para los EXISTS puede que ya no sea cierto para la ACTUALIZACIÓN.

Esto es precisamente por lo que las instrucciones UPDATE/DELETE/INSERT permiten una cláusula WHERE. Úselo!

Cuestiones relacionadas