Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Atomarer UPSERT in SQL Server 2005

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
  • es gibt eine Racebedingung im ersten INSERT. Der Schlüssel existiert möglicherweise nicht während der inneren Abfrage SELECT, aber er existiert zum Zeitpunkt INSERT, was zu einer Schlüsselverletzung führt.
  • es gibt eine Racebedingung zwischen INSERT und UPDATE. Der Schlüssel kann vorhanden sein, wenn er in der inneren Abfrage von INSERT geprüft wird, ist aber verschwunden, wenn UPDATE ausgeführt wird.

Für die zweite Racebedingung könnte man argumentieren, dass der Schlüssel sowieso durch den gleichzeitigen Thread gelöscht worden wäre, also ist es nicht wirklich ein verlorenes Update.

Die optimale Lösung besteht normalerweise darin, den wahrscheinlichsten Fall zu versuchen und den Fehler zu behandeln, wenn er fehlschlägt (natürlich innerhalb einer Transaktion):

  • Falls der Schlüssel wahrscheinlich fehlt, immer zuerst einstecken. Behandle die Verletzung der eindeutigen Einschränkung, fallback to update.
  • Wenn der Schlüssel wahrscheinlich vorhanden ist, immer zuerst aktualisieren. Einfügen, wenn keine Zeile gefunden wurde. Mögliche Verletzung von Unique Constraints behandeln, auf Update zurückgreifen.

Neben der Korrektheit ist dieses Muster auch optimal für die Geschwindigkeit:Es ist effizienter zu versuchen, die Ausnahme einzufügen und zu behandeln, als falsche Lockups durchzuführen. Lockups bedeuten logische Seitenlesevorgänge (was physische Seitenlesevorgänge bedeuten kann), und IO (sogar logisch) ist teurer als SEH.

Aktualisieren @Peter

Warum ist keine einzelne Aussage „atomar“? Nehmen wir an, wir haben eine triviale Tabelle:

create table Test (id int primary key);

Wenn ich nun diese einzelne Anweisung von zwei Threads in einer Schleife ausführen würde, wäre sie "atomar", wie Sie sagen, es kann eine No-Race-Bedingung geben:

  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

Doch in nur wenigen Sekunden tritt eine Verletzung des Primärschlüssels auf:

Msg 2627, Level 14, State 1, Line 4
Verletzung der PRIMARY KEY-Einschränkung 'PK__Test__24927208'. Doppelter Schlüssel kann nicht in Objekt „dbo.Test“ eingefügt werden.

Warum ist das so? Sie haben recht damit, dass der SQL-Abfrageplan bei DELETE ... FROM ... JOIN das "Richtige" tut , auf WITH cte AS (SELECT...FROM ) DELETE FROM cte und in vielen anderen Fällen. In diesen Fällen gibt es jedoch einen entscheidenden Unterschied:Die „Unterabfrage“ bezieht sich auf das Ziel eines Updates oder löschen Betrieb. Für solche Fälle verwendet der Abfrageplan tatsächlich eine geeignete Sperre, tatsächlich ist dieses Verhalten in bestimmten Fällen kritisch, wie zum Beispiel bei der Implementierung von Warteschlangen, die Tabellen als Warteschlangen verwenden.

Aber sowohl in der ursprünglichen Frage als auch in meinem Beispiel wird die Unterabfrage vom Abfrageoptimierer nur als Unterabfrage in einer Abfrage angesehen, nicht als eine spezielle Abfrage vom Typ "Nach Aktualisierung suchen", die einen besonderen Sperrschutz benötigt. Das Ergebnis ist, dass die Ausführung der Unterabfrage-Suche von einem gleichzeitigen Beobachter als eigenständige Operation beobachtet werden kann , wodurch das 'atomare' Verhalten der Anweisung gebrochen wird. Wenn keine besonderen Vorsichtsmaßnahmen getroffen werden, können mehrere Threads versuchen, denselben Wert einzufügen, da beide davon überzeugt sind, dass sie es überprüft haben und der Wert noch nicht existiert. Nur einer kann erfolgreich sein, der andere trifft die PK-Verletzung. QED.