Fügen Sie eine Zeile nur ein, wenn sie noch nicht vorhanden ist

72

Ich hatte immer etwas Ähnliches verwendet, um dies zu erreichen:

INSERT INTO TheTable
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WHERE
        PrimaryKey = @primaryKey)

... aber einmal unter Last ist eine Primärschlüsselverletzung aufgetreten. Dies ist die einzige Anweisung, die überhaupt in diese Tabelle eingefügt wird. Bedeutet dies also, dass die obige Aussage nicht atomar ist?

Das Problem ist, dass es fast unmöglich ist, dies nach Belieben wiederherzustellen.

Vielleicht könnte ich es wie folgt ändern:

INSERT INTO TheTable
WITH
    (HOLDLOCK,
    UPDLOCK,
    ROWLOCK)
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WITH
        (HOLDLOCK,
        UPDLOCK,
        ROWLOCK)
    WHERE
        PrimaryKey = @primaryKey)

Obwohl ich vielleicht die falschen Schlösser benutze oder zu viele Schlösser oder so.

Ich habe andere Fragen auf stackoverflow.com gesehen, bei denen die Antworten ein "IF (SELECT COUNT (*) ... INSERT" usw.) vorschlagen, aber ich war immer unter der (möglicherweise falschen) Annahme, dass eine einzelne SQL-Anweisung atomar sein würde.

Hat jemand irgendwelche Ideen?

Adam
quelle
3
Haben Sie versucht, eine Zusammenführung ohne WHEN MATCHEDKlausel zu verwenden?
Am
3
Auf welcher Version von SQL Server befinden Sie sich?
Martin Smith
Es variiert je nach Kunde. Alles zwischen 2000 und 2008 R2. Obwohl wir vielleicht am 7. waren, als die Erklärung ursprünglich geschrieben wurde!
Adam
Ich muss mir diese neue (für mich) MERGEAussage ansehen . Funktioniert es in diesem Fall besser?
Adam
1
Ich verstehe den Punkt nicht! Fügen Sie einfach Ihre Daten ein, und wenn die PK bereits vorhanden ist, schlägt die Einfügung fehl, und das ist in Ordnung. Oder fehlt mir etwas?
Patrick Honorez

Antworten:

65

Was ist mit dem "JFDI" -Muster?

BEGIN TRY
   INSERT etc
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
END CATCH

Im Ernst, dies ist am schnellsten und gleichzeitig ohne Sperren, insbesondere bei hohen Lautstärken. Was ist, wenn das UPDLOCK eskaliert und die gesamte Tabelle gesperrt ist?

Lesen Sie Lektion 4 :

Lektion 4: Bei der Entwicklung des Upsert-Prozesses vor dem Optimieren der Indizes vertraute ich zunächst darauf, dass die If Exists(Select…)Zeile für jedes Element ausgelöst und Duplikate verboten werden. Nada. In kurzer Zeit gab es Tausende von Duplikaten, da dasselbe Element in derselben Millisekunde auf den Upsert traf und bei beiden Transaktionen ein nicht vorhandenes Element angezeigt wurde und das Einfügen durchgeführt wurde. Nach vielen Tests bestand die Lösung darin, den eindeutigen Index zu verwenden, den Fehler abzufangen und erneut zu versuchen, damit die Transaktion die Zeile sehen und stattdessen ein Update und eine Einfügung durchführen kann.

gbn
quelle
Danke - okay, ich stimme zu, dass dies wahrscheinlich das ist, was ich am Ende verwenden werde, und die Antwort auf die eigentliche Frage ist.
Adam
1
Ich weiß, dass es schlecht ist, sich auf solche Fehler zu verlassen, aber ich frage mich, ob dies mit einer Geraden INSERT(ohne die EXISTS) besser funktioniert (dh versuchen Sie, egal was einzufügen, und ignorieren Sie einfach den Fehler 2627).
Adam
1
Das hängt davon ab, ob Sie meistens Werte einfügen, die nicht existieren, oder meistens Werte, die existieren. Im letzteren Fall würde ich behaupten, dass die Leistung schlechter sein wird, da Tonnen von Ausnahmen ausgelöst und ignoriert werden.
GSerg
@Gserg: richtig. Aber dann hätte OP eine INSERT / UPDATE-Frage gestellt und nicht auf Inert getestet. Wir verwenden dies, um ein paar tausend Duplikate in Dutzend Millionen neuen Zeilen pro Tag
herauszufiltern
4
@student 35k tps = "35000 Transaktionen pro Sekunde". Der TRY CATCH verhindert, dass ein doppelter Eintrag eingefügt wird, indem der eindeutige Fehler bei der Einschränkungsverletzung (Fehlernummer 2627) abgefangen und ignoriert wird. Der CATCH wird den Fehler nur dann erneut auslösen, wenn er nicht 2627 ist. Es gibt ein Problem mit diesem Snippet, da eine eindeutige Indexverletzung der Fehler 2601 ist. Sie müssen also nach diesen beiden Codes suchen . Diese Lösung funktioniert auch nur für einzeilige INSERTs. Wenn Sie versuchen, von einer Tabelle in eine andere Tabelle einzufügen, benötigen Sie eine andere Strategie.
Jim
24

Ich habe HOLDLOCK hinzugefügt, das ursprünglich nicht vorhanden war. Bitte ignorieren Sie die Version ohne diesen Hinweis.

Für mich sollte dies ausreichen:

INSERT INTO TheTable 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 0
     FROM TheTable WITH (UPDLOCK, HOLDLOCK)
     WHERE PrimaryKey = @primaryKey) 

Wenn Sie eine Zeile tatsächlich aktualisieren möchten, falls vorhanden, und einfügen, wenn dies nicht der Fall ist, ist diese Frage möglicherweise hilfreich.

GSerg
quelle
2
Was sperren Sie, wenn die Zeile nicht vorhanden ist?
Martin Smith
3
Ein relevanter Bereich im Index (in diesem Fall der Primärschlüssel).
GSerg
@ GSerg vereinbart. Das pessimistische / optimistische Sperren der select-Anweisung erfordert eine Direktive.
DaveWilliamson
2
Die Tests zeigen, dass zwei select 'Done.' where exists(select 0 from foo_testing with(updlock) where id = 4);, sofern id=4nicht vorhanden , nicht miteinander in Konflikt stehen, was bedeutet, dass meine ursprüngliche Antwort tatsächlich falsch war. Die Lösung besteht darin, den HOLDLOCKHinweis hinzuzufügen . Siehe die bearbeitete Antwort. Danke, dass du mich nervt :)
GSerg
3
Es gibt eine gute Erklärung dafür, warum diese Sperre in Daniels Antwort auf meine (sehr ähnliche) Frage erforderlich ist
Iain
17

Sie könnten MERGE verwenden:

MERGE INTO Target
USING (VALUES (@primaryKey, @value1, @value2)) Source (key, value1, value2)
ON Target.key = Source.key
WHEN MATCHED THEN
    UPDATE SET value1 = Source.value1, value2 = Source.value2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Name, ReasonType) VALUES (@primaryKey, @value1, @value2)
Chris Smith
quelle
In diesem Fall können Sie das 'WHEN MATCHED THEN' entfernen, da Adam nur einfügen muss, wenn es fehlt, nicht Upsert.
Iain
4
Entschuldigung, aber ohne Hinweise zur Haltesperre zu Ihrer Zusammenführungsanweisung hinzuzufügen, haben Sie genau das Problem, um das sich das OP kümmert.
EBarr
7
Lesen Sie diesen Artikel für mehr über @ EBarrs Punkt
Martin Smith
1
@MartinSmith - das ist genau der Artikel, den ich gelesen habe, als ich auf das Problem gestoßen bin! Danke für den Hinweis.
EBarr
In der MSDN-Dokumentation heißt es (im Leistungstipp), dass Sie Einfügen verwenden sollten, wenn es nicht vorhanden ist, anstatt zusammenzuführen, es sei denn, Komplexität ist erforderlich ... msdn.microsoft.com/en-us/library/…
Mladen Mihajlovic
1

Ich weiß nicht, ob dies der "offizielle" Weg ist, aber Sie könnten es versuchen INSERTund zurückgreifen, UPDATEwenn es fehlschlägt.

Marcelo Cantos
quelle
1

Erstens ein großes Dankeschön an unseren Mann @gbn für seine Beiträge zur Community. Ich kann gar nicht erklären, wie oft ich seinem Rat folge.

Wie auch immer, genug Fanboying.

Um seine Antwort etwas zu ergänzen, "verbessern" Sie sie vielleicht. Für diejenigen, die sich wie ich unwohl fühlen, was in dem <> 2627Szenario zu tun ist (und nein, ein Leerzeichen CATCHist keine Option). Ich habe dieses kleine Nugget von Technet gefunden .

    BEGIN TRY
       INSERT etc
    END TRY
    BEGIN CATCH
        IF ERROR_NUMBER() <> 2627
          BEGIN
                DECLARE @ErrorMessage NVARCHAR(4000);
                DECLARE @ErrorSeverity INT;
                DECLARE @ErrorState INT;

                SELECT @ErrorMessage = ERROR_MESSAGE(),
                @ErrorSeverity = ERROR_SEVERITY(),
                @ErrorState = ERROR_STATE();

                    RAISERROR (
                        @ErrorMessage,
                        @ErrorSeverity,
                        @ErrorState
                    );
          END
    END CATCH
pim
quelle
Dies ist genau das, was ich in der vorherigen Antwort richtungslos gelassen habe. +1 an euch beide!
T0t3sMcG0t3s
-5

Ich habe in der Vergangenheit eine ähnliche Operation mit einer anderen Methode durchgeführt. Zuerst deklariere ich eine Variable, die den Primärschlüssel enthält. Dann fülle ich diese Variable mit der Ausgabe einer select-Anweisung, die nach einem Datensatz mit diesen Werten sucht. Dann mache ich und IF-Anweisung. Wenn der Primärschlüssel null ist, fügen Sie ihn ein, andernfalls geben Sie einen Fehlercode zurück.

     DECLARE @existing varchar(10)
    SET @existing = (SELECT primaryKey FROM TABLE WHERE param1field = @param1 AND param2field = @param2)

    IF @existing is not null
    BEGIN
    INSERT INTO Table(param1Field, param2Field) VALUES(param1, param2)
    END
    ELSE
    Return 0
END
Marc
quelle
Warum nicht einfach tun: WENN NICHT EXISTIERT (SELECT * FROM TABLE WHERE param1field = @ param1 AND param2field = @ param2) BEGIN INSERT INTO Table (param1Field, param2Field) VALUES (param1, param2) END
Vidar Nordnes
Ja, aber das sieht so aus, als ob es offen für Parallelitätsprobleme ist (dh was passiert, wenn bei einer anderen Verbindung zwischen Ihrer Auswahl und Ihrer Einfügung etwas passiert?)
Adam
2
@Adam Marc's Code oben ist nicht besser, um Sperrprobleme zu vermeiden. Die einzigen zwei Möglichkeiten zur Behandlung von Parallelitätsproblemen sind das Sperren mit WITH (UPDLOCK, HOLDLOCK) oder das Behandeln des Einfügefehlers und das Konvertieren in ein Update.
ErikE