sql >> Base de Datos >  >> RDS >> Sqlserver

UPSERT atómico en SQL Server 2005

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
  • hay una condición de carrera en el primer INSERT. Es posible que la clave no exista durante la consulta interna SELECCIONAR, pero sí existe en el momento de INSERTAR, lo que resulta en una violación de la clave.
  • hay una condición de carrera entre INSERTAR y ACTUALIZAR. La clave puede existir cuando se verifica en la consulta interna de INSERTAR, pero desaparece cuando se ejecuta la ACTUALIZACIÓN.

Para la segunda condición de carrera, se podría argumentar que el subproceso concurrente habría eliminado la clave de todos modos, por lo que no es realmente una actualización perdida.

La solución óptima suele ser probar el caso más probable y manejar el error si falla (dentro de una transacción, por supuesto):

  • si es probable que falte la clave, insértela siempre primero. Manejar la violación de la restricción única, volver a actualizar.
  • si es probable que la clave esté presente, siempre actualice primero. Insertar si no se encontró ninguna fila. Manejar posible violación de restricción única, respaldo para actualizar.

Además de la corrección, este patrón también es óptimo para la velocidad:es más eficiente tratar de insertar y manejar la excepción que hacer bloqueos espurios. Los bloqueos significan lecturas de páginas lógicas (que pueden significar lecturas de páginas físicas), y IO (incluso lógico) es más costoso que SEH.

Actualizar @Pedro

¿Por qué una sola declaración no es 'atómica'? Digamos que tenemos una tabla trivial:

create table Test (id int primary key);

Ahora, si ejecutara esta declaración única desde dos subprocesos, en un bucle, sería 'atómica', como usted dice, puede existir una condición de no carrera:

  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

Sin embargo, en solo un par de segundos, se produce una violación de la clave principal:

Mensaje 2627, Nivel 14, Estado 1, Línea 4
Violación de la restricción PRIMARY KEY 'PK__Test__24927208'. No se puede insertar una clave duplicada en el objeto 'dbo.Test'.

¿Porqué es eso? Tiene razón en que el plan de consulta SQL hará lo 'correcto' en DELETE ... FROM ... JOIN , en WITH cte AS (SELECT...FROM ) DELETE FROM cte y en muchos otros casos. Pero hay una diferencia crucial en estos casos:la 'subconsulta' se refiere al objetivo de una actualización o eliminar operación. Para tales casos, el plan de consulta usará un bloqueo apropiado, de hecho, este comportamiento es crítico en ciertos casos, como cuando se implementan colas usando tablas como colas.

Pero en la pregunta original, así como en mi ejemplo, el optimizador de consultas ve la subconsulta como una subconsulta en una consulta, no como una consulta especial de tipo 'buscar actualizaciones' que necesita una protección de bloqueo especial. El resultado es que la ejecución de la búsqueda de subconsulta puede ser observada como una operación distinta por un observador concurrente , rompiendo así el comportamiento 'atómico' de la declaración. A menos que se tomen precauciones especiales, varios subprocesos pueden intentar insertar el mismo valor, ambos convencidos de que lo han verificado y el valor aún no existe. Solo uno puede tener éxito, el otro golpeará la violación de PK. QED.