web-dev-qa-db-de.com

Lösungen für INSERT OR UPDATE unter SQL Server

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?

547
Chris Cudmore

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
346
aku

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 :-(

368
Keith

Machen Sie einen UPSERT:

 UPDATE MyTable SET FieldA = @ FieldA WHERE Key = @ Key 
 
 IF @@ ROWCOUNT = 0 
 INSERT IN MyTable (FieldA) VALUES (@FieldA) 

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

159
Beau Crawford

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:

82
Aaron Bertrand
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.

54
Esteban Araya

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 .

36
Eric Weilnau

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;
10
user243131
/*
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.

8
Denver

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`
7
Daniel Acosta

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.

4
Kristen

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
3
Saleh Najar

In SQL Server 2008 können Sie die MERGE-Anweisung verwenden

3
Bart

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.

2
bjorsig

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.

1
ZXX

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

  1. Schlüssel ist nicht definiert
  2. Thread 1 schlägt mit Update fehl
  3. Thread 2 schlägt mit Update fehl
  4. Genau einer der Threads 1 oder 2 ist beim Einfügen erfolgreich. Z.B. Faden 1
  5. Der andere Thread schlägt mit Einfügen (mit doppeltem Fehlerschlüssel) fehl - Thread 2.

    • Ergebnis: Das "erste" der beiden einzufügenden Schritte entscheidet über den Wert.
    • Gewünschtes Ergebnis: Der letzte der 2 Threads, die Daten schreiben (aktualisieren oder einfügen), sollte den Wert bestimmen

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.

1
runec

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
0
Dev