Soluciones para INSERTAR O ACTUALIZAR en SQL Server
-
01-07-2019 - |
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?
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)
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 ...])
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
- llave no está definido
- El hilo 1 falla con la actualización
- El hilo 2 falla con la actualización
- Exactamente uno de los hilos 1 o 2 tiene éxito con la inserción.P.ej.hilo 1
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ú:
- hacer la actualizacion
- 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.