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 Slots
passiert , wenn sich die Ergebnisse der Ansicht ändern, bevor das Update durchgeführt wird. Was ist, wenn SQL Server SHARED
die 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 int
Spalte 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:
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 :
KeineVerletzung desFehlerschlüsselsBEGIN 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 MAX
und 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 SalesYTD
falsch.
Wie konnte ich all die Jahre etwas tun?