Ist eine einzelne SQL Server-Anweisung atomar und konsistent?

74

Ist eine Anweisung in SQL Server ACID?

Was ich damit meine

Bei einer einzelnen T-SQL-Anweisung, die nicht in ein BEGIN TRANSACTION/ eingeschlossen ist COMMIT TRANSACTION, sind die Aktionen dieser Anweisung:

  • Atomic : Entweder werden alle Datenänderungen durchgeführt oder keine.
  • Konsistent : Nach Abschluss einer Transaktion müssen alle Daten in einem konsistenten Zustand bleiben.
  • Isoliert : Änderungen, die durch gleichzeitige Transaktionen vorgenommen werden, müssen von den Änderungen isoliert werden, die durch andere gleichzeitige Transaktionen vorgenommen werden.
  • Dauerhaft : Nachdem eine Transaktion abgeschlossen wurde, sind ihre Auswirkungen dauerhaft im System vorhanden.

Der Grund, den ich frage

Ich habe eine einzelne Anweisung in einem Live-System, die anscheinend gegen die Regeln der Abfrage verstößt.

Tatsächlich lautet meine T-SQL-Anweisung:

--If there are any slots available, 
--then find the earliest unbooked transaction and mark it booked
UPDATE Transactions
SET Booked = 1
WHERE TransactionID = (
   SELECT TOP 1 TransactionID
   FROM Slots
      INNER JOIN Transactions t2
      ON Slots.SlotDate = t2.TransactionDate
   WHERE t2.Booked = 0 --only book it if it's currently unbooked
   AND Slots.Available > 0 --only book it if there's empty slots
   ORDER BY t2.CreatedDate)

Hinweis : Eine einfachere konzeptionelle Variante könnte jedoch sein:

--Give away one gift, as long as we haven't given away five
UPDATE Gifts
SET GivenAway = 1
WHERE GiftID = (
   SELECT TOP 1 GiftID
   FROM Gifts
   WHERE g2.GivenAway = 0
   AND (SELECT COUNT(*) FROM Gifts g2 WHERE g2.GivenAway = 1) < 5
   ORDER BY g2.GiftValue DESC
)

Beachten Sie in diesen beiden Aussagen, dass es sich um einzelne Aussagen handelt (UPDATE...SET...WHERE ).

Es gibt Fälle, in denen die falsche Transaktion "gebucht" wird . es ist tatsächlich eine spätere Auswahl Transaktion ausgewählt. Nachdem ich 16 Stunden lang darauf gestarrt habe, bin ich ratlos. Es ist, als würde SQL Server einfach gegen die Regeln verstoßen.

Ich habe mich gefragt, was Slotspassiert , wenn sich die Ergebnisse der Ansicht ändern, bevor das Update durchgeführt wird. Was ist, wenn SQL Server SHAREDdie Transaktionen an diesem Datum nicht sperrt ? ? Ist es möglich, dass eine einzelne Aussage inkonsistent sein kann?

Also habe ich beschlossen, es zu testen

Ich habe mich entschlossen zu prüfen, ob die Ergebnisse von Unterabfragen oder inneren Operationen inkonsistent sind. Ich habe eine einfache Tabelle mit einer einzelnen intSpalte erstellt:

CREATE TABLE CountingNumbers (
   Value int PRIMARY KEY NOT NULL
)

Aus mehreren Verbindungen heraus rufe ich in einer engen Schleife die einzelne T-SQL-Anweisung auf :

INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers

Mit anderen Worten lautet der Pseudocode:

while (true)
{
    ADOConnection.Execute(sql);
}

Und innerhalb weniger Sekunden bekomme ich:

Violation of PRIMARY KEY constraint 'PK__Counting__07D9BBC343D61337'. 
Cannot insert duplicate key in object 'dbo.CountingNumbers'. 
The duplicate value is (1332)

Sind Aussagen atomar?

Die Tatsache, dass eine einzelne Aussage nicht atomar war, lässt mich fragen, ob einzelne Aussagen atomar sind.

Oder gibt es eine subtilere Definition von Anweisung , die sich (zum Beispiel) von der unterscheidet, die SQL Server als Anweisung betrachtet:

Geben Sie hier die Bildbeschreibung ein

Bedeutet dies grundsätzlich, dass SQL Server-Anweisungen innerhalb der Grenzen einer einzelnen T-SQL-Anweisung nicht atomar sind?

Und wenn eine einzelne Aussage atomar ist, was erklärt die Schlüsselverletzung?

Aus einer gespeicherten Prozedur heraus

Anstatt dass ein Remote-Client n Verbindungen öffnet , habe ich es mit einer gespeicherten Prozedur versucht:

CREATE procedure [dbo].[DoCountNumbers] AS

SET NOCOUNT ON;

DECLARE @bumpedCount int
SET @bumpedCount = 0

WHILE (@bumpedCount < 500) --safety valve
BEGIN
SET @bumpedCount = @bumpedCount+1;

PRINT 'Running bump '+CAST(@bumpedCount AS varchar(50))

INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers

IF (@bumpedCount >= 500)
BEGIN
    PRINT 'WARNING: Bumping safety limit of 500 bumps reached'
END
END

PRINT 'Done bumping process'

und öffnete 5 Registerkarten in SSMS, drückte jeweils F5 und sah zu, wie auch sie gegen ACID verstießen:

Running bump 414
Msg 2627, Level 14, State 1, Procedure DoCountNumbers, Line 14
Violation of PRIMARY KEY constraint 'PK_CountingNumbers'. 
Cannot insert duplicate key in object 'dbo.CountingNumbers'. 
The duplicate key value is (4414).
The statement has been terminated.

Der Fehler ist also unabhängig von ADO, ADO.net oder keinem der oben genannten.

Seit 15 Jahren gehe ich davon aus, dass eine einzelne Anweisung in SQL Server konsistent ist. und der einzige

Was ist mit TRANSACTION ISOLATION LEVEL xxx?

Für verschiedene Varianten des auszuführenden SQL-Batches:

  • Standard (Lese festgeschrieben) : Schlüsselverletzung

    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    
  • Standard (Lese festgeschrieben), explizite Transaktion : Keine Verletzung des Fehlerschlüssels

    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    
  • serialisierbar : Deadlock

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    
  • Snapshot (nach Änderung der Datenbank, um die Snapshot-Isolation zu aktivieren): Schlüsselverletzung

    SET TRANSACTION ISOLATION LEVEL SNAPSHOT
    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    

Bonus

  • Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64)
  • Standardtransaktionsisolationsstufe ( READ COMMITTED)

Es stellt sich heraus, dass jede Abfrage, die ich jemals geschrieben habe, fehlerhaft ist

Dies ändert sicherlich die Dinge. Jede Update-Anweisung, die ich jemals geschrieben habe, ist grundlegend kaputt. Z.B:

--Update the user with their last invoice date
UPDATE Users 
SET LastInvoiceDate = (SELECT MAX(InvoiceDate) FROM Invoices WHERE Invoices.uid = Users.uid)

Falscher Wert; weil eine andere Rechnung nach dem MAXund vor dem eingefügt werden könnte UPDATE. Oder ein Beispiel von BOL:

UPDATE Sales.SalesPerson
SET SalesYTD = SalesYTD + 
    (SELECT SUM(so.SubTotal) 
     FROM Sales.SalesOrderHeader AS so
     WHERE so.OrderDate = (SELECT MAX(OrderDate)
                           FROM Sales.SalesOrderHeader AS so2
                           WHERE so2.SalesPersonID = so.SalesPersonID)
     AND Sales.SalesPerson.BusinessEntityID = so.SalesPersonID
     GROUP BY so.SalesPersonID);

Ohne exklusive Holdlocks ist das SalesYTDfalsch.

Wie konnte ich all die Jahre etwas tun?

Ian Boyd
quelle
Was genau meinen Sie mit "Es gibt Fälle, in denen die falsche Transaktion" gebucht "wird; es wird tatsächlich eine spätere Transaktion ausgewählt." ?
Ypercubeᵀᴹ
Können Sie es unter SERIALIZABLE zum Scheitern bringen? Wenn Sie interessiert wären, den tatsächlichen Ausführungsplan nach einer erfolgreichen Aktualisierung zu sehen, verstehen Sie die Indizes in der Tabelle.
Aaron Bertrand
5
INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbersist unter UNCOMMITTED, COMMITTED, REPEATABLE READ und SERIALIZABLE nicht sicher, da SSperren erforderlich sind . Und ein SSchloss ist mit einem anderen SSchloss kompatibel . Andere gleichzeitige Anweisungen / Transaktionen können weiterhin dieselben Zeilen lesen. Um sicher zu gehen, benötigt dieser Ansatz eine SERIALIZABLE-Isolationsstufe plus XSperren oder XSperren + HOLDLOCK-Tabellenhinweise : INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers WITH(XLOCK, HOLDLOCK).
Bogdan Sahlean
3
Sie scheinen den allgemeinen Fehler zu machen, Atom und Isolation zu verwechseln. Atomic bedeutet nur, dass es als Einheit erfolgreich ist oder fehlschlägt (Transaktion alle festgeschrieben oder alle zurückgesetzt). Es sagt nichts über die Sichtbarkeit von Änderungen aus gleichzeitigen Transaktionen aus.
Martin Smith
3
@ BogdanSahlean - Nein, es bezieht sich auf den Titel der Frage. Weder Atomizität noch Konsistenz versprechen, was angenommen zu werden scheint. Atomic: Bedeutet nur, dass eine Einheit erfolgreich ist oder fehlschlägt und Consistent: Keine Einschränkungen usw. verletzt werden.
Martin Smith

Antworten:

22

Ich habe unter der Annahme gearbeitet, dass eine einzelne Anweisung in SQL Server konsistent ist

Diese Annahme ist falsch. Die folgenden zwei Transaktionen haben eine identische Sperrsemantik:

STATEMENT

BEGIN TRAN; STATEMENT; COMMIT

Kein Unterschied. Einzelne Anweisungen und automatische Festschreibungen ändern nichts.

Das Zusammenführen aller Logik zu einer Anweisung hilft also nicht (wenn ja, war es ein Zufall, weil sich der Plan geändert hat).

Lassen Sie uns das Problem beheben. SERIALIZABLEbehebt die Inkonsistenz, die angezeigt wird, da dadurch sichergestellt wird, dass sich Ihre Transaktionen so verhalten, als würden sie einzeln ausgeführt. Entsprechend verhalten sie sich so, als würden sie sofort ausgeführt.

Sie werden Deadlocks bekommen. Wenn Sie mit einer Wiederholungsschleife einverstanden sind, sind Sie an diesem Punkt fertig.

Wenn Sie mehr Zeit investieren möchten, wenden Sie Sperrhinweise an, um den exklusiven Zugriff auf die relevanten Daten zu erzwingen:

UPDATE Gifts  -- U-locked anyway
SET GivenAway = 1
WHERE GiftID = (
   SELECT TOP 1 GiftID
   FROM Gifts WITH (UPDLOCK, HOLDLOCK) --this normally just S-locks.
   WHERE g2.GivenAway = 0
    AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
   ORDER BY g2.GiftValue DESC
)

Sie sehen jetzt eine reduzierte Parallelität. Das kann je nach Belastung völlig in Ordnung sein.

Die Natur Ihres Problems macht es schwierig, Parallelität zu erreichen. Wenn Sie eine Lösung dafür benötigen, müssen wir invasivere Techniken anwenden.

Sie können das UPDATE etwas vereinfachen:

WITH g AS (
   SELECT TOP 1 Gifts.*
   FROM Gifts
   WHERE g2.GivenAway = 0
    AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
   ORDER BY g2.GiftValue DESC
)
UPDATE g  -- U-locked anyway
SET GivenAway = 1

Dadurch wird eine unnötige Verknüpfung beseitigt.

usr
quelle
2
Es ist etwas schrecklich zu lernen. Ich habe jahrelang einzelne SQL-Anweisungen geschrieben, unter der Annahme, dass das Endergebnis der Anweisung mit dem übereinstimmt, was ich verlangt habe. Beachten Sie auch, dass keine der Abfragen genau das ist, was das reale System ist (dies würde die Frage sehr komplex machen). Das Konzept, das ich untersuche und bei dem meine gesamte Weltanschauung auseinandergerissen wird, ist, ob eine Aussage in sich selbst konsistent ist. (Ich wusste immer, dass mehrere Anweisungen innerhalb eines Stapels oder mehrerer Stapel explizite Transaktionen erfordern).
Ian Boyd
@IanBoyd, also hast du nicht gedacht, dass die Isolationsstufe dabei eine Rolle spielt?
Ypercubeᵀᴹ
@ypercube Ich hatte die Rolle der Isolationsstufe in Betracht gezogen. aber ich habe nur READ COMMITTED in Betracht gezogen.
Ian Boyd
Nun, meine ganze Welt hat sich aufgelöst. Jede UPDATE- oder DELETE-Anweisung, die ich jemals geschrieben habe, ist grundsätzlich falsch. Ich nahm an, ich könnte nach Werten fragen und diese Werte für die Dauer der Anweisung korrekt haben.
Ian Boyd
2
@ IanBoyd stimmt, Sie müssen jetzt alles prüfen, was wichtig ist. Andererseits finde ich die Parallelität von Datenbanken in der Praxis nicht sehr problematisch. Aus irgendeinem Grund sind die meisten Abfragen nicht problematisch. Vielleicht, weil sie kalt sind und ich sie einfach SERIALISIERBAR machen kann. Manchmal können Sie die SNAPSHOT-Isolation verwenden, um eine konsistente Ansicht der Datenbank zu erhalten. Manchmal können Sie eine einfache Sperrhierarchie verwenden (Beispiel: Wenn eine Transaktion Kundendaten ändern möchte (möglicherweise eine Bestellung hinzufügen), wird zuerst die entsprechende Kundenzeile gesperrt. Dadurch werden Parallelitätsprobleme für einzelne Kunden beseitigt.).
usr
3

Unten finden Sie ein Beispiel für eine UPDATE-Anweisung, die einen Zählerwert atomar erhöht

-- Do this once for test setup
CREATE TABLE CountingNumbers (Value int PRIMARY KEY NOT NULL)
INSERT INTO CountingNumbers VALUES(1) 

-- Run this in parallel: start it in two tabs on SQL Server Management Studio
-- You will see each connection generating new numbers without duplicates and without timeouts
while (1=1)
BEGIN
  declare @nextNumber int
  -- Taking the Update lock is only relevant in case this statement is part of a larger transaction
  -- to prevent deadlock
  -- When executing without a transaction, the statement will itself be atomic
  UPDATE CountingNumbers WITH (UPDLOCK, ROWLOCK) SET @nextNumber=Value=Value+1
  print @nextNumber
END
Ries Vriend
quelle