SQL Server - Wählen Sie diese Option, während Sie sie in eine andere Transaktion einfügen. Dies führt zu unerwarteten Ergebnissen

8

Ich bin über eine Situation gestolpert, die mein Wissen über Transaktionen und Sperren grundlegend verändert (ich weiß allerdings nicht viel), und ich brauche Hilfe, um es zu verstehen.

Nehmen wir an, ich habe einen Tisch wie diesen:

CREATE TABLE [dbo].[SomeTable](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[SomeData] [varchar](200) NOT NULL,
[Moment] [datetime] NOT NULL,
[SomeInt] [bigint] NOT NULL
) ON [PRIMARY]

und ich führe diese Abfrage "1000 Zeilen in eine Transaktion einfügen" aus:

BEGIN TRAN t1

DECLARE @i INT = 0

WHILE @i < 1000
BEGIN
    SET @i = @i + 1

    INSERT INTO [SomeTable] ([SomeData] ,Moment, SomeInt)
    VALUES (CONVERT(VARCHAR(255), NEWID()), getdate(), @i)

    WAITFOR DELAY '00:00:00:010'
END

COMMIT TRAN t1

Während diese Transaktion ausgeführt wird, führe ich eine einfache Auswahl aus:

SELECT Id, Moment, SomeData, SomeInt FROM [SomeTable]

Es ist nicht immer möglich, es zu reproduzieren (anscheinend hängt es vom Zeitpunkt ab), aber manchmal gibt die ausgewählte Abfrage nach Abschluss der Einfügungstransaktion weniger als 1000 Zeilen zurück. In meiner Unwissenheit habe ich geglaubt, dass select immer 1000 Zeilen zurückgibt (vorausgesetzt, die Isolationsstufe ist Read Committed), aber offensichtlich habe ich falsch verstanden, wie Transaktionen und Sperren funktionieren.

Wenn ich jedoch einen Primärschlüssel in die ID-Spalte einfüge (die einen Clustered-Index generiert), gibt die Auswahlabfrage, solange ich es versucht habe, alle 1000 Zeilen zurück. Wenn Sie Indizes auf andere Weise platzieren, mit einem Clustered-Index für den zusammengesetzten Schlüssel und einem Nicht-Clustered-Index für einige andere Spalten, kann dies wiederum dazu führen, dass weniger Zeilen zurückgegeben werden, als ich erwartet habe.

Ich habe also folgende Fragen:

  1. Warum gibt select nicht immer alle von der Transaktion festgeschriebenen Zeilen zurück?
  2. Wenn dies erwartetes Verhalten ist, wie kann es am besten funktionieren, wie ich es erwartet habe? Grundsätzlich möchte ich auswählen, dass der Status der Tabelle nach (oder vor) der Transaktion zurückgegeben wird, nicht einige halbfertige Daten. Die Snapshot-Isolierung ist derzeit keine Option. Das Setzen von TABLOCK scheint die Arbeit zu erledigen, aber gibt es eine bessere Lösung? Im wirklichen Leben habe ich Tabellen, die ich auf dieser Ebene nicht sperren möchte, wenn es nicht unbedingt notwendig ist.
  3. Warum ändert das Setzen eines Index dieses Verhalten?

Danke im Voraus.

Sack
quelle

Antworten:

12

Ich habe es nicht geschafft, dies zu reproduzieren, nachdem ich Ihren Code einige Male ausgeführt habe.

Ich gehe davon aus, dass es passieren muss, wenn eine spätere Zeile auf eine frühere Seite in der Datei eingefügt wird.

Die Reihenfolge der Operationen ist also (zum Beispiel)

  • In den Heap eingefügte Zeilen auf den Seiten 200, 207, 223
  • Die Select-Anweisung startet und führt einen nach Zuordnung geordneten Scan durch. Stellt fest, dass die erste Seite 200 ist und blockiert ist, bis eine Zeilensperre freigegeben wird.
  • Andere Zeilen werden bei der ersten Transaktion eingefügt. Einige von ihnen werden auf einer Seite vor 200 zugewiesen. Fügen Sie Transaktions-Commits ein.
  • Die Zeilensperre wird aufgehoben und der zugeordnete geordnete Scan fortgesetzt. Zeilen früher in der Datei werden übersehen.

Die Tabelle umfasste 10 Seiten. Standardmäßig werden die ersten 8 Seiten aus gemischten Bereichen zugewiesen, und dann wird ihnen ein einheitlicher Umfang zugewiesen. Möglicherweise war in Ihrem Fall vor den verwendeten gemischten Ausmaßen Speicherplatz in der Datei für einen freien einheitlichen Umfang verfügbar.

Sie können diese Theorie testen, indem Sie Folgendes in einem anderen Fenster SELECTausführen, nachdem Sie das Problem reproduziert haben, und prüfen, ob die fehlenden Zeilen des Originals alle am Anfang dieser Ergebnismenge erscheinen.

SELECT [SomeData],
       Moment,
       SomeInt,
       file_id,
       page_id,
       slot_id
FROM   [SomeTable] 
/*Undocumented - Use at own risk*/
CROSS APPLY sys.fn_PhysLocCracker(%% physloc %%)
ORDER BY page_id, SomeInt

Die Operation für eine indizierte Tabelle erfolgt in der Reihenfolge der Indexschlüssel und nicht in der Zuordnungsreihenfolge, sodass dieses spezielle Szenario keine Auswirkungen hat.

Ein nach Zuordnung geordneter Scan kann für einen Index ausgeführt werden, wird jedoch nur berücksichtigt, wenn die Tabelle ausreichend groß ist und die Isolationsstufe nicht festgeschrieben gelesen wird oder eine Tabellensperre gehalten wird.

Da ein festgeschriebener Lesevorgang im Allgemeinen Sperren aufhebt, sobald die Daten gelesen werden, kann ein Scan des Index Zeilen zweimal oder gar nicht lesen (wenn der Indexschlüssel durch eine gleichzeitige Transaktion aktualisiert wird, wodurch die Zeile vorwärts oder rückwärts verschoben wird ) Weitere Informationen zu dieser Art von Problem finden Sie unter Read Read Committed Isolation Level .


Übrigens hatte ich mir ursprünglich für den indizierten Fall vorgestellt, dass sich der Index in einer der Spalten befindet, die im Verhältnis zur Einfügereihenfolge zunimmt (eine von Id, Moment, SomeInt). Selbst wenn der Clustered-Index zufällig ist, tritt SomeDatadas Problem dennoch nicht auf.

Ich habe es versucht

DBCC TRACEON(3604, 1200, -1) /*Caution. Global trace flag. Outputs lock info
                               on every connection*/

SELECT TOP 2 *,
             %%LOCKRES%%
FROM   [SomeTable] WITH(nolock)
ORDER BY [SomeData];

SELECT *,
       %%LOCKRES%%
FROM   [SomeTable]
ORDER BY [SomeData];

/*Turn off trace flags. Doesn't check whether or not they were on already 
  before we started, with TRACEOFF*/
DBCC TRACEOFF(3604, 1200, -1)

Die Ergebnisse waren wie folgt

Geben Sie hier die Bildbeschreibung ein

Die zweite Ergebnismenge enthält alle 1.000 Zeilen. Die Sperrinformationen zeigen, dass der Scan nicht nur von diesem Punkt an fortgesetzt wird , obwohl er beim Aufheben 24c910701749der Sperre auf die Sperrressource gewartet hat. Stattdessen wird diese Sperre sofort aufgehoben und eine Zeilensperre für die neue erste Zeile erworben.

Geben Sie hier die Bildbeschreibung ein

Martin Smith
quelle
Martin, danke, dass du dir die Mühe gemacht hast und deine Erklärung, es ist sehr hilfreich. Ich werde einige Zeit brauchen, um mehr mit den von Ihnen bereitgestellten Beispielen und den realen Tabellen zu experimentieren, die ich habe. Bisher habe ich das Problem reproduziert (Hit & Miss-Ansatz, auch die Wartezeiten für Transaktionen und die Anzahl der in einem Durchgang eingefügten Zeilen wurden auf 10000 geändert). Wählen Sie also parallel ausgeführt 22684 von 30000 Zeilen aus. Durch Testabfrage erhalten Sie vorausgesetzt (fn_PhysLocCracker), ausgeschlossene Zeilen befinden sich auf den Seiten 384 bis 405 von 405. So sehen drei Randfallzeilen aus
poke
Das Komische ist, dass die Tabelle in der realen Welt ein CI in vier Spalten und 7 NCI hat, aber ich kann das Problem trotzdem reproduzieren. Das Tabellendesign ist absolut schlecht, aber ich kann auf dieser Seite nicht zu viel tun (Legacy-Design mit vielen externen Abfragen), außer um diese albernen Indizes zu ändern. Ich habe bereits experimentiert, indem ich sie alle gelöscht und nur ein CI in der Identitätsspalte und ein eindeutiges NCI belassen habe, und das Problem war (anscheinend) behoben. Ich bin immer noch verwirrt, wie sich das Ändern von Indizes auf solch ein grundlegendes Verhalten auswirken kann. Ich muss mehr mit Beispielen experimentieren, die Sie bereitgestellt haben, um es besser zu verstehen.
stupsen