Ist ein MERGE mit OUTPUT besser als ein bedingtes INSERT und SELECT?

12

Wir begegnen oft der Situation "Wenn nicht vorhanden, einfügen". Dan Guzmans Blog bietet eine exzellente Untersuchung, wie man diesen Prozess threadsicher macht.

Ich habe eine Basistabelle, die einfach eine Zeichenfolge zu einer Ganzzahl von a katalogisiert SEQUENCE. In einer gespeicherten Prozedur muss ich entweder den Integer-Schlüssel für den Wert abrufen, falls vorhanden, oder INSERTihn und dann den resultierenden Wert. Die dbo.NameLookup.ItemNameSpalte unterliegt einer Eindeutigkeitsbeschränkung, sodass die Datenintegrität nicht gefährdet ist, aber ich möchte nicht auf die Ausnahmen stoßen.

Es ist kein, IDENTITYalso kann ich nicht bekommen SCOPE_IDENTITYund der Wert könnte NULLin bestimmten Fällen sein.

In meiner Situation muss ich mich nur mit der INSERTSicherheit auf dem Tisch befassen, also versuche ich zu entscheiden, ob es besser ist, MERGEwie folgt vorzugehen:

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

Ich könnte dies ohne Verwendung MERGEvon nur einer Bedingung tun , INSERTgefolgt von einer SELECT Ich denke, dass dieser zweite Ansatz für den Leser klarer ist, aber ich bin nicht überzeugt, dass es eine "bessere" Praxis ist

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

Oder vielleicht gibt es einen anderen besseren Weg, den ich nicht in Betracht gezogen habe

Ich habe andere Fragen gesucht und referenziert. Diese: /programming/5288283/sql-server-insert-if-not-exists-best-practice ist die am besten geeignete, die ich finden konnte, scheint aber für meinen Anwendungsfall nicht sehr zutreffend zu sein. Andere Fragen zu dem IF NOT EXISTS() THENAnsatz, die ich nicht für akzeptabel halte.

Matthew
quelle
Haben Sie versucht, mit Tabellen zu experimentieren, die größer sind als Ihr Puffer? Ich habe Erfahrungen gemacht, bei denen die Merge-Leistung nachlässt, sobald die Tabelle eine bestimmte Größe erreicht.
pacreely

Antworten:

8

Da Sie eine Sequenz verwenden, können Sie dieselbe NEXT VALUE FOR- Funktion verwenden, die Sie bereits in einer Standardeinschränkung für das IdPrimärschlüsselfeld verwendet haben, um im Voraus einen neuen IdWert zu generieren . Das erste Generieren des Werts bedeutet, dass Sie sich keine Sorgen machen müssen SCOPE_IDENTITY, was bedeutet, dass Sie weder die OUTPUTKlausel noch ein zusätzliches SELECTElement 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...CATCHKonstrukt können Sie einen bestimmten Fehler (in diesem Fall: "Unique Constraint Violation", Nachricht 2601) effektiv abfangen und erneut ausführen SELECT, um den IdWert abzurufen, da wir wissen, dass er jetzt existiert, weil er sich CATCHmit diesem bestimmten im Block befindet Error. Andere Fehler können im typischen behandelt werden RAISERROR/ RETURNoder THROWArt 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 TRYVerwendung der WHERE NOT EXISTSKlausel?

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

  1. Wenn Sie über genügend Ausführungen dieses Verfahrens verfügen, sodass Sie sich über Kollisionen Gedanken machen müssen, möchten Sie nicht:
    1. Machen Sie mehr Schritte als nötig
    2. Halten Sie alle Ressourcen länger als nötig gesperrt
  2. 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 CATCHBlock 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 serializableein 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 OUTPUTKlausel (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 OUTPUTParameter. 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 OUTPUTKlausel 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 OUTPUTParameter 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 serializablein 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:

  1. 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.
  2. 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.
  3. 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").
  4. 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 INSERTVersuch 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 IDWert 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 CATCHBlock nicht allzu oft. Es könnten nur Situationen sein, in denen zwei Sitzungen,ItemNameINSERT...SELECTgenau im selben Moment, so dass beide Sitzungen ein "true" für WHERE NOT EXISTSgenau im selben Moment erhalten und somit beide versuchen, INSERTdas genau im selben Moment zu tun . Dieses sehr spezielle Szenario ist weitaus seltener als das Auswählen eines vorhandenen Szenarios ItemNameoder das Einfügen eines neuen Szenarios, ItemNamewenn 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, Gund M.
  • D, dann ist der Bereich 2 (von Cbis F) gesperrt: Sitzung 56 kann E(noch) keinen Wert von einfügen . Aber Sitzung 56 kann Werte einfügen A, Gund M.
  • M, dann ist der Bereich 4 (von Jbis $) gesperrt: Sitzung 56 kann X(noch) keinen Wert von einfügen . Aber Sitzung 56 kann Werte einfügen A, Dund 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:

  1. Alle Anfragen beziehen sich auf eindeutige Schlüsselwerte:

    In diesem Fall wird der CATCHBlock 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.

  2. 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 CATCHBlock 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 updlockwurde 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 serializableVerhalten nur INSERTOperationen in dem Bereich verhindert, der gelesen und daher gesperrt wurde; SELECTOperationen in diesem Bereich werden dadurch nicht verhindert .

    In serializablediesem 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 :).

Solomon Rutzky
quelle
7

Aktualisierte Antwort


Antwort an @srutzky

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.

Ich bin damit einverstanden und aus den gleichen Gründen verwende ich Ausgabeparameter, wenn ich umsichtig bin . Es war mein Fehler, bei meiner ersten Antwort keinen Ausgabeparameter zu verwenden. Ich war faul.

Hier ist eine überarbeitete Prozedur mit einem Ausgabeparameter, zusätzlichen Optimierungen und dem, next value forwas @srutzky in seiner Antwort erklärt :

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

Hinweis zum Update : Durch das Einbeziehen updlockder Option select werden in diesem Szenario die richtigen Sperren aktiviert. Vielen Dank an @srutzky, der darauf hingewiesen hat, dass dies zu Deadlocks führen kann, wenn es nur serializableauf dem Computer verwendet wird select.

Hinweis: Dies ist möglicherweise nicht der Fall, aber wenn dies möglich ist, wird die Prozedur mit einem Wert für @vValueId, include set @vValueId = null;after aufgerufen set xact_abort on;, andernfalls kann sie entfernt werden.


Zu @ srutzkys Beispielen für das Sperrverhalten von Schlüsselbereichen:

@srutzky verwendet nur einen Wert in seiner Tabelle und sperrt den Schlüssel "next" / "infinity" für seine Tests, um das Sperren des Schlüsselbereichs zu veranschaulichen. Während seine Tests veranschaulichen, was in diesen Situationen passiert, glaube ich, dass die Art und Weise, in der die Informationen präsentiert werden, zu falschen Annahmen über die Menge an Sperren führen kann, die bei der Verwendung serializablein dem in der ursprünglichen Frage dargestellten Szenario zu erwarten sind .

Auch wenn ich eine Voreingenommenheit (vielleicht fälschlicherweise) in der Art und Weise wahrnehme, in der er seine Erklärungen und Beispiele für die Tastensperrung präsentiert, sind sie immer noch richtig.


Nach weiteren Recherchen fand ich einen besonders relevanten Blog-Artikel von Michael J. Swart aus dem Jahr 2011: Mythbusting: Concurrent Update / Insert Solutions . Darin testet er mehrere Methoden auf Genauigkeit und Gleichzeitigkeit. Methode 4: Erhöhte Isolation + Feineinstellung von Sperren basiert auf Sam Saffrons Einfüge- oder Aktualisierungsmuster für SQL Server und ist die einzige Methode im ursprünglichen Test, mit der seine Erwartungen erfüllt wurden (zusammen mit merge with (holdlock)).

Im Februar 2016 veröffentlichte Michael J. Swart Ugly Pragmatism For The Win . In diesem Beitrag behandelt er einige zusätzliche Anpassungen, die er an seinen Safran-Upsert-Prozeduren vorgenommen hat, um die Sperrung zu verringern (die ich in die obige Prozedur aufgenommen habe).

Nachdem er diese Änderungen vorgenommen hatte, war Michael nicht glücklich darüber, dass sein Verfahren immer komplizierter wurde, und befragte einen Kollegen namens Chris. Chris hat den gesamten ursprünglichen Mythbusters-Beitrag gelesen, alle Kommentare gelesen und nach dem TRY CATCH JFDI-Muster von @ gbn gefragt . Dieses Muster ähnelt @ srutzkys Antwort und ist die Lösung, die Michael letztendlich in dieser Instanz verwendet hat.

Michael J Swart:

Gestern hatte ich meine Meinung geändert, wie ich am besten Parallelität herstellen kann. Ich beschreibe verschiedene Methoden in Mythbusting: Concurrent Update / Insert Solutions. Meine bevorzugte Methode ist die Erhöhung der Isolationsstufe und die Feinabstimmung der Sperren.

Zumindest war das meine Präferenz. Ich habe kürzlich meinen Ansatz geändert, um eine Methode zu verwenden, die gbn in den Kommentaren vorgeschlagen hat. Er beschreibt seine Methode als das "TRY CATCH JFDI-Muster". Normalerweise vermeide ich solche Lösungen. Es gibt eine Faustregel, die besagt, dass Entwickler sich nicht darauf verlassen sollten, Fehler oder Ausnahmen für den Kontrollfluss abzufangen. Aber ich habe gestern diese Faustregel gebrochen.

Übrigens, ich liebe die Beschreibung der GBN für das Muster „JFDI“. Es erinnert mich an Shia Labeoufs Motivationsvideo.


Meiner Meinung nach sind beide Lösungen machbar. Ich ziehe es zwar immer noch vor, die Isolationsstufe zu erhöhen und die Sperren fein abzustimmen, aber @ srutzkys Antwort ist auch gültig und kann in Ihrer spezifischen Situation leistungsfähiger sein oder auch nicht.

Vielleicht komme ich auch in Zukunft zu dem gleichen Schluss wie Michael J. Swart, aber ich bin einfach noch nicht da.


Es ist nicht meine Präferenz, aber hier ist, wie meine Anpassung von Michael J. Stewarts Adaption von @ gbns Try Catch JFDI- Prozedur aussehen würde:

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

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 .

Aaron Bertrands Kommentare zu Michael J Swarts Beitrag verweisen auf relevante Tests, die er durchgeführt hat, und führten zu diesem Austausch. Auszug aus dem Kommentarbereich zu Ugly Pragmatism For the Win :

Manchmal führt JFDI jedoch zu einer insgesamt schlechteren Leistung, je nachdem, wie viel Prozent der Aufrufe fehlschlagen. Das Auslösen von Ausnahmen ist mit erheblichem Aufwand verbunden. Ich habe das in ein paar Beiträgen gezeigt:

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/überprüfen-von- potenziellen- Einschränkungsverletzungen-vor-eintritt-in-sql-server-versuch-und-fangen-logik/

Kommentar von Aaron Bertrand - 11. Februar 2016 um 11:49 Uhr

und die Antwort von:

Du hast Recht, Aaron, und wir haben es getestet.

Es stellt sich heraus, dass in unserem Fall der Prozentsatz der fehlgeschlagenen Anrufe 0 war (auf den nächsten Prozentsatz gerundet).

Ich denke, Sie veranschaulichen den Punkt, dass so viel wie möglich von Fall zu Fall über die Einhaltung von Faustregeln bewertet wird.

Aus diesem Grund haben wir die nicht unbedingt erforderliche WHERE NOT EXISTS-Klausel hinzugefügt.

Kommentar von Michael J. Swart - 11. Februar 2016 um 11:57 Uhr


Neue Links:


Ursprüngliche Antwort


Ich bevorzuge immer noch den Upsert-Ansatz von Sam Saffron gegenüber der Verwendung merge, insbesondere wenn es sich um eine einzelne Zeile handelt.

Ich würde diese Upsert-Methode folgendermaßen an diese Situation anpassen:

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

Ich würde mit Ihrer Benennung übereinstimmen, und so wie es serializableist holdlock, wählen Sie eine aus und verwenden Sie sie konsequent. Ich neige dazu, zu verwenden, serializableweil es derselbe Name ist, der bei der Angabe verwendet wird set transaction isolation level serializable.

Durch die Verwendung von serializableoder wird holdlockeine Bereichssperre verwendet, die auf dem Wert basiert, der @vNamealle anderen Operationen warten lässt, wenn sie Werte auswählen oder einfügen dbo.NameLookup, die den Wert in der whereKlausel enthalten.

Damit die Bereichssperre ordnungsgemäß funktioniert, muss ein Index für die ItemNameSpalte vorhanden sein. Dies gilt auch für die Verwendung merge.


Hier ist , was das Verfahren aussehen würde meist folgenden Erland Sommarskog des White Paper für die Fehlerbehandlung , mit throw. Wenn throwSie Ihre Fehler nicht auslösen, ändern Sie sie so, dass sie mit den übrigen Verfahren übereinstimmen:

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

Um zusammenzufassen, was in der obigen Prozedur vor sich geht: set nocount on; set xact_abort on;Wie Sie es immer tun , dann ist unsere Eingabevariable is nulloder leer select id = cast(null as int)das Ergebnis. Wenn es nicht null oder leer ist, dann holen Sie das Idfür unsere Variable, während Sie diese Stelle halten, falls es nicht da ist. Wenn der da Idist, schick ihn raus. Wenn es nicht da ist, legen Sie es ein und senden Sie das neue Id.

Währenddessen warten andere Aufrufe dieser Prozedur, die versuchen, die ID für denselben Wert zu finden, bis die erste Transaktion abgeschlossen ist, und wählen Sie diese aus und geben Sie sie zurück. Andere Aufrufe dieser Prozedur oder andere Anweisungen, die nach anderen Werten suchen, werden fortgesetzt, da dies nicht im Wege steht.

Obwohl ich mit @srutzky einverstanden bin, dass Sie mit Kollisionen umgehen und die Ausnahmen für diese Art von Problem schlucken können, ziehe ich es persönlich vor, eine maßgeschneiderte Lösung zu finden, um dies nach Möglichkeit zu vermeiden. In diesem Fall bin ich nicht der Meinung, dass die Verwendung der Sperren von serializableein umständlicher Ansatz ist, und ich wäre zuversichtlich, dass sie mit hoher Parallelität gut umgehen können.

Zitat aus der SQL Server-Dokumentation auf der Tabelle Hinweise serializable/holdlock :

SERIALISIERBAR

Entspricht HOLDLOCK. Verschärft freigegebene Sperren, indem sie bis zum Abschluss einer Transaktion beibehalten werden, anstatt die freigegebene Sperre aufzuheben, sobald die erforderliche Tabelle oder Datenseite nicht mehr benötigt wird, unabhängig davon, ob die Transaktion abgeschlossen wurde oder nicht. Der Scan wird mit derselben Semantik wie eine Transaktion ausgeführt, die auf der Isolationsstufe SERIALIZABLE ausgeführt wird. Weitere Informationen zu Isolationsstufen finden Sie unter SET TRANSACTION ISOLATION LEVEL (Transact-SQL).

Zitat aus der SQL Server-Dokumentation zur Transaktionsisolationsstufeserializable

SERIALIZABLE Gibt Folgendes an:

  • Anweisungen können keine Daten lesen, die geändert, aber noch nicht von anderen Transaktionen festgeschrieben wurden.

  • Keine andere Transaktion kann Daten ändern, die von der aktuellen Transaktion gelesen wurden, bis die aktuelle Transaktion abgeschlossen ist.

  • 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.


Links zur obigen Lösung:

MERGEhat einen fleckigen Verlauf, und es scheint mehr Zeit in Anspruch zu nehmen, um sicherzustellen, dass sich der Code unter all dieser Syntax so verhält, wie Sie es möchten. Relevante mergeArtikel:

Ein letztes Glied, Kendra Little, hat einen groben Vergleich von mergevsinsert with left join mit dem Vorbehalt angestellt, dass "ich keine gründlichen Belastungstests durchgeführt habe", aber es ist immer noch eine gute Lektüre.

SqlZim
quelle