Da Sie eine Sequenz verwenden, können Sie dieselbe NEXT VALUE FOR- Funktion verwenden, die Sie bereits in einer Standardeinschränkung für das Id
Primärschlüsselfeld verwendet haben, um im Voraus einen neuen Id
Wert zu generieren . Das erste Generieren des Werts bedeutet, dass Sie sich keine Sorgen machen müssen SCOPE_IDENTITY
, was bedeutet, dass Sie weder die OUTPUT
Klausel noch ein zusätzliches SELECT
Element benötigen , um den neuen Wert zu erhalten. Sie werden den Wert haben, bevor Sie das tun INSERT
, und Sie müssen sich nicht einmal damit anlegen SET IDENTITY INSERT ON / OFF
:-)
Das kümmert sich also um einen Teil der Gesamtsituation. Der andere Teil behandelt das Problem der gleichzeitigen Ausführung von zwei Prozessen genau zur gleichen Zeit, findet keine vorhandene Zeile für genau die gleiche Zeichenfolge und fährt mit fort INSERT
. Es geht darum, die Verletzung der eindeutigen Einschränkung zu vermeiden, die auftreten würde.
Eine Möglichkeit, diese Art von Parallelitätsproblemen zu behandeln, besteht darin, diesen bestimmten Vorgang als Single-Thread-Vorgang zu erzwingen. Der Weg, dies zu tun, ist die Verwendung von Anwendungssperren (die über Sitzungen hinweg funktionieren). Während sie effektiv sind, können sie für eine Situation wie diese, in der die Häufigkeit von Kollisionen wahrscheinlich ziemlich gering ist, etwas unbeholfen sein.
Die andere Möglichkeit, mit den Kollisionen umzugehen, besteht darin, zu akzeptieren, dass sie manchmal auftreten, und mit ihnen umzugehen, anstatt zu versuchen, sie zu vermeiden. Mit dem TRY...CATCH
Konstrukt können Sie einen bestimmten Fehler (in diesem Fall: "Unique Constraint Violation", Nachricht 2601) effektiv abfangen und erneut ausführen SELECT
, um den Id
Wert abzurufen, da wir wissen, dass er jetzt existiert, weil er sich CATCH
mit diesem bestimmten im Block befindet Error. Andere Fehler können im typischen behandelt werden RAISERROR
/ RETURN
oder THROW
Art und Weise.
Testkonfiguration: Sequenz, Tabelle und eindeutiger Index
USE [tempdb];
CREATE SEQUENCE dbo.MagicNumber
AS INT
START WITH 1
INCREMENT BY 1;
CREATE TABLE dbo.NameLookup
(
[Id] INT NOT NULL
CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
[ItemName] NVARCHAR(50) NOT NULL
);
CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
ON dbo.NameLookup ([ItemName]);
GO
Testkonfiguration: Gespeicherte Prozedur
CREATE PROCEDURE dbo.GetOrInsertName
(
@SomeName NVARCHAR(50),
@ID INT OUTPUT,
@TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;
BEGIN TRY
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName
AND @TestRaceCondition = 0;
IF (@ID IS NULL)
BEGIN
SET @ID = NEXT VALUE FOR dbo.MagicNumber;
INSERT INTO dbo.NameLookup ([Id], [ItemName])
VALUES (@ID, @SomeName);
END;
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
BEGIN
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName;
END;
ELSE
BEGIN
;THROW; -- SQL Server 2012 or newer
/*
DECLARE @ErrorNumber INT = ERROR_NUMBER(),
@ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
RETURN;
*/
END;
END CATCH;
GO
Der Test
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT,
@TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO
Frage von OP
Warum ist das besser als das MERGE
? Bekomme ich nicht die gleiche Funktionalität ohne die TRY
Verwendung der WHERE NOT EXISTS
Klausel?
MERGE
hat verschiedene "Probleme" (mehrere Referenzen sind in der Antwort von @ SqlZim verlinkt, so dass Sie diese Informationen hier nicht duplizieren müssen). Außerdem gibt es bei diesem Ansatz keine zusätzliche Sperre (weniger Konflikte), sodass die Parallelität verbessert werden sollte. Bei diesem Ansatz wird es nie zu einer Unique Constraint-Verletzung kommen, ganz ohne HOLDLOCK
, usw. Es ist so gut wie garantiert, dass es funktioniert.
Die Gründe für diesen Ansatz sind:
- Wenn Sie über genügend Ausführungen dieses Verfahrens verfügen, sodass Sie sich über Kollisionen Gedanken machen müssen, möchten Sie nicht:
- Machen Sie mehr Schritte als nötig
- Halten Sie alle Ressourcen länger als nötig gesperrt
- Da Kollisionen nur bei neuen Einträgen auftreten können (neue Einträge werden genau zur gleichen Zeit eingereicht ), ist die Häufigkeit, mit der man überhaupt in den
CATCH
Block fällt , ziemlich gering. Es ist sinnvoller, den Code zu optimieren, der 99% der Zeit ausgeführt wird, als den Code, der 1% der Zeit ausgeführt wird (es sei denn, es sind keine Kosten für die Optimierung von beiden erforderlich, dies ist hier jedoch nicht der Fall).
Kommentar von @ SqlZims Antwort (Hervorhebung hinzugefügt)
Ich persönlich bevorzuge, um zu versuchen und Schneider eine Lösung zu tun zu vermeiden , dass , wenn möglich . In diesem Fall bin ich nicht der Meinung, dass die Verwendung der Sperren von serializable
ein umständlicher Ansatz ist, und ich wäre zuversichtlich, dass sie mit hoher Parallelität gut umgehen können.
Ich würde diesem ersten Satz zustimmen, wenn er geändert würde, um "und wenn besonnen" zu sagen. Nur weil etwas technisch möglich ist, heißt das nicht, dass die Situation (dh der beabsichtigte Anwendungsfall) davon profitiert.
Das Problem, das ich bei diesem Ansatz sehe, ist, dass es mehr sperrt als vorgeschlagen wird. Es ist wichtig, die zitierte Dokumentation zu "serializable" erneut zu lesen, insbesondere die folgenden (Hervorhebung hinzugefügt):
- Andere Transaktionen können keine neuen Zeilen mit Schlüsselwerten einfügen, die in den Bereich der von Anweisungen in der aktuellen Transaktion gelesenen Schlüssel fallen würden , bis die aktuelle Transaktion abgeschlossen ist.
Hier ist der Kommentar im Beispielcode:
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
Das operative Wort dort ist "Reichweite". Die Sperre wird nicht nur auf den Wert in @vName
, sondern genauer gesagt auf einen Bereich, der bei beginntDie Position, an der dieser neue Wert abgelegt werden soll (dh zwischen den vorhandenen Schlüsselwerten auf beiden Seiten, auf die der neue Wert passt), nicht jedoch der Wert selbst. Das heißt, andere Prozesse können keine neuen Werte einfügen, je nachdem, welche Werte gerade gesucht werden. Wenn die Suche am oberen Ende des Bereichs ausgeführt wird, wird das Einfügen von Elementen, die dieselbe Position einnehmen könnten, blockiert. Wenn beispielsweise die Werte "a", "b" und "d" vorhanden sind und ein Prozess "f" auswählt, ist es nicht möglich, die Werte "g" oder sogar "e" einzufügen ( da einer von denen wird sofort nach "d" kommen). Das Einfügen eines Werts von "c" ist jedoch möglich, da dieser Wert nicht in den Bereich "reserviert" gestellt wird.
Das folgende Beispiel soll dieses Verhalten veranschaulichen:
(In der Abfrage-Registerkarte (dh Sitzung) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');
BEGIN TRAN;
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'test8';
--ROLLBACK;
(In der Abfrage-Registerkarte (dh Sitzung) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Wenn der Wert "C" vorhanden ist und der Wert "A" ausgewählt (und daher gesperrt) ist, können Sie den Wert "D", jedoch nicht den Wert "B" eingeben:
(In der Abfrage-Registerkarte (dh Sitzung) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');
BEGIN TRAN
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'testA';
--ROLLBACK;
(In der Abfrage-Registerkarte (dh Sitzung) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Um ehrlich zu sein, gibt es in meinem vorgeschlagenen Ansatz, wenn es eine Ausnahme gibt, 4 Einträge im Transaktionsprotokoll, die bei diesem "serialisierbaren Transaktionsansatz" nicht vorkommen. ABER, wie ich oben sagte, wenn die Ausnahme 1% (oder sogar 5%) der Zeit auftritt, ist dies weitaus weniger belastend als der weitaus wahrscheinlichere Fall, dass das anfängliche SELECT INSERT-Operationen vorübergehend blockiert.
Ein weiteres, wenn auch geringfügiges Problem bei diesem Ansatz mit "serialisierbarer Transaktion + OUTPUT-Klausel" ist, dass die OUTPUT
Klausel (in ihrer derzeitigen Verwendung) die Daten als Ergebnismenge zurücksendet. Eine Ergebnismenge erfordert mehr Aufwand (wahrscheinlich auf beiden Seiten: in SQL Server zum Verwalten des internen Cursors und in der App-Ebene zum Verwalten des DataReader-Objekts) als ein einfacher OUTPUT
Parameter. Da es sich nur um einen einzelnen Skalarwert handelt und davon ausgegangen wird, dass es sich um eine hohe Ausführungshäufigkeit handelt, summiert sich der zusätzliche Aufwand für die Ergebnismenge wahrscheinlich auf.
Während die OUTPUT
Klausel so verwendet werden könnte OUTPUT
, dass ein Parameter zurückgegeben wird, wären zusätzliche Schritte erforderlich, um eine temporäre Tabelle oder Tabellenvariable zu erstellen und anschließend den Wert aus dieser temporären Tabelle / Tabellenvariablen in den OUTPUT
Parameter auszuwählen .
Weitere Klarstellung: Antwort auf @ SqlZims Antwort (aktualisierte Antwort) auf meine Antwort auf @ SqlZims Antwort (in der ursprünglichen Antwort) auf meine Aussage zu Nebenläufigkeit und Leistung ;-)
Tut mir leid, wenn dieser Teil ein bisschen lang ist, aber an diesem Punkt sind wir nur auf die Nuancen der beiden Ansätze beschränkt.
Ich glaube, dass die Art und Weise, wie die Informationen dargestellt werden, zu falschen Annahmen über den Umfang der Sperren führen kann, die bei der Verwendung serializable
in dem in der ursprünglichen Frage dargestellten Szenario zu erwarten sind .
Ja, ich gebe zu, dass ich voreingenommen bin, wenn auch um fair zu sein:
- Es ist für einen Menschen unmöglich, zumindest in geringem Maße voreingenommen zu sein, und ich versuche, es auf ein Minimum zu beschränken.
- Das angegebene Beispiel war simpel, diente jedoch zur Veranschaulichung, um das Verhalten zu vermitteln, ohne es zu komplizieren. Es war nicht beabsichtigt, eine übermäßige Häufigkeit zu implizieren, obwohl ich verstehe, dass ich auch nicht ausdrücklich etwas anderes angegeben habe, und es könnte so gelesen werden, dass es ein größeres Problem impliziert, als es tatsächlich existiert. Ich werde versuchen, das weiter unten zu klären.
- Ich habe auch ein Beispiel für das Sperren eines Bereichs zwischen zwei vorhandenen Schlüsseln aufgenommen (der zweite Satz der Blöcke "Abfrage-Tab 1" und "Abfrage-Tab 2").
- Ich habe die "versteckten Kosten" meines Ansatzes gefunden (und mich freiwillig gemeldet), nämlich die vier zusätzlichen Tran-Log-Einträge jedes Mal, wenn der
INSERT
Versuch aufgrund einer Unique Constraint-Verletzung fehlschlägt. Das habe ich in keiner der anderen Antworten / Posts gesehen.
In Bezug auf @ gbns "JFDI" -Ansatz, Michael J. Swarts "Ugly Pragmatism For The Win" -Post und Aaron Bertrands Kommentar zu Michaels Post (in Bezug auf seine Tests, die zeigen, welche Szenarien die Leistung verringert haben) sowie Ihren Kommentar zu Ihrer "Anpassung von Michael J Stewarts Adaption der JFDI-Prozedur "Try Catch" von @ gbn mit folgenden Worten:
Wenn Sie öfter neue Werte einfügen als vorhandene Werte auswählen, ist dies möglicherweise performanter als die Version von @ srutzky. Ansonsten würde ich @ srutzkys Version dieser vorziehen.
In Bezug auf diese Diskussion zwischen gbn / Michael / Aaron in Bezug auf den "JFDI" -Ansatz wäre es falsch, meinen Vorschlag mit dem "JFDI" -Ansatz von gbn gleichzusetzen. Aufgrund der Art des Vorgangs "Abrufen oder Einfügen" ist es ausdrücklich erforderlich SELECT
, den ID
Wert für vorhandene Datensätze abzurufen. Dieses SELECT dient als IF EXISTS
Überprüfung, wodurch dieser Ansatz eher der "CheckTryCatch" -Variation von Aarons Tests entspricht. Michaels umgeschriebener Code (und Ihre letzte Adaption von Michaels Adaption) beinhaltet auch WHERE NOT EXISTS
, dass Sie diese Prüfung zuerst durchführen müssen. Daher trifft mein Vorschlag (zusammen mit Michaels endgültigem Code und Ihrer Anpassung seines endgültigen Codes) den CATCH
Block nicht allzu oft. Es könnten nur Situationen sein, in denen zwei Sitzungen,ItemName
INSERT...SELECT
genau im selben Moment, so dass beide Sitzungen ein "true" für WHERE NOT EXISTS
genau im selben Moment erhalten und somit beide versuchen, INSERT
das genau im selben Moment zu tun . Dieses sehr spezielle Szenario ist weitaus seltener als das Auswählen eines vorhandenen Szenarios ItemName
oder das Einfügen eines neuen Szenarios, ItemName
wenn kein anderer Prozess dies im selben Moment versucht .
IM HINBLICK AUF DAS OBENE: Warum bevorzuge ich meinen Ansatz?
Schauen wir uns zunächst an, welche Sperren beim "serialisierbaren" Ansatz stattfinden. Wie oben erwähnt, hängt der "Bereich", der gesperrt wird, von den vorhandenen Schlüsselwerten zu beiden Seiten ab, zu denen der neue Schlüsselwert passen würde. Der Anfang oder das Ende des Bereichs kann auch der Anfang oder das Ende des Index sein, wenn in dieser Richtung kein Schlüsselwert vorhanden ist. Angenommen, wir haben den folgenden Index und die folgenden Schlüssel ( ^
stellt den Anfang des Index dar, während $
er das Ende darstellt):
Range #: |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value: ^ C F J $
Wenn Sitzung 55 versucht, einen Schlüsselwert einzufügen von:
A
, dann ist der Bereich # 1 (von ^
bis C
) gesperrt: Sitzung 56 kann keinen Wert von einfügen B
, selbst wenn er eindeutig und gültig ist (noch). Aber Sitzung 56 kann Werte einfügen D
, G
und M
.
D
, dann ist der Bereich 2 (von C
bis F
) gesperrt: Sitzung 56 kann E
(noch) keinen Wert von einfügen . Aber Sitzung 56 kann Werte einfügen A
, G
und M
.
M
, dann ist der Bereich 4 (von J
bis $
) gesperrt: Sitzung 56 kann X
(noch) keinen Wert von einfügen . Aber Sitzung 56 kann Werte einfügen A
, D
und G
.
Wenn mehr Schlüsselwerte hinzugefügt werden, werden die Bereiche zwischen den Schlüsselwerten enger, wodurch die Wahrscheinlichkeit / Häufigkeit verringert wird, dass mehrere Werte gleichzeitig in den gleichen Bereich eingefügt werden. Zugegebenermaßen ist dies kein großes Problem, und glücklicherweise scheint es sich um ein Problem zu handeln, das im Laufe der Zeit tatsächlich abnimmt.
Das Problem mit meinem Ansatz wurde oben beschrieben: Es tritt nur auf, wenn zwei Sitzungen gleichzeitig versuchen, denselben Schlüsselwert einzufügen . In dieser Hinsicht kommt es darauf an, wie hoch die Wahrscheinlichkeit ist, dass etwas passiert: Es werden zwei verschiedene, aber nahe beieinander liegende Schlüsselwerte gleichzeitig versucht, oder es wird derselbe Schlüsselwert gleichzeitig versucht? Ich nehme an, die Antwort liegt in der Struktur der App, die die Einfügungen ausführt, aber im Allgemeinen würde ich annehmen, dass es wahrscheinlicher ist, dass zwei verschiedene Werte eingefügt werden, die zufällig denselben Bereich teilen. Die einzige Möglichkeit, dies wirklich zu wissen, besteht darin, beide auf dem OP-System zu testen.
Als nächstes betrachten wir zwei Szenarien und wie jeder Ansatz damit umgeht:
Alle Anfragen beziehen sich auf eindeutige Schlüsselwerte:
In diesem Fall wird der CATCH
Block in meinem Vorschlag nie eingegeben, daher kein "Problem" (dh 4 Transprotokolleinträge und die dafür erforderliche Zeit). Bei der "serialisierbaren" Methode besteht jedoch immer die Möglichkeit, andere Einfügungen im gleichen Bereich zu blockieren (wenn auch nicht für sehr lange Zeit), auch wenn alle Einfügungen eindeutig sind.
Hohe Häufigkeit von Anfragen für denselben Schlüsselwert zur selben Zeit:
In diesem Fall - eine sehr geringe Eindeutigkeit in Bezug auf eingehende Anfragen nach nicht vorhandenen Schlüsselwerten - wird der CATCH
Block in meinem Vorschlag regelmäßig eingetragen. Dies hat zur Folge, dass für jede fehlgeschlagene Einfügung ein automatischer Rollback durchgeführt und die 4 Einträge in das Transaktionsprotokoll geschrieben werden müssen, was jedes Mal einen leichten Leistungseinbruch darstellt. Die Gesamtoperation sollte jedoch niemals fehlschlagen (zumindest nicht deshalb).
(Es gab ein Problem mit der vorherigen Version des "aktualisierten" Ansatzes, bei dem Deadlocks aufgetreten sind. Es updlock
wurde ein Hinweis hinzugefügt, der dieses Problem behebt und keine Deadlocks mehr verursacht.)ABER in der "serialisierbaren" Methode (sogar in der aktualisierten, optimierten Version) blockiert der Vorgang. Warum? Weil das serializable
Verhalten nur INSERT
Operationen in dem Bereich verhindert, der gelesen und daher gesperrt wurde; SELECT
Operationen in diesem Bereich werden dadurch nicht verhindert .
In serializable
diesem Fall scheint der Ansatz keinen zusätzlichen Overhead zu haben und könnte eine geringfügig bessere Leistung erbringen als von mir vorgeschlagen.
Wie bei vielen / den meisten Diskussionen zur Leistung ist der einzige Weg, um wirklich ein Gefühl dafür zu bekommen, wie sich etwas auswirkt, es in der Zielumgebung auszuprobieren, in der es ausgeführt wird, da es so viele Faktoren gibt, die sich auf das Ergebnis auswirken können. An diesem Punkt wird es keine Ansichtssache sein :).