2010-08-04 22 views
62

Siempre había usado algo similar al siguiente para lograrlo:insertar una fila si no está ya allí

INSERT INTO TheTable 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 
     NULL 
    FROM 
     TheTable 
    WHERE 
     PrimaryKey = @primaryKey) 

... pero una vez bajo carga, se produjo una violación de clave principal. Esta es la única declaración que se inserta en esta tabla. Entonces, ¿esto significa que la declaración anterior no es atómica?

El problema es que esto es casi imposible de recrear a voluntad.

Tal vez podría cambiarlo al algo como lo siguiente:

INSERT INTO TheTable 
WITH 
    (HOLDLOCK, 
    UPDLOCK, 
    ROWLOCK) 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 
     NULL 
    FROM 
     TheTable 
    WITH 
     (HOLDLOCK, 
     UPDLOCK, 
     ROWLOCK) 
    WHERE 
     PrimaryKey = @primaryKey) 

Aunque, tal vez estoy usando las cerraduras incorrecta o el uso excesivo de bloqueo o algo así.

He visto otras preguntas en stackoverflow.com donde las respuestas sugieren "IF (SELECT COUNT (*) ... INSERT", etc., pero siempre estuve bajo la suposición (tal vez incorrecta) de que una sola instrucción SQL sería atómica.

¿alguien tiene alguna idea?

+3

¿Ha intentado usar una fusión sin una cláusula 'CUANDO ESTÁ ENCAMINADO'? –

+3

¿En qué versión de SQL Server estás? –

+0

Varía según el cliente. Cualquier cosa entre e incluyendo 2000 y 2008 R2. ¡Aunque pudimos haber estado en 7 cuando la declaración fue escrita originalmente! – Adam

Respuesta

51

¿Qué pasa con el patrón "JFDI"?

BEGIN TRY 
    INSERT etc 
END TRY 
BEGIN CATCH 
    IF ERROR_NUMBER() <> 2627 
     RAISERROR etc 
END CATCH 

En serio, este es el más rápido y el más concurrente sin bloqueos, especialmente en grandes volúmenes. ¿Qué sucede si el UPDLOCK se escala y toda la tabla está bloqueada?

Read lesson 4:

Lección 4: Al desarrollar el proc upsert antes del ajuste de los índices, lo primero que confiaba en que la línea If Exists(Select…) dispararía a cualquier elemento y prohibiría duplicados. Nada. En poco tiempo, había miles de duplicados porque el mismo elemento chocaría con el upsert en el mismo milisegundo y ambas transacciones verían que no existe y realizaría la inserción. Después de muchas pruebas, la solución fue utilizar el índice único, detectar el error y volver a intentar, permitiendo que la transacción vea la fila y realice una actualización en lugar de una inserción.

+0

Gracias, está bien, estoy de acuerdo en que esto es probablemente lo que terminaré usando, y es la respuesta a la pregunta real. – Adam

+1

Sé que es malo confiar en errores como este, pero me pregunto si hacerlo con solo un 'INSERT' directo (sin los' EXISTS') funcionaría mejor (es decir, intente insertar sin importar nada y simplemente ignore el error 2627). – Adam

+0

Eso depende de si usted inserta principalmente valores que no existen o, en su mayoría, valores que * do * existen. En el último caso, yo diría que el rendimiento será más pobre debido a toneladas de excepciones que se plantean e ignoran. – GSerg

1

no sé si esta es la forma "oficial", pero se puede probar el INSERT, y caer de nuevo a UPDATE si falla.

21

Agregué HOLDLOCK que no estaba presente originalmente. No tenga en cuenta la versión sin esta pista

En lo que a mí respecta, esto debería ser suficiente:

INSERT INTO TheTable 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 0 
    FROM TheTable WITH (UPDLOCK, HOLDLOCK) 
    WHERE PrimaryKey = @primaryKey) 

Además, si en realidad se quiere actualizar una fila si es que existe e insertar si no lo hace, es posible encontrar this question útil.

+1

¿Qué está bloqueando cuando la fila no existe? –

+2

Un rango relevante en el índice (la clave principal en este caso). – GSerg

+0

@GSerg de acuerdo. El bloqueo pesimista/optimista de la instrucción select necesita una directiva. – DaveWilliamson

-3

He realizado una operación similar en el pasado con un método diferente. Primero, declaro una variable para mantener la clave primaria. Luego lleno esa variable con el resultado de una declaración de selección que busca un registro con esos valores. Entonces hago y declaración IF. Si la clave principal es nula, inserte, en caso contrario, devuelva un código de error.

 DECLARE @existing varchar(10) 
    SET @existing = (SELECT primaryKey FROM TABLE WHERE param1field = @param1 AND param2field = @param2) 

    IF @existing is not null 
    BEGIN 
    INSERT INTO Table(param1Field, param2Field) VALUES(param1, param2) 
    END 
    ELSE 
    Return 0 
END 
+0

¿Por qué no hacer: SI NO EXISTE (SELECT * FROM tabla WHERE param1field = @ param1 = Y param2field @ param2) COMENZAR INSERT INTO tabla (param1Field, param2Field) VALORES (param1, param2) END –

+0

Sí, pero parece que está abierto a problemas de concurrencia (es decir, ¿qué pasa si sucede algo en otra conexión entre su selección y su inserción?) – Adam

+2

@Adam El código de Marc anterior no es mejor para evitar problemas de bloqueo. Las únicas dos formas de manejar problemas de simultaneidad son bloquear utilizando WITH (UPDLOCK, HOLDLOCK) o manejar el error de inserción y convertirlo a una actualización. – ErikE

15

Se podría utilizar la combinación de:

MERGE INTO Target 
USING (VALUES (@primaryKey, @value1, @value2)) Source (key, value1, value2) 
ON Target.key = Source.key 
WHEN MATCHED THEN 
    UPDATE SET value1 = Source.value1, value2 = Source.value2 
WHEN NOT MATCHED BY TARGET THEN 
    INSERT (Name, ReasonType) VALUES (@primaryKey, @value1, @value2) 
+0

En este caso, puede eliminar el mensaje 'CUANDO SE HAYA CONOCIDO ENTONCES', ya que Adam solo debe insertarlo si falta, no lo inserta. – Iain

+3

Lo sentimos, pero sin agregar sugerencias de bloqueo de bloqueo a su declaración de fusión, tendrá el problema exacto que le preocupa al OP. – EBarr

+7

Ver [este artículo] (http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx) para obtener más información sobre el punto –

0

En primer lugar, enorme grito a nuestra @gbn hombre por sus contribuciones a la comunidad. Ni siquiera puedo empezar a explicar con qué frecuencia me encuentro siguiendo su consejo.

De todos modos, suficiente fanboy-ing.

Para agregar un poco a su respuesta, tal vez "mejorar". Para aquellos, como yo, nos sentimos incómodos con qué hacer en el escenario <> 2627 (y no hay un CATCH vacío no es una opción). Encontré esta pequeña pepita de technet.

BEGIN TRY 
     INSERT etc 
    END TRY 
    BEGIN CATCH 
     IF ERROR_NUMBER() <> 2627 
      BEGIN 
       DECLARE @ErrorMessage NVARCHAR(4000); 
       DECLARE @ErrorSeverity INT; 
       DECLARE @ErrorState INT; 

       SELECT @ErrorMessage = ERROR_MESSAGE(), 
       @ErrorSeverity = ERROR_SEVERITY(), 
       @ErrorState = ERROR_STATE(); 

        RAISERROR (
         @ErrorMessage, 
         @ErrorSeverity, 
         @ErrorState 
        ); 
      END 
    END CATCH 
Cuestiones relacionadas