2010-08-29 18 views
11

Quiero hacer la versión SELECT/INSERT de un UPSERT. A continuación se muestra una plantilla del código existente:Seleccionar/Insertar versión de un Upsert: ¿hay un patrón de diseño para alta concurrencia?

// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50)) 

IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE) 
BEGIN 
    INSERT Table VALUES (@Value) 
    SELECT @id = SCOPEIDENTITY() 
END 
ELSE 
    SELECT @id = RowID FROM Table WHERE RowValue = @VALUE) 

Se llamará a la consulta desde muchas sesiones concurrentes. Mis pruebas de rendimiento muestran que emitirá consistentemente infracciones de clave primaria bajo una carga específica.

¿Existe un método de alta concurrencia para esta consulta que le permita mantener el rendimiento al tiempo que se evita la inserción de datos que ya existen?

+1

Esto es similar a: http://stackoverflow.com/questions/13540/insert-update-stored-proc-on-sql-server/193876#193876 –

+0

Estoy de acuerdo, es similar, pero yo diría que la diferencia es que no hay necesidad de una actualización, solo una instrucción de inserción o selección.En su respuesta, usa SERIALIZABLE como una sugerencia conjunta. ¿Sería esa tu recomendación para la consulta anterior en la declaración SELECT? – 8kb

+0

Yerp una inserción con la pista serializable, potencialmente en una transacción debería hacer el truco. Puedo intentar escribir una respuesta, pero este iPad me hace sonar descarado :( –

Respuesta

15

Puede usar los BLOQUEOS para hacer cosas SERIALIZABLES, pero esto reduce la simultaneidad. ¿Por qué no probar primero la condición común ("principalmente insertar o seleccionar en su mayoría") seguido de un manejo seguro de la acción "remedial"? Es decir, el patrón "JFDI" ...

Parcialmente inserciones esperadas (estadio de béisbol 70-80% +):

Sólo trate de insertar. Si falla, la fila ya se ha creado. No necesita preocuparse por la concurrencia porque TRY/CATCH se ocupa de los duplicados por usted.

BEGIN TRY 
    INSERT Table VALUES (@Value) 
    SELECT @id = SCOPEIDENTITY() 
END TRY 
BEGIN CATCH 
    IF ERROR_NUMBER() <> 2627 
     RAISERROR etc 
    ELSE -- only error was a dupe insert so must already have a row to select 
     SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
END CATCH 

Parcialmente seleccionados:

similares, pero trata de obtener datos de primera. Sin datos = INSERT es necesario. Nuevamente, si 2 llamadas simultáneas intentan INSERTAR porque ambas encontraron la fila que faltaba en los controles TRY/CATCH.

BEGIN TRY 
    SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
    IF @@ROWCOUNT = 0 
    BEGIN 
     INSERT Table VALUES (@Value) 
     SELECT @id = SCOPEIDENTITY() 
    END 
END TRY 
BEGIN CATCH 
    IF ERROR_NUMBER() <> 2627 
     RAISERROR etc 
    ELSE 
     SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
END CATCH 

La segunda parece repetirse, pero es muy concurrente. Cerraduras lograrían el mismo, pero a expensas de concurrencia ...

Editar:

Por qué no utilizar la combinación de ...

si se utiliza la cláusula OUTPUT lo hará solamente retorno lo que está actualizado Por lo tanto, necesita una ACTUALIZACIÓN ficticia para generar la tabla INSERTED para la cláusula OUTPUT. Si tiene que hacer actualizaciones ficticias con muchas llamadas (como lo implica OP) es una gran cantidad de logs de registro solo para poder usar MERGE.

+0

@gbn - ¿por qué usaría su segunda sugerencia (para alta concurrencia) en lugar del comando MERGE? (suponiendo que @ 8kb está usando sql 20 08+)? –

+0

@ Pure.Krome: porque es insertar/seleccionar, no insertar/actualizar. Terminará con una actualización ficticia para usar OUTPUT. Estupendo. – gbn

+0

@gbn, por lo que no podemos verificar SCOPE_IDENTITY para obtener la _última/última identidad_ que se insertó ... suponiendo que se haya realizado una inserción. si no lo hizo, entonces puede usar @@ Rowcount para verificar que la actualización funcionó ... ?? –

1
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50)) 

- asegúrese de tener un índice único no agrupado en RowValue y RowID como su índice agrupado.

IF EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE) 
    SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
ELSE BEGIN 
    INSERT Table VALUES (@Value) 
    SELECT @id = SCOPEIDENTITY() 
END 
0

Como siempre, la respuesta de gbn es correcta y, en última instancia, me llevó a donde tenía que estar. Sin embargo, encontré un caso particular de borde que no estaba cubierto por su enfoque. Que es un error 2601 que identifica un Unique Index Violation.

Para compensar esto, he modificado el código como sigue

... 
declare @errornumber int = ERROR_NUMBER() 
if @errornumber <> 2627 and @errornumber <> 2601 
... 

Esperamos que esto ayude a alguien!

Cuestiones relacionadas