Nehmen Sie eine Tabellenstruktur von MyTable(KEY, datafield1, datafield2...)
an.
Oft möchte ich entweder einen vorhandenen Datensatz aktualisieren oder einen neuen Datensatz einfügen, wenn er nicht vorhanden ist.
Im Wesentlichen:
IF (key exists)
run update command
ELSE
run insert command
Was ist der beste Weg, dies zu schreiben?
vergessen Sie nicht über Transaktionen. Die Leistung ist gut, aber ein einfacher Ansatz (WENN EXISTS ..) ist sehr gefährlich.
Wenn mehrere Threads versuchen, das Einfügen oder Aktualisieren durchzuführen, kann dies leicht zu einer Verletzung des Primärschlüssels führen.
Die von @Beau Crawford & @Esteban angebotenen Lösungen zeigen eine allgemeine Idee, sind jedoch fehleranfällig.
Um Deadlocks und PK-Verstöße zu vermeiden, können Sie Folgendes verwenden:
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
oder
begin tran
update table with (serializable) set ...
where key = @key
if @@rowcount = 0
begin
insert into table (key, ...) values (@key,..)
end
commit tran
Siehe meine ausführliche Antwort auf eine sehr ähnliche vorherige Frage
@ Beau Crawfords ist ein guter Weg in SQL 2005 und darunter, aber wenn Sie rep gewähren, sollte es an den Ersten gehen, der SO it . Das einzige Problem ist, dass es für Einfügungen immer noch zwei IO Operationen gibt.
MS Sql2008 führt merge
aus dem SQL: 2003-Standard ein:
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, ... )
Jetzt ist es wirklich nur eine IO Operation, aber schrecklicher Code :-(
Machen Sie einen UPSERT:
UPDATE MyTable SET FieldA = @ FieldA WHERE Key = @ Key IF @@ ROWCOUNT = 0 INSERT IN MyTable (FieldA) VALUES (@FieldA)
Viele Leute werden vorschlagen, dass Sie MERGE
verwenden, aber ich warne Sie davor. Standardmäßig schützt es Sie nicht mehr als mehrere Anweisungen vor Nebenläufigkeits- und Wettlaufbedingungen, birgt jedoch andere Gefahren:
http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/
Selbst mit dieser "einfacheren" Syntax bevorzuge ich diesen Ansatz (Fehlerbehandlung aus Gründen der Kürze weggelassen):
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;
Viele Leute werden diesen Weg vorschlagen:
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;
Damit ist jedoch sichergestellt, dass Sie die Tabelle möglicherweise zweimal lesen müssen, um die zu aktualisierenden Zeilen zu finden. Im ersten Beispiel müssen Sie die Zeile (n) immer nur einmal suchen. (In beiden Fällen erfolgt eine Einfügung, wenn beim ersten Lesevorgang keine Zeilen gefunden wurden.)
Andere schlagen diesen Weg vor:
BEGIN TRY
INSERT ...
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627
UPDATE ...
END CATCH
Dies ist jedoch problematisch, wenn es viel teurer ist, SQL Server Ausnahmen abfangen zu lassen, die Sie an erster Stelle hätten verhindern können, außer in dem seltenen Szenario, in dem fast jede Einfügung fehlschlägt. Das beweise ich hier:
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)
Bearbeiten:
Leider muss ich auch zu meinem eigenen Nachteil zugeben, dass die Lösungen, die dies ohne Auswahl tun, besser zu sein scheinen, da sie die Aufgabe mit einem Schritt weniger erledigen.
Wenn Sie mehrere Datensätze gleichzeitig UPSERTEN möchten, können Sie die ANSI SQL: 2003-DML-Anweisung MERGE verwenden.
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 ...])
Check out Nachahmung der MERGE-Anweisung in SQL Server 2005 .
Obwohl es ziemlich spät ist, dies zu kommentieren, möchte ich ein vollständigeres Beispiel mit MERGE hinzufügen.
Solche Insert + Update-Anweisungen werden normalerweise als "Upsert" -Anweisungen bezeichnet und können mit MERGE in SQL Server implementiert werden.
Ein sehr gutes Beispiel finden Sie hier: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
In den obigen Abschnitten werden auch Sperr- und Parallelitätsszenarien erläutert.
Ich werde das gleiche als Referenz zitieren:
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
Ersetzen Sie Tabellen- und Feldnamen durch die gewünschten Namen. Achten Sie auf die mit ON Bedingung. Stellen Sie dann den entsprechenden Wert (und Typ) für die Variablen in der DECLARE-Zeile ein.
Prost.
Sie können die Anweisung MERGE
verwenden. Diese Anweisung wird verwendet, um Daten einzufügen, wenn sie nicht vorhanden sind, oder um Daten zu aktualisieren, wenn sie vorhanden sind.
MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
Wenn Sie die Route UPDATE if-no-rows-updated dann INSERT verwenden, sollten Sie zuerst INSERT ausführen, um eine Racebedingung zu verhindern (vorausgesetzt, es wird kein dazwischenliegendes DELETE ausgeführt).
INSERT INTO MyTable (Key, FieldA)
SELECT @Key, @FieldA
WHERE NOT EXISTS
(
SELECT *
FROM MyTable
WHERE Key = @Key
)
IF @@ROWCOUNT = 0
BEGIN
UPDATE MyTable
SET [email protected]
WHERE [email protected]
IF @@ROWCOUNT = 0
... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END
Abgesehen von der Vermeidung einer Racebedingung führt dies in den meisten Fällen zum Fehlschlagen des INSERT und zur Verschwendung von CPU, wenn der Datensatz bereits vorhanden ist.
Die Verwendung von MERGE ist wahrscheinlich ab SQL2008 vorzuziehen.
Das hängt vom Nutzungsmuster ab. Man muss das große Bild des Gebrauchs betrachten, ohne sich in den Details zu verlieren. Wenn das Verwendungsmuster nach dem Erstellen des Datensatzes beispielsweise zu 99% aktualisiert wird, ist UPSERT die beste Lösung.
Nach dem ersten Einfügen (Treffer) werden alle einzelnen Anweisungen aktualisiert, ohne Wenn und Aber. Die 'wo'-Bedingung für die Einfügung ist erforderlich, sonst werden Duplikate eingefügt, und Sie möchten sich nicht mit dem Sperren befassen.
UPDATE <tableName> SET <field>[email protected] WHERE [email protected];
IF @@ROWCOUNT = 0
BEGIN
INSERT INTO <tableName> (field)
SELECT @field
WHERE NOT EXISTS (select * from tableName where key = @key);
END
In SQL Server 2008 können Sie die MERGE-Anweisung verwenden
MS SQL Server 2008 führt die MERGE-Anweisung ein, die meines Erachtens Teil des SQL: 2003-Standards ist. Wie viele gezeigt haben, ist es keine große Sache, Fälle mit einer Zeile zu behandeln, aber wenn es sich um große Datensätze handelt, benötigt man einen Cursor mit allen damit verbundenen Leistungsproblemen. Die MERGE-Anweisung wird beim Umgang mit großen Datenmengen sehr begrüßt.
Bevor alle aus Angst vor diesen schändlichen Benutzern, die Ihre Sprocs direkt ausführen, zu HOLDLOCK-s springen :-) Lassen Sie mich darauf hinweisen, dass Sie die Einzigartigkeit neuer PK-s durch das Design garantieren müssen (Identitätsschlüssel, Sequenzgeneratoren in Oracle, eindeutige Indizes für externe IDs, Abfragen, die von Indizes abgedeckt werden). Das ist das A und O des Problems. Wenn Sie das nicht haben, werden Sie keine HOLDLOCK-s des Universums retten, und wenn Sie das haben, brauchen Sie bei der ersten Auswahl nichts weiter als UPDLOCK (oder verwenden Sie zuerst update).
Sprocs werden normalerweise unter sehr kontrollierten Bedingungen und unter der Annahme eines vertrauenswürdigen Anrufers (Mid Tier) ausgeführt. Das bedeutet, dass, wenn in einem einfachen Upsert-Muster (Aktualisieren + Einfügen oder Zusammenführen) jemals ein doppeltes PK auftritt, dies einen Fehler in Ihrem Design der mittleren Ebene oder Tabelle bedeutet und es gut ist, dass SQL in einem solchen Fall einen Fehler auslöst und den Datensatz ablehnt. Wenn Sie in diesem Fall ein HOLDLOCK setzen, bedeutet dies, dass Sie Ausnahmen essen und potenziell fehlerhafte Daten einlesen.
Allerdings ist die Verwendung von MERGE oder UPDATE dann INSERT auf Ihrem Server einfacher und weniger fehleranfällig, da Sie nicht daran denken müssen, (UPDLOCK) hinzuzufügen, um zuerst auszuwählen. Wenn Sie Einfügungen/Aktualisierungen in kleinen Stapeln vornehmen, müssen Sie Ihre Daten kennen, um zu entscheiden, ob eine Transaktion angemessen ist oder nicht. Es ist nur eine Sammlung von nicht verwandten Datensätzen, dann wird eine zusätzliche "umhüllende" Transaktion nachteilig sein.
Sind die Rennbedingungen wirklich wichtig, wenn Sie zuerst ein Update und dann eine Beilage versuchen? Nehmen wir an, Sie haben zwei Threads, die einen Wert für key key festlegen möchten:
Thread 1: Wert = 1
Thread 2: Wert = 2
Beispiel für ein Rennbedingungsszenario
Der andere Thread schlägt mit Einfügen (mit doppeltem Fehlerschlüssel) fehl - Thread 2.
Aber; In einer Multithread-Umgebung entscheidet der OS-Scheduler über die Reihenfolge der Thread-Ausführung. In dem obigen Szenario, in dem diese Race-Bedingung vorliegt, hat das Betriebssystem die Ausführungsreihenfolge festgelegt. Dh: Es ist falsch zu sagen, dass "Thread 1" oder "Thread 2" vom Standpunkt des Systems aus "zuerst" war.
Wenn die Ausführungszeit für Thread 1 und Thread 2 so kurz ist, spielt das Ergebnis der Race-Bedingung keine Rolle. Die einzige Anforderung sollte sein, dass einer der Threads den resultierenden Wert definiert.
Für die Implementierung: Wenn die Aktualisierung gefolgt von der Einfügung den Fehler "Schlüssel duplizieren" ergibt, sollte dies als Erfolg gewertet werden.
Außerdem sollte man natürlich niemals davon ausgehen, dass der Wert in der Datenbank mit dem zuletzt von Ihnen geschriebenen Wert übereinstimmt.
Ich hatte folgende Lösung ausprobiert und es funktioniert bei mir, wenn gleichzeitig eine Aufforderung zur Einfügung einer Anweisung auftritt.
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