2010-03-26 15 views
47

¿Cuál es el patrón correcto para hacer un "UPSERT" atómico (ACTUALIZAR cuando existe, INSERTAR de lo contrario) en SQL Server 2005?UPSERT atómico en SQL Server 2005

I ver una gran cantidad de código en SO (por ejemplo, véase Check if a row exists, otherwise insert) con el siguiente patrón de dos partes:

UPDATE ... 
FROM ... 
WHERE <condition> 
-- race condition risk here 
IF @@ROWCOUNT = 0 
    INSERT ... 

o

IF (SELECT COUNT(*) FROM ... WHERE <condition>) = 0 
    -- race condition risk here 
    INSERT ... 
ELSE 
    UPDATE ... 

donde < condición> será una evaluación de los recursos naturales llaves. Ninguno de los enfoques anteriores parece tratar bien con la concurrencia. Si no puedo tener dos filas con la misma clave natural, parece que todo el riesgo anterior inserta filas con las mismas claves naturales en los escenarios de condición de carrera.

He estado usando el siguiente enfoque pero me sorprende no ver en cualquier lugar de respuestas de la gente por lo que me pregunto lo que está mal con él:

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> 

Tenga en cuenta que la condición de carrera es mencionado aquí una diferente de las del código anterior. En el código anterior, el problema eran las lecturas fantasmas (filas que se insertan entre UPDATE/IF o entre SELECT/INSERT por otra sesión). En el código anterior, la condición de carrera tiene que ver con DELETE. ¿Es posible que una sesión coincidente sea eliminada por otra sesión DESPUÉS de que se ejecute (DONDE NO EXISTE) pero antes de que se ejecute INSERT? No está claro dónde DONDE NO EXISTE pone un candado en algo junto con la ACTUALIZACIÓN.

¿Esto es atómico? No puedo encontrar dónde se documentará esto en la documentación de SQL Server.

EDIT: Me doy cuenta de que esto se puede hacer con las transacciones, pero creo que tendría que establecer el nivel de transacción en SERIALIZABLE para evitar el problema de lectura fantasma? Seguramente eso es exagerado para un problema tan común?

+0

Mladen Prajdić tiene un artículo aquí que puede ser interesante. http://www.sqlteam.com/article/application-locks-or-mutexes-in-sql-server-2005 y aquí http://weblogs.sqlteam.com/mladenp/archive/2007/07/30/60273 .aspx –

+2

El *** patrón *** correcto para * cualquier * solicitud que implique la palabra "Atomic" y más de una instrucción SQL debe * siempre * estar obligado con BEGIN TRANSACTION y COMMIT/ROLLBACK. – RBarryYoung

Respuesta

2

Un truco que he visto es probar INSERTAR y, si falla, realizar la ACTUALIZACIÓN.

+2

Las inserciones atómicas condicional son generalmente más rápidas que un bloque TRY CATCH. Consulte aquí para obtener una evaluación comparativa: http://stackoverflow.com/questions/1688618/sql-insert-but-avoid-duplicates/1689104#1689104 –

27
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> 
  • existe una condición de carrera en el primer INSERT. Es posible que la clave no exista durante la consulta interna SELECT, pero existe al momento de INSERT, lo que da como resultado una violación de clave.
  • hay una condición de carrera entre INSERT y UPDATE. La clave puede existir cuando está marcada en la consulta interna de INSERT, pero desaparece cuando se ejecuta ACTUALIZACIÓN.

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

La solución óptima es por lo general para tratar el caso más probable, y controlar el error si se produce un error (dentro de una transacción, por supuesto):

  • si la clave es probable que falta, inserte siempre primero. Maneje la violación única de restricción, repliegue para 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, alternativa para actualizar.

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

actualización @Peter

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

create table Test (id int primary key); 

Ahora bien, si había corrido esta sola declaración de dos hilos, en un bucle, sería 'atómica', como usted dice, puede existir una condición de ninguna raza:

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 sólo un par de segundos, una violación de clave principal se produce:

Msg 2627, nivel 14, estado 1, línea 4
Violación de restricción PRIMARY KEY 'PK__Test__2492 7208 '. No se puede insertar una clave duplicada en el objeto 'dbo.Test'.

¿Por qué 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 borre operación. En tales casos, el plan de consulta utilizará un bloqueo apropiado, de hecho, este comportamiento es crítico en ciertos casos, como cuando se implementan las colas Using tables as Queues.

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 'escanear para actualizar' que necesita protección de bloqueo especial. El resultado es que la ejecución de la búsqueda de subconsulta se puede observar como una operación distinta por un observador concurent, rompiendo así el comportamiento 'atómico' de la instrucción. A menos que se tome una precaución especial, varios hilos pueden intentar insertar el mismo valor, ambos convencidos de que se han verificado y el valor no existe. Solo uno puede tener éxito, el otro golpeará la violación PK. QED.

+0

Esto es incorrecto. La inserción condicional w/where cláusula es atómica. No hay condiciones de carrera. –

+3

@Peter: el hecho de que esté dentro de una transacción automática no lo convierte en atómico. –

+0

Si proporciona una cita, podría estar convencido, pero no tiene sentido. ¿Qué garantiza la atomicidad en la transacción explícita de declaraciones múltiples? La transacción. ¿Cómo es esa transacción diferente a una transacción de declaración única, implícita o no? ¿El INSERT ... DONDE está desenvuelto en dos transacciones? No. Entonces, o es atómico, o las transacciones no garantizan la atomicidad. Si las transacciones no garantizan la atomicidad, entonces ¿por qué molestarse en envolver su ACTUALIZACIÓN ... INSERTAR en una transacción? –

3

EDIT: Remus es correcto, la cláusula condicional w/where no garantiza un estado coherente entre la subconsulta correlacionada y la inserción de la tabla.

Quizás las sugerencias correctas de la tabla podrían forzar un estado consistente. INSERT <table> WITH (TABLOCKX, HOLDLOCK) parece funcionar, pero no tengo idea si ese es el nivel óptimo de bloqueo para una inserción condicional.

En una prueba trivial como la descrita por Remus, TABLOCKX, HOLDLOCK mostró ~ 5x el volumen de inserción sin sugerencias de tabla, y sin los errores PK o el curso.

respuesta original, incorrecto:

Es esta atómica?

Sí, el inserto condicional w/cláusula where es atómica, y su forma INSERT ... WHERE NOT EXISTS() ... UPDATE es la forma correcta de realizar una UPSERT.

yo añadiría IF @@ROWCOUNT = 0 entre la inserción y actualización:

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

IF @@ROWCOUNT = 0 BEGIN 
    UPDATE ... 
    WHERE <natural keys> 
END 

declaraciones individuales siempre se ejecutan dentro de una transacción, ya sea propio (autocommit y implicit) o junto con otras pautas (explicit).

+0

¡Gracias por esos recursos! Como señala Remus, eso no garantiza que sea atómico, así que voy a tener que seguir el enfoque de bloqueo explícito de Arthur a pesar de que es más feo IMO :( – rabidpebble

+0

@rabidpebble: por lo que sé, Remus está equivocado. Las declaraciones son * siempre * ejecutadas dentro de una transacción, y la transacción garantiza la atomicidad. Si no fuera así, ¿por qué molestaría alguna vez las transacciones explícitas de declaración múltiple? –

6

Pasar updlock, rowlock, holdlock insinúa cuando se prueba la existencia de la fila. Holdlock asegura que todas las inserciones estén serializadas; rowlock permite actualizaciones concurrentes a filas existentes.

Las actualizaciones aún pueden bloquearse si su PK es un bigint, ya que el hash interno está degenerado para los valores de 64 bits.

begin tran -- default read committed isolation level is fine 

if not exists (select * from <table> with (updlock, rowlock, holdlock) where <PK = ...> 
    -- insert 
else 
    -- update 

commit