Doppelte Datensätze, die ohne Duplikate aus der Tabelle zurückgegeben wurden

8

Ich habe eine gespeicherte Prozedur, die eine ausgelastete Warteschlangentabelle abfragt, die zum Verteilen der Arbeit in unserem System verwendet wird. Die betreffende Tabelle hat einen Primärschlüssel für WorkID und keine Duplikate.

Eine vereinfachte Version der Abfrage lautet:

INSERT INTO #TempWorkIDs (WorkID)
SELECT
        W.WorkID

    FROM
        dbo.WorkTable W

    WHERE
        (@bool_param = 0 AND
        ((W.InProgress = 0
         AND ISNULL(W.UserID, -1) != @userid_param
         AND (@bool_filtered = 0
              OR W.TypeID IN (SELECT TypeID FROM #Types AS t)))
         OR 
         (@bool_param = 1
          AND W.InProgress = 1
          AND W.UserID != @userid_param)
        OR
        (@Auto_Param = 0
         AND W.UserID = @userid_param)))
         OR
         (@bool_param = 1 AND W.UserID = @userid_param)
    OPTION
        (RECOMPILE)

Die #TypesTabelle wird früher in der Prozedur ausgefüllt.

Wie gesagt, WorkTableist beschäftigt, und manchmal wird während der Ausführung dieser Abfrage I SUSPECT einer der Datensätze von einem Satz von Filtern in den WHEREanderen verschoben . Dies geschieht insbesondere dann, wenn jemand mit der Arbeit an einem Element beginnt und W.InProgresssich von 0 auf 1 ändert. In diesem Fall tritt eine doppelte Schlüsselverletzung auf, wenn ich versuche, der temporären Tabelle, in die diese Abfrage eingefügt wird, einen Primärschlüssel hinzuzufügen.

Ich habe im Abfrageplan, der generiert wurde, als der Fehler auftritt, bestätigt, dass keine Parallelität vorliegt, die Isolationsstufe ist READ COMMITTEDund keine doppelten Datensätze in der Quelltabelle vorhanden sind. Sie können auch sehen, dass es hier keine JOINs oder andere Möglichkeit gibt, kartesische Produkte zu erhalten.

Dies ist der anonymisierte Abfrageplan:

Geben Sie hier die Bildbeschreibung ein

Die Frage ist, was verursacht die Duplikate und wie kann ich sie stoppen?

Ich denke READ COMMITTEDsollte hier funktionieren, ich brauche Sperren. Ich bin mir fast sicher, dass die Dupes auftreten, wenn sich das InProgressBit in einem Datensatz ändert, während ich abfrage. Ich weiß das, weil die Tabelle den Zeitpunkt dieser Änderung speichert und es innerhalb von Millisekunden ist, wenn ich den Fehler abfrage und erhalte.

JNK
quelle

Antworten:

9

Es gibt einige knifflige Szenarien, die dazu führen können, dass dieselbe Zeile auch unter der READ COMMITTEDIsolationsstufe zweimal aus einem Index gelesen wird .

Ihre Abfrage ist nicht für einen Scan der Zuordnungsreihenfolge qualifiziert , daher liest die Speicher-Engine die Daten aus der Tabelle in der Reihenfolge des Clusterschlüssels.

Für Ihre Tabelle haben Sie InProgressdie erste Spalte des Clusterschlüssels. Es ist wahrscheinlich, dass Sie beim Durchsuchen der Tabelle Zeilen- oder Seitensperren erhalten. Wenn Sie eine Zeile in der Nähe des Scan-Starts lesen, lösen Sie die Sperre für diese Zeile. Diese Zeile wird so aktualisiert, dass sie InProgressvon 0 auf 1 wechselt. Anschließend wird die Zeile auf einer anderen Seite erneut gelesen. In WorkIDIhrer Abfrage werden möglicherweise doppelte Werte angezeigt .

Es gibt viele Problemumgehungen. Sie können in einen Heap einfügen und einfach doppelte Werte entfernen. Sie können DISTINCTder Abfrage ein hinzufügen . Sie können auch eine Isolationsstufe für die Zeilenversionierung aktivieren, um eine stabile Ansicht des festgeschriebenen Status der Datenbank bereitzustellen, entweder zu Beginn der Transaktion ( Snapshot-Isolation ) oder wie zu Beginn der Anweisung ( Lesen der festgeschriebenen Snapshot-Isolation) ).

Vielleicht ist es angebracht, Sperrhinweise hinzuzufügen oder die Struktur der Tabelle zu ändern. Für eine ziemlich unterhaltsame Lösung (wahrscheinlich nicht für die Produktion geeignet) können Sie versuchen, den Index rückwärts zu lesen. Dies kann mit einem überflüssigen TOPzusammen mit einem erfolgen ORDER BY. Unten finden Sie eine sehr einfache Demo, um den Punkt zu veranschaulichen:

CREATE TABLE #WorkTable (
    InProgress TINYINT NOT NULL,
    WorkID INT NOT NULL
    , PRIMARY KEY (InProgress, WorkID)
);

INSERT INTO #WorkTable WITH (TABLOCK)
SELECT (RN - 1) / 5000, RN
FROM
(
    SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t
OPTION (MAXDOP 1);

Die folgende Abfrage hat die Eigenschaft Ordered: false, liest die Daten jedoch weiterhin in der Reihenfolge der Clusterschlüssel:

SELECT WorkId
FROM #WorkTable;

Bei der folgenden Abfrage werden die Daten jedoch in umgekehrter Clusterreihenfolge gelesen:

SELECT TOP (9223372036854775807) WorkId
FROM #WorkTable
ORDER BY InProgress DESC, WorkId DESC;

Wir können dies anhand der Scan-Eigenschaften erkennen:

rückwärts scannen

Für Ihre Tabelle bedeutet dies, dass wenn eine Zeile so aktualisiert wird, dass sie InProgresssich von 0 auf 1 ändert, die Wahrscheinlichkeit, dass sie zweimal angezeigt wird, weitaus geringer ist. Es wird möglicherweise überhaupt nicht angezeigt, was ein anderes Problem sein könnte.

Joe Obbish
quelle