Pregunta

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

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

Esencialmente:

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

¿Cuál es la forma más eficaz de escribir esto?

¿Fue útil?

Solución

No te olvides de las transacciones.El rendimiento es bueno, pero el enfoque simple (SI EXISTE...) es muy peligroso.
Cuando múltiples hilos intentarán realizar insertos o actualizaciones, puede obtener fácilmente la violación de la clave principal.

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

Para evitar interbloqueos y violaciones de PK, puedes 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

Otros consejos

Mira mi respuesta detallada a una pregunta anterior muy similar

@Beau Crawford's es una buena manera en SQL 2005 y versiones anteriores, aunque si otorga representación, debe ir a la primer chico en hacerlo.El único problema es que para las inserciones siguen siendo dos operaciones de E/S.

MS Sql2008 presenta merge del 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 en realidad es solo una operación IO, pero un código horrible :-(

Hacer una UPSERT:

UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key

IF @@ROWCOUNT = 0
   INSERT INTO MyTable (FieldA) VALUES (@FieldA)

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

Mucha gente le sugerirá que utilice MERGE, pero te advierto que no lo hagas.De forma predeterminada, no lo protege de la concurrencia y las condiciones de carrera más que las declaraciones múltiples, pero 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 (se omite el manejo de errores por motivos de 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á 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 esto logra es garantizar que es posible que deba leer la tabla dos veces para ubicar las filas que se actualizarán.En el primer ejemplo, solo necesitará ubicar las filas una vez.(En ambos casos, si no se encuentran filas en la lectura inicial, se produce una inserción).

Otros sugerirán esta manera:

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

Sin embargo, esto es problemático aunque solo sea por permitir que SQL Server detecte excepciones que usted podría haber evitado en primer lugar, es mucho más costoso, excepto en el raro escenario en el que casi todas las inserciones fallan.Lo pruebo aquí:

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

Editar:

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

Si desea UPSERT más de un registro a la vez, puede utilizar la instrucció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 ...])

Verificar Imitando la declaración MERGE en SQL Server 2005.

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

Estas declaraciones Insert+Update generalmente se denominan 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 también explica escenarios de bloqueo y concurrencia.

Citaré lo mismo como 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;
/*
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 tablas y campos por lo que necesite.Cuidar el usando ON condición.Luego establezca el valor (y tipo) apropiado para las variables en la línea DECLARAR.

Salud.

Puedes usar MERGE Declaración, esta declaración se utiliza para insertar datos si no existen o actualizar si existen.

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

Si va por la ruta ACTUALIZAR si no hay filas actualizadas y luego INSERTAR, considere hacer INSERTAR primero para evitar una condición de carrera (suponiendo que no intervenga ELIMINAR)

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 existe, esto provocará que INSERT falle, desperdiciando CPU.

Probablemente sea preferible usar MERGE para SQL2008 en adelante.

En SQL Server 2008 puede utilizar la declaración MERGE

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

Después de la primera inserción (acierto), serán todas las actualizaciones de declaraciones individuales, sin peros ni peros.La condición 'dónde' en la inserción es necesaria; de lo contrario, se insertarán duplicados y no querrás 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

MS SQL Server 2008 introduce la declaración MERGE, que creo que es parte del estándar SQL:2003.Como muchos han demostrado, no es gran cosa 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 conlleva.La declaración MERGE será una adición muy bienvenida cuando se trate de grandes conjuntos de datos.

Antes de que todos salten a HOLDLOCK-s por miedo a que estos usuarios malvados ejecuten sus sprocs directamente :-) permítanme señalar que debe garantizar la singularidad de las nuevas PK por diseño (claves de identidad, generadores de secuencias en Oracle, índices únicos para ID externos, consultas cubiertas por índices).Ese es el alfa y omega del asunto.Si no tienes eso, ningún HOLDLOCK-s del universo te salvará y si lo tienes, entonces no necesitas nada más allá de UPDLOCK en la primera selección (o usar la actualización primero).

Los sprocs normalmente se ejecutan en condiciones muy controladas y con la suposición de que hay una persona que llama de confianza (nivel medio).Lo que significa que si un patrón de inserción simple (actualizar+insertar o fusionar) alguna vez ve PK duplicado, eso significa un error en su diseño de tabla o de 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 consumir excepciones y recibir datos potencialmente defectuosos, además de reducir su rendimiento.

Dicho esto, usar FUSIONAR o ACTUALIZAR y luego INSERTAR es más fácil en su servidor y menos propenso a errores ya que no tiene que recordar agregar (UPDLOCK) para seleccionar primero.Además, si realiza inserciones/actualizaciones en lotes pequeños, necesita conocer sus datos para poder decidir si una transacción es apropiada o no.Si se trata sólo de una colección de registros no relacionados, las transacciones "envolventes" adicionales serán perjudiciales.

¿Realmente importan las condiciones de carrera si primero intentas una actualización seguida de una inserción?Digamos que tienes dos hilos que quieren establecer un valor para la clave llave:

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

Ejemplo de escenario de condición de carrera

  1. llave no está definido
  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.P.ej.hilo 1
  5. El otro hilo falla al insertar (con error de clave duplicada): hilo 2.

    • Resultado:El "primero" de los dos peldaños a insertar decide el valor.
    • Resultado buscado:El último de los 2 hilos para escribir datos (actualizar o insertar) debería 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 el "hilo 1" o el "hilo 2" fueron el "primero" desde el punto de vista del sistema.

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

Para la implementación:Si la actualización seguida de la inserción da como resultado el error "clave duplicada", esto debe considerarse como un éxito.

Además, por supuesto, nunca se debe asumir que el valor en la base de datos es el mismo que el valor que escribió por última vez.

Probé la siguiente solución y funciona para mí cuando se produce una solicitud simultánea de declaració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

Puede utilizar esta consulta.Trabaja en todas las ediciones de SQL Server.Es simple y claro.Pero necesitas usar 2 consultas.Puedes usarlo 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:Por favor explique las respuestas negativas.

Si usa ADO.NET, DataAdapter se encarga de esto.

Si quieres manejarlo tú mismo, esta es la manera:

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

Entonces tú:

  1. hacer la actualizacion
  2. Si la actualización falla porque ya existe un registro con la clave, realice la inserción.Si la actualización no falla, habrá terminado.

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

Haciendo un si existe...demás ...Implica realizar dos solicitudes como mínimo (una para verificar y otra para tomar medidas).El siguiente enfoque requiere sólo uno cuando el registro existe, 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')

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

FirstSP:
If Exists
   Call SecondSP (UpdateProc)
Else
   Call ThirdSP (InsertProc)

Ahora bien, no sigo mis propios consejos muy a menudo, así que tómalo con cautela.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top