Soluzioni per INSERT O AGGIORNAMENTO su SQL Server
-
01-07-2019 - |
Domanda
Supponi una struttura di tabella di MyTable (KEY, datafield1, datafield2 ...)
.
Spesso voglio aggiornare un record esistente o inserire un nuovo record se non esiste.
In sostanza:
IF (key exists)
run update command
ELSE
run insert command
Qual è il modo migliore per scrivere questo?
Soluzione
non dimenticare le transazioni. Le prestazioni sono buone, ma l'approccio semplice (SE ESISTE ..) è molto pericoloso.
Quando più thread tenteranno di eseguire l'inserimento o l'aggiornamento, puoi farlo facilmente
ottenere la violazione della chiave primaria.
Soluzioni fornite da @Beau Crawford & amp; @Esteban mostra un'idea generale ma soggetta a errori.
Per evitare deadlock e violazioni di PK puoi usare qualcosa del genere:
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
Altri suggerimenti
Vedi la mia risposta dettagliata a una domanda precedente molto simile
@Beau Crawford's è un buon metodo in SQL 2005 e in basso, anche se se stai concedendo la reputazione dovrebbe andare al primo ragazzo a SO . L'unico problema è che per gli inserti sono ancora due operazioni di I / O.
MS Sql2008 introduce merge
dallo standard 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, ... )
Ora è davvero solo un'operazione di I / O, ma un codice orribile :-(
Esegui un UPSERT:
UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key IF @@ROWCOUNT = 0 INSERT INTO MyTable (FieldA) VALUES (@FieldA)
Molte persone ti suggeriranno di usare MERGE
, ma ti avverto di non farlo. Per impostazione predefinita, non ti protegge dalla concorrenza e dalle condizioni di gara più di più dichiarazioni, ma introduce altri pericoli:
http://www.mssqltips.com/sqlservertip/ 3074 / uso-cautela-con-SQL-server-merge-statement /
Anche con questo "più semplice" sintassi disponibile, preferisco ancora questo approccio (gestione degli errori omessa per brevità):
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;
Molte persone suggeriranno in questo modo:
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;
Ma tutto ciò è garantire che potrebbe essere necessario leggere due volte la tabella per individuare le righe da aggiornare. Nel primo esempio, dovrai sempre individuare le righe una sola volta. (In entrambi i casi, se non viene trovata alcuna riga dalla lettura iniziale, si verifica un inserimento.)
Altri suggeriranno in questo modo:
BEGIN TRY
INSERT ...
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627
UPDATE ...
END CATCH
Tuttavia, ciò è problematico se non altro per il fatto che lasciare che SQL Server rilevi le eccezioni che si sarebbe potuto prevenire in primo luogo è molto più costoso, tranne nel raro scenario in cui quasi tutti gli inserimenti falliscono. Dimostro altrettanto qui:
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)
Modifica:
Purtroppo, anche a mio svantaggio, devo ammettere che le soluzioni che lo fanno senza una scelta sembrano essere migliori dal momento che svolgono il compito con un passo in meno.
Se si desidera UPSERT più di un record alla volta, è possibile utilizzare l'istruzione DML ANSI SQL: 2003 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 ...])
Scopri Imitazione dell'istruzione MERGE in SQL Server 2005 .
Anche se è abbastanza tardi per commentare questo, voglio aggiungere un esempio più completo usando MERGE.
Tali istruzioni Insert + Update sono generalmente chiamate " Upsert " e possono essere implementate usando MERGE in SQL Server.
Un ottimo esempio è dato qui: http: //weblogs.sqlteam .com / dang / archive / 2009/01/31 / UPSERT-Race-Condizione-Con-MERGE.aspx
Quanto sopra spiega anche gli scenari di blocco e concorrenza.
Citerò lo stesso per riferimento:
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
Sostituisci i nomi delle tabelle e dei campi con qualsiasi cosa tu abbia bisogno. Prenditi cura della condizione utilizzando ON . Quindi impostare il valore appropriato (e digitare) per le variabili nella riga DECLARE.
Saluti.
Puoi utilizzare l'istruzione MERGE
, questa istruzione viene utilizzata per inserire dati se non esistono o aggiornare se esistono.
MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
Se vai al UPDATE se-no-righe-aggiornato, allora INSERT route, considera di fare prima INSERT per prevenire una race condition (supponendo che non intervenga DELETE)
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
Oltre a evitare una condizione di competizione, se nella maggior parte dei casi il record esisterà già, ciò causerà il fallimento di INSERT, sprecando CPU.
L'uso di MERGE è probabilmente preferibile per SQL2008 in poi.
In SQL Server 2008 è possibile utilizzare l'istruzione MERGE
Dipende dal modello di utilizzo. Bisogna guardare il quadro generale sull'uso senza perdersi nei dettagli. Ad esempio, se il modello di utilizzo prevede aggiornamenti del 99% dopo la creazione del record, "UPSERT" è la soluzione migliore.
Dopo il primo inserimento (hit), saranno tutti gli aggiornamenti a singola istruzione, senza se e senza ma. La condizione "dove" sull'inserto è necessaria, altrimenti inserirà i duplicati e non si desidera gestire il blocco.
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 l'istruzione MERGE, che credo faccia parte dello standard SQL: 2003. Come molti hanno dimostrato, non è un grosso problema gestire casi di una riga, ma quando si tratta di set di dati di grandi dimensioni, è necessario un cursore, con tutti i problemi di prestazioni che si presentano. La dichiarazione MERGE sarà molto apprezzata quando si tratta di set di dati di grandi dimensioni.
Prima che tutti saltino a HOLDLOCK per paura di questi utenti disonesti che gestiscono direttamente i tuoi sprocs :-) fammi sottolineare che devi garantire l'unicità dei nuovi PK in base alla progettazione (identità chiavi, generatori di sequenze in Oracle, indici univoci per ID esterni, query coperte da indici). Questo è l'alfa e l'omega del problema. Se non lo possiedi, nessun HOLDLOCK dell'universo ti salverà e se lo possiedi, non avrai bisogno di nulla oltre UPDLOCK alla prima selezione (o di utilizzare prima l'aggiornamento).
Gli Sprocs normalmente funzionano in condizioni molto controllate e con l'ipotesi di un chiamante di fiducia (livello intermedio). Ciò significa che se un semplice schema di upsert (aggiornamento + inserimento o unione) vede mai un PK duplicato significa un bug nella progettazione di livello intermedio o tabella ed è positivo che SQL urlerà un errore in questo caso e rifiuterà il record. Inserire un HOLDLOCK in questo caso equivale a mangiare eccezioni e a prendere dati potenzialmente difettosi, oltre a ridurre la tua perf.
Detto questo, usando MERGE o UPDATE, INSERT è più facile sul tuo server e meno soggetto a errori poiché non devi ricordare di aggiungere (UPDLOCK) per selezionare prima. Inoltre, se si stanno effettuando inserimenti / aggiornamenti in piccoli lotti, è necessario conoscere i propri dati per decidere se una transazione è appropriata o meno. È solo una raccolta di record non correlati, quindi ulteriori "avvolgenti" la transazione sarà dannosa.
Le condizioni di gara contano davvero se provi prima un aggiornamento seguito da un inserto? Diciamo che hai due thread che vogliono impostare un valore per la chiave chiave :
Discussione 1: valore = 1
Discussione 2: valore = 2
Esempio di scenario delle condizioni di gara
- chiave non è definito
- Discussione 1 non riuscita con aggiornamento
- Discussione 2 non riuscita con aggiornamento
- Esattamente uno dei thread 1 o 2 ha esito positivo con l'inserimento. Per esempio. thread 1
-
L'altro thread ha esito negativo con insert (con chiave duplicata errore) - thread 2.
- Risultato: il "primo" quot dei due gradini da inserire, decide il valore.
- Risultato desiderato: l'ultimo dei 2 thread per scrivere i dati (aggiornare o inserire) dovrebbe decidere il valore
Ma; in un ambiente multithread, lo scheduler del sistema operativo decide l'ordine di esecuzione del thread: nello scenario sopra, dove abbiamo questa condizione di competizione, è stato il sistema operativo a decidere la sequenza di esecuzione. Vale a dire: è sbagliato dire che "thread 1" oppure "filetto 2" era "primo" dal punto di vista del sistema.
Quando il tempo di esecuzione è così vicino per il thread 1 e il thread 2, il risultato della race condition non ha importanza. L'unico requisito dovrebbe essere che uno dei thread definisca il valore risultante.
Per l'implementazione: se l'aggiornamento seguito da insert risulta in errore "chiave duplicata", questo dovrebbe essere considerato un successo.
Inoltre, ovviamente, non si dovrebbe mai presumere che il valore nel database sia lo stesso del valore scritto per ultimo.
Avevo provato la soluzione di seguito e funziona per me, quando si verifica una richiesta simultanea per l'istruzione insert.
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
È possibile utilizzare questa query. Funziona in tutte le edizioni di SQL Server. È semplice e chiaro. Ma devi usare 2 query. Puoi usare se non puoi usare 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: Spiegare i negativi delle risposte
Se si utilizza ADO.NET, DataAdapter gestisce questo.
Se vuoi gestirlo da solo, questo è il modo:
Assicurati che ci sia un vincolo chiave principale nella colonna chiave.
Quindi tu:
- Esegui l'aggiornamento
- Se l'aggiornamento non riesce perché esiste già un record con la chiave, eseguire l'inserimento. Se l'aggiornamento non fallisce, hai finito.
Puoi anche farlo al contrario, ovvero esegui prima l'inserimento e esegui l'aggiornamento se l'inserimento non riesce. Normalmente il primo modo è migliore, perché gli aggiornamenti vengono eseguiti più spesso degli inserti.
Fare un if esiste ... altrimenti ... implica fare almeno due richieste (una per verificare, una per agire). L'approccio seguente richiede solo uno in cui esiste il record, due se è richiesto un inserimento:
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')
Di solito faccio quello che hanno detto molti altri manifesti riguardo a verificarne prima l'esistenza e poi fare qualunque sia il percorso corretto. Una cosa che dovresti ricordare quando fai questo è che il piano di esecuzione memorizzato nella cache da sql potrebbe non essere ottimale per un percorso o per l'altro. Credo che il modo migliore per farlo sia chiamare due diverse procedure memorizzate.
FirstSP: If Exists Call SecondSP (UpdateProc) Else Call ThirdSP (InsertProc)
Ora, non seguo i miei consigli molto spesso, quindi prenderli con un granello di sale.
Effettua una selezione, se ottieni un risultato, aggiornalo, in caso contrario, crealo.