Soluciones para INSERTAR O ACTUALIZAR en SQL Server


599

Suponga una estructura de tabla de MyTable(KEY, datafield1, datafield2...).

A menudo quiero actualizar un registro existente o insertar un nuevo registro si no existe.

Esencialmente:

IF (key exists)
  run update command
ELSE
  run insert command

¿Cuál es la mejor manera de escribir esto?



28
Para cualquier persona que se encuentre con esta pregunta por primera vez, asegúrese de leer todas las respuestas y sus comentarios. La edad a veces puede conducir a información engañosa ...
Aaron Bertrand

1
Considere usar el operador EXCEPT, que se introdujo en SQL Server 2005.
Tarzán,

Respuestas:


370

No te olvides de las transacciones. El rendimiento es bueno, pero el enfoque simple (SI EXISTE ...) es muy peligroso.
Cuando varios subprocesos intenten realizar la inserción o actualización, puede obtener fácilmente la violación de la clave principal.

Las soluciones proporcionadas por @Beau Crawford y @Esteban muestran una idea general pero propensa a errores.

Para evitar puntos muertos y violaciones de PK, puede usar algo como esto:

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert into table (key, ...)
   values (@key, ...)
end
commit tran

o

begin tran
   update table with (serializable) set ...
   where key = @key

   if @@rowcount = 0
   begin
      insert into table (key, ...) values (@key,..)
   end
commit tran

1
Pregunta hecha por la solución más eficaz en lugar de la más segura. Mientras que una transacción agrega seguridad al proceso, también agrega una sobrecarga.
Luke Bennett

31
Ambos métodos aún pueden fallar. Si dos hilos concurrentes hacen lo mismo en la misma fila, el primero tendrá éxito, pero el segundo inserto fallará debido a una violación de clave primaria. Una transacción no garantiza que la inserción tendrá éxito incluso si la actualización falla porque el registro existe. Para garantizar que cualquier número de transacciones concurrentes tendrá éxito, DEBE utilizar un candado.
Jean Vincent

77
@aku ¿alguna razón por la que usó sugerencias de tabla ("con (xxxx)") en lugar de "ESTABLECER NIVEL DE AISLAMIENTO DE TRANSACCIÓN SERIALIZABLE" justo antes de COMENZAR TRAN?
EBarr

44
@CashCow, el último gana, esto es lo que se supone que debe hacer INSERT o UPDATE: el primero se inserta, el segundo actualiza el registro. Agregar un bloqueo permite que esto suceda en un período de tiempo muy corto, evitando un error.
Jean Vincent

1
Siempre pensé que usar pistas de bloqueo son malas, y deberíamos dejar que el motor interno de Microsoft dicte bloqueos. ¿Es esta la aparente excepción a la regla?

382

Vea mi respuesta detallada a una pregunta anterior muy similar

@Beau Crawford's es una buena manera en SQL 2005 y versiones posteriores, aunque si está otorgando representante, debería ir al primer tipo en SO . El único problema es que para las inserciones siguen siendo dos operaciones de E / S.

MS Sql2008 introduce mergedesde el estándar SQL: 2003:

merge tablename with(HOLDLOCK) as target
using (values ('new value', 'different value'))
    as source (field1, field2)
    on target.idfield = 7
when matched then
    update
    set field1 = source.field1,
        field2 = source.field2,
        ...
when not matched then
    insert ( idfield, field1, field2, ... )
    values ( 7,  source.field1, source.field2, ... )

Ahora es realmente solo una operación de E / S, pero un código horrible :-(


10
@Ian Boyd: sí, esa es la sintaxis del estándar SQL: 2003, no la upsertque casi todos los demás proveedores de bases de datos decidieron admitir. La upsertsintaxis es una forma mucho más agradable de hacer esto, por lo que al menos MS también debería haberla admitido. No es como si fuera la única palabra clave no estándar en T-SQL
Keith

1
¿Algún comentario sobre la pista de bloqueo en otras respuestas? (lo descubriré pronto, pero si es la forma recomendada, recomiendo agregarlo en la respuesta)
eglasius

25
Consulte aquí weblogs.sqlteam.com/dang/archive/2009/01/31/… para obtener una respuesta sobre cómo evitar que las condiciones de carrera causen errores que pueden ocurrir incluso cuando se usa la MERGEsintaxis.
Seph

55
@Seph es una verdadera sorpresa, algo así como un error de Microsoft: -Supongo que eso significa que necesita una HOLDLOCKoperación de fusión en situaciones de alta concurrencia.
Keith

11
Esta respuesta realmente necesita actualizarse para tener en cuenta el comentario de Seph sobre que no es seguro para subprocesos sin un HOLDLOCK. Según la publicación vinculada, MERGE elimina implícitamente un bloqueo de actualización, pero lo libera antes de insertar filas, lo que puede causar una condición de carrera y violaciones de la clave principal en la inserción. Al usar HOLDLOCK, los bloqueos se mantienen hasta después de que ocurre la inserción.
Triynko

169

Haz un UPSERT:

ACTUALIZAR MyTable SET FieldA = @ FieldA WHERE Key = @ Key

IF @@ ROWCOUNT = 0
   INSERTAR EN LOS VALORES MyTable (FieldA) (@FieldA)

http://en.wikipedia.org/wiki/Upsert


77
Las infracciones de la clave principal no deberían ocurrir si tiene aplicadas las restricciones de índice únicas adecuadas. El objetivo de la restricción es evitar que sucedan filas duplicadas. No importa cuántos hilos intente insertar, la base de datos se serializará según sea necesario para imponer la restricción ... y si no lo hace, entonces el motor no tiene valor. Por supuesto, envolver esto en una transacción serializada lo haría más correcto y menos susceptible a puntos muertos o inserciones fallidas.
Triynko

19
@Triynko, creo que @Sam Saffron significa que si dos hilos + se intercalan en la secuencia correcta, el servidor SQL arrojará un error que indica que se habría producido una violación de la clave principal . Envolverlo en una transacción serializable es la forma correcta de evitar errores en el conjunto de declaraciones anteriores.
EBarr

1
Incluso si tiene una clave primaria que es un incremento automático, su preocupación será entonces cualquier restricción única que pueda estar sobre la mesa.
Seph

1
la base de datos debe ocuparse de los principales problemas clave. Lo que está diciendo es que si la actualización falla y otro proceso llega primero con una inserción, su inserción fallará. En ese caso, tienes una condición de carrera de todos modos. El bloqueo no cambiará el hecho de que la condición posterior será que uno de los procesos que intenta escribir obtendrá el valor.
CashCow

93

Mucha gente le sugerirá que use MERGE, pero le advierto que no lo haga. Por defecto, no lo protege de concurrencia y condiciones de carrera más que múltiples declaraciones, e introduce otros peligros:

http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/

Incluso con esta sintaxis "más simple" disponible, sigo prefiriendo este enfoque (el manejo de errores se omite por brevedad):

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END
COMMIT TRANSACTION;

Mucha gente sugerirá de esta manera:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
  INSERT ...
END
COMMIT TRANSACTION;

Pero todo lo que se logra es garantizar que deba leer la tabla dos veces para ubicar las filas que se actualizarán. En la primera muestra, solo necesitará ubicar las filas una vez. (En ambos casos, si no se encuentran filas de la lectura inicial, se produce una inserción).

Otros sugerirán de esta manera:

BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

Sin embargo, esto es problemático si no es por otra razón que dejar que SQL Server capture excepciones que podría haber evitado en primer lugar es mucho más costoso, excepto en el raro escenario en el que casi cada inserción falla. Aquí lo pruebo:


3
¿Qué pasa con la inserción / actualización DESDE una tabla tem que inserta / actualiza muchos registros?
user960567

@ user960567 Bueno,UPDATE target SET col = tmp.col FROM target INNER JOIN #tmp ON <key clause>; INSERT target(...) SELECT ... FROM #tmp AS t WHERE NOT EXISTS (SELECT 1 FROM target WHERE key = t.key);
Aaron Bertrand

44
nice respondió después de más de 2 años :)
user960567

12
@ user960567 Lo siento, no siempre recibo notificaciones de comentarios en tiempo real.
Aaron Bertrand

60
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

Editar:

Por desgracia, incluso en mi propio detrimento, debo admitir que las soluciones que hacen esto sin una selección parecen ser mejores, ya que logran la tarea con un paso menos.


66
Todavía me gusta más este. El upsert se parece más a la programación por efecto secundario, y nunca he visto la pequeña búsqueda de índice agrupado de esa selección inicial para causar problemas de rendimiento en una base de datos real.
Eric Z Beard

38

Si desea UPSERTAR más de un registro a la vez, puede usar la declaración ANSI SQL: 2003 DML MERGE.

MERGE INTO table_name WITH (HOLDLOCK) USING table_name ON (condition)
WHEN MATCHED THEN UPDATE SET column1 = value1 [, column2 = value2 ...]
WHEN NOT MATCHED THEN INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...])

Consulte la declaración Mimicking MERGE en SQL Server 2005 .


1
En Oracle, emitir una declaración MERGE creo que bloquea la mesa. ¿Sucede lo mismo en SQL * Server?
Mike McAllister

13
MERGE es susceptible a las condiciones de carrera (consulte weblogs.sqlteam.com/dang/archive/2009/01/31/… ) a menos que haga que tenga bloqueos certian. Además, eche un vistazo al rendimiento de MERGE en SQL Profiler ... encuentro que es típicamente más lento y genera más lecturas que soluciones alternativas.
EBarr

@EBarr - Gracias por el enlace en las cerraduras. He actualizado mi respuesta para incluir la sugerencia de bloqueo sugerida.
Eric Weilnau


10

Aunque es bastante tarde para comentar sobre esto, quiero agregar un ejemplo más completo usando MERGE.

Dichas declaraciones Insert + Update generalmente se llaman declaraciones "Upsert" y se pueden implementar usando MERGE en SQL Server.

Aquí se da un muy buen ejemplo: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

Lo anterior explica los escenarios de bloqueo y concurrencia también.

Citaré lo mismo para referencia:

ALTER PROCEDURE dbo.Merge_Foo2
      @ID int
AS

SET NOCOUNT, XACT_ABORT ON;

MERGE dbo.Foo2 WITH (HOLDLOCK) AS f
USING (SELECT @ID AS ID) AS new_foo
      ON f.ID = new_foo.ID
WHEN MATCHED THEN
    UPDATE
            SET f.UpdateSpid = @@SPID,
            UpdateTime = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT
      (
            ID,
            InsertSpid,
            InsertTime
      )
    VALUES
      (
            new_foo.ID,
            @@SPID,
            SYSDATETIME()
      );

RETURN @@ERROR;

1
Hay otras cosas de las que preocuparse con MERGE: mssqltips.com/sqlservertip/3074/…
Aaron Bertrand

8
/*
CREATE TABLE ApplicationsDesSocietes (
   id                   INT IDENTITY(0,1)    NOT NULL,
   applicationId        INT                  NOT NULL,
   societeId            INT                  NOT NULL,
   suppression          BIT                  NULL,
   CONSTRAINT PK_APPLICATIONSDESSOCIETES PRIMARY KEY (id)
)
GO
--*/

DECLARE @applicationId INT = 81, @societeId INT = 43, @suppression BIT = 0

MERGE dbo.ApplicationsDesSocietes WITH (HOLDLOCK) AS target
--set the SOURCE table one row
USING (VALUES (@applicationId, @societeId, @suppression))
    AS source (applicationId, societeId, suppression)
    --here goes the ON join condition
    ON target.applicationId = source.applicationId and target.societeId = source.societeId
WHEN MATCHED THEN
    UPDATE
    --place your list of SET here
    SET target.suppression = source.suppression
WHEN NOT MATCHED THEN
    --insert a new line with the SOURCE table one row
    INSERT (applicationId, societeId, suppression)
    VALUES (source.applicationId, source.societeId, source.suppression);
GO

Reemplace los nombres de tabla y campo por lo que necesite. Tenga cuidado con el uso de la condición ON . Luego establezca el valor apropiado (y el tipo) para las variables en la línea DECLARE.

Salud.


7

Puede usar la MERGEDeclaración, esta declaración se utiliza para insertar datos si no existe o actualizar si existe.

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`

@RamenChef No entiendo. ¿Dónde están las cláusulas WHEN MATCHED?
likejudo

@likejudo No escribí esto; Solo lo revisé. Pregúntale al usuario que escribió la publicación.
RamenChef

5

Si va a ACTUALIZAR si no hay filas actualizadas, luego INSERTAR ruta, considere hacer INSERTAR primero para evitar una condición de carrera (suponiendo que no haya DELETE interviniente)

INSERT INTO MyTable (Key, FieldA)
   SELECT @Key, @FieldA
   WHERE NOT EXISTS
   (
       SELECT *
       FROM  MyTable
       WHERE Key = @Key
   )
IF @@ROWCOUNT = 0
BEGIN
   UPDATE MyTable
   SET FieldA=@FieldA
   WHERE Key=@Key
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END

Además de evitar una condición de carrera, si en la mayoría de los casos el registro ya existirá, esto hará que INSERT falle, desperdiciando la CPU.

Usar MERGE probablemente sea preferible para SQL2008 en adelante.


Idea interesante, pero sintaxis incorrecta. SELECT necesita un FROM <table_source> y un TOP 1 (a menos que el table_source elegido tenga solo 1 fila).
jk7

Gracias. Lo he cambiado a NO EXISTE. Solo habrá una fila coincidente debido a la prueba de "clave" según O / P (aunque puede que sea una clave de varias partes :))
Kristen

4

Eso depende del patrón de uso. Uno tiene que mirar el panorama general de uso sin perderse en los detalles. Por ejemplo, si el patrón de uso es 99% de actualizaciones después de que se haya creado el registro, entonces 'UPSERT' es la mejor solución.

Después de la primera inserción (hit), serán todas las actualizaciones de declaraciones individuales, sin ifs o buts. La condición 'dónde' en el inserto es necesaria; de lo contrario, insertará duplicados y no querrá lidiar con el bloqueo.

UPDATE <tableName> SET <field>=@field WHERE key=@key;

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (field)
   SELECT @field
   WHERE NOT EXISTS (select * from tableName where key = @key);
END

2

MS SQL Server 2008 presenta la declaración MERGE, que creo que es parte del estándar SQL: 2003. Como muchos han demostrado, no es un gran problema manejar casos de una fila, pero cuando se trata de grandes conjuntos de datos, se necesita un cursor, con todos los problemas de rendimiento que se presentan. La declaración MERGE será muy bienvenida cuando se trate de grandes conjuntos de datos.


1
Nunca he necesitado usar un cursor para hacer esto con grandes conjuntos de datos. Solo necesita una actualización que actualice los registros que coinciden y una inserción con una selección en lugar de una cláusula de valores que dejó uniones a la tabla.
HLGEM

1

Antes de que todos salten a HOLDLOCK-s por miedo de estos usuarios infames que ejecutan sus sprocs directamente :-) déjenme señalar que deben garantizar la unicidad de los nuevos PK-s por diseño (claves de identidad, generadores de secuencia en Oracle, índices únicos para ID-s externos, consultas cubiertas por índices). Ese es el alfa y omega del problema. Si no tiene eso, ningún HOLDLOCK-s del universo lo salvará y si lo tiene, entonces no necesita nada más que UPDLOCK en la primera selección (o para usar la actualización primero).

Las Sprocs normalmente se ejecutan en condiciones muy controladas y con la suposición de un llamador confiable (nivel medio). Lo que significa que si un patrón de inserción simple (actualización + inserción o combinación) alguna vez ve PK duplicada, eso significa un error en su diseño de tabla o nivel medio y es bueno que SQL grite una falla en tal caso y rechace el registro. Colocar un HOLDLOCK en este caso equivale a comer excepciones y tomar datos potencialmente defectuosos, además de reducir su rendimiento.

Dicho esto, el uso de MERGE o UPDATE y luego INSERT es más fácil en su servidor y menos propenso a errores, ya que no tiene que acordarse de agregar (UPDLOCK) a la primera selección. Además, si está haciendo inserciones / actualizaciones en pequeños lotes, necesita conocer sus datos para decidir si una transacción es apropiada o no. Es solo una colección de registros no relacionados, entonces la transacción "envolvente" adicional será perjudicial.


1
Si solo realiza una actualización, luego inserte sin ningún bloqueo o aislamiento elevado, entonces dos usuarios podrían intentar pasar los mismos datos (no lo consideraría un error en el nivel medio si dos usuarios intentaran enviar exactamente la misma información en Al mismo tiempo, depende mucho del contexto, ¿no?). Ambos ingresan la actualización, que devuelve 0 filas para ambos, luego ambos intentan insertar. Uno gana, el otro obtiene una excepción. Esto es lo que la gente suele tratar de evitar.
Aaron Bertrand

1

¿Importan realmente las condiciones de carrera si primero intentas una actualización seguida de una inserción? Digamos que tiene dos hilos que desean establecer un valor para la clave clave :

Hilo 1: valor = 1
Hilo 2: valor = 2

Ejemplo de escenario de condición de carrera

  1. la clave no está definida
  2. El hilo 1 falla con la actualización
  3. El hilo 2 falla con la actualización
  4. Exactamente uno de los hilos 1 o 2 tiene éxito con la inserción. Ej. Hilo 1
  5. El otro hilo falla con la inserción (con clave duplicada de error) - hilo 2.

    • Resultado: el "primero" de los dos peldaños para insertar, decide el valor.
    • Resultado deseado: el último de los 2 hilos para escribir datos (actualizar o insertar) debe decidir el valor

Pero; en un entorno multiproceso, el programador del sistema operativo decide el orden de ejecución del subproceso; en el escenario anterior, donde tenemos esta condición de carrera, fue el sistema operativo el que decidió la secuencia de ejecución. Es decir: es incorrecto decir que "hilo 1" o "hilo 2" fue "primero" desde el punto de vista del sistema.

Cuando el tiempo de ejecución es tan cercano para el hilo 1 y el hilo 2, el resultado de la condición de carrera no importa. El único requisito debería ser que uno de los hilos definiera el valor resultante.

Para la implementación: si la actualización seguida de la inserción produce el error "clave duplicada", esto debe tratarse como correcto.

Además, uno nunca debería asumir que el valor en la base de datos es el mismo que escribió el último.


1

En SQL Server 2008 puede usar la instrucción MERGE


11
Este es un comentario. en ausencia de un código de ejemplo real, esto es como muchos otros comentarios en el sitio.
swasheck

Muy viejo, pero un ejemplo sería bueno.
Matt McCabe

0

Intenté la solución a continuación y funciona para mí, cuando se produce una solicitud concurrente para la instrucción de inserción.

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert table (key, ...)
   values (@key, ...)
end
commit tran

0

Puedes usar esta consulta. Trabaja en todas las ediciones de SQL Server. Es simple y claro. Pero necesitas usar 2 consultas. Puedes usar si no puedes usar MERGE

    BEGIN TRAN

    UPDATE table
    SET Id = @ID, Description = @Description
    WHERE Id = @Id

    INSERT INTO table(Id, Description)
    SELECT @Id, @Description
    WHERE NOT EXISTS (SELECT NULL FROM table WHERE Id = @Id)

    COMMIT TRAN

NOTA: explique las respuestas negativas


Estoy adivinando la falta de bloqueo?
Zeek2

No falta bloqueo ... Yo uso "TRAN". Las transacciones predeterminadas del servidor SQL tienen bloqueo.
Victor Sanchez

-2

Si usa ADO.NET, el DataAdapter maneja esto.

Si desea manejarlo usted mismo, esta es la forma:

Asegúrese de que haya una restricción de clave principal en su columna de clave.

Entonces tú:

  1. Hacer la actualización
  2. Si la actualización falla porque ya existe un registro con la clave, realice la inserción. Si la actualización no falla, ya ha terminado.

También puede hacerlo al revés, es decir, primero realice la inserción y actualice si la inserción falla. Normalmente, la primera forma es mejor, porque las actualizaciones se realizan con más frecuencia que las inserciones.


... y hacer la inserción primero (sabiendo que algunas veces fallará) es costoso para SQL Server. sqlperformance.com/2012/08/t-sql-queries/error-handling
Aaron Bertrand

-3

Hacer un if existe ... más ... implica hacer dos solicitudes como mínimo (una para verificar, una para tomar medidas) El siguiente enfoque requiere solo uno donde existe el registro, dos si se requiere una inserción:

DECLARE @RowExists bit
SET @RowExists = 0
UPDATE MyTable SET DataField1 = 'xxx', @RowExists = 1 WHERE Key = 123
IF @RowExists = 0
  INSERT INTO MyTable (Key, DataField1) VALUES (123, 'xxx')

-3

Usualmente hago lo que varios de los otros carteles han dicho con respecto a verificar que exista primero y luego hacer lo que sea la ruta correcta. Una cosa que debe recordar al hacer esto es que el plan de ejecución en caché por sql podría no ser óptimo para una ruta u otra. Creo que la mejor manera de hacer esto es llamar a dos procedimientos almacenados diferentes.

FirstSP:
Si existe
   Llame a SecondSP (UpdateProc)
Más
   Llamar a ThirdSP (InsertProc)

Ahora, no sigo mis propios consejos muy a menudo, así que tómalo con un grano de sal.


Esto puede haber sido relevante en versiones antiguas de SQL Server, pero las versiones modernas tienen compilación a nivel de declaración. Las horquillas, etc., no son un problema, y ​​el uso de procedimientos separados para estas cosas no resuelve ninguno de los problemas inherentes a la elección entre una actualización y una inserción de todos modos ...
Aaron Bertrand

-10

Haga una selección, si obtiene un resultado, actualícelo, si no, créelo.


3
Eso son dos llamadas a la base de datos.
Chris Cudmore

3
No veo un problema con eso.
Clint Ecker

10
El problema es dos llamadas a la base de datos, terminas duplicando el número de viajes de ida y vuelta a la base de datos. Si la aplicación llega a la base de datos con muchas inserciones / actualizaciones, afectará el rendimiento. UPSERT es una mejor estrategia.
Kev

55
También crea una condición de carrera no?
niico
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.