web-dev-qa-db-fra.com

Une seule instruction SQL Server est-elle atomique et cohérente?

Une instruction dans SQL Server est-elle ACID?

Ce que je veux dire par là

Étant donné une seule instruction T-SQL, pas enveloppé dans un BEGIN TRANSACTION/COMMIT TRANSACTION, sont les actions de cette déclaration:

  • Atomic: soit toutes ses modifications de données sont effectuées, soit aucune n'est effectuée.
  • Cohérent: Une fois terminée, une transaction doit laisser toutes les données dans un état cohérent.
  • Isolé: Les modifications apportées par des transactions simultanées doivent être isolées des modifications apportées par toute autre transaction simultanée.
  • Durable: Une fois la transaction terminée, ses effets sont définitivement en place dans le système.

La raison pour laquelle je demande

J'ai une seule déclaration dans un système en direct qui semble violer les règles de la requête.

En effet, ma déclaration T-SQL est:

--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)

Note: Mais une variante conceptuelle plus simple pourrait être:

--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
)

Dans ces deux instructions, notez qu'il s'agit d'instructions uniques (UPDATE...SET...WHERE).

Il y a des cas où la mauvaise transaction est "réservée" ; il s'agit en fait de sélectionner une transaction ultérieure . Après avoir regardé ça pendant 16 heures, je suis perplexe. C'est comme si SQL Server violait simplement les règles.

Je me demandais si les résultats de la vue Slots changeaient avant la mise à jour? Que faire si SQL Server ne détient pas les verrous SHARED sur les transactions à ce date? Est-il possible qu'une seule déclaration puisse être incohérente?

J'ai donc décidé de le tester

J'ai décidé de vérifier si les résultats des sous-requêtes ou des opérations internes sont incohérents. J'ai créé une table simple avec une seule colonne int:

CREATE TABLE CountingNumbers (
   Value int PRIMARY KEY NOT NULL
)

À partir de plusieurs connexions, dans une boucle étroite, j'appelle la instruction T-SQL unique:

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

En d'autres termes, le pseudo-code est:

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

Et en quelques secondes, je reçois:

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

Les déclarations sont-elles atomiques?

Le fait qu'une seule déclaration n'était pas atomique me fait me demander si des déclarations uniques sont atomiques?

Ou existe-t-il une définition plus subtile de l'instruction , qui diffère de ( par exemple) ce que SQL Server considère comme une instruction:

enter image description here

Cela signifie-t-il fondamentalement que, dans le cadre d'une seule instruction T-SQL, les instructions SQL Server ne sont pas atomiques?

Et si une seule déclaration est atomique, qu'est-ce qui explique la violation clé?

Depuis une procédure stockée

Plutôt qu'un client distant ouvrant n connexions, je l'ai essayé avec une procédure stockée:

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'

et a ouvert 5 onglets dans SSMS, a appuyé sur F5 dans chacun, et a regardé comme ils violaient aussi ACID:

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.

L'échec est donc indépendant d'ADO, d'ADO.net ou de rien de ce qui précède.

Depuis 15 ans, j'opère sous l'hypothèse qu'une seule instruction dans SQL Server est cohérente; et le seul

Qu'en est-il du NIVEAU D'ISOLEMENT DE TRANSACTION xxx?

Pour que différentes variantes du batch SQL s'exécutent:

  • par défaut (lecture validée): violation de clé

    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    
  • par défaut (lecture validée), transaction explicitepas d'erreur violation de clé

    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    
  • sérialisable: blocage

    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
    
  • instantané (après avoir modifié la base de données pour activer l'isolement de l'instantané): violation de clé

    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
    

Prime

  • Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64)
  • Niveau d'isolement des transactions par défaut (READ COMMITTED)

Il s'avère que chaque requête que j'ai écrite est cassée

Cela change certainement les choses. Chaque déclaration de mise à jour que j'ai jamais écrite est fondamentalement cassée. Par exemple.:

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

Mauvaise valeur; car une autre facture pourrait être insérée après le MAX et avant le UPDATE. Ou un exemple de 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);

sans verrous exclusifs, le SalesYTD est faux.

Comment ai-je pu faire quoi que ce soit pendant toutes ces années.

64
Ian Boyd

J'ai fonctionné sous l'hypothèse qu'une seule instruction dans SQL Server est cohérente

Cette hypothèse est fausse. Les deux transactions suivantes ont une sémantique de verrouillage identique:

STATEMENT

BEGIN TRAN; STATEMENT; COMMIT

Aucune différence. Les instructions simples et les validations automatiques ne changent rien.

Donc, fusionner toute la logique en une seule instruction n'aide pas (si c'est le cas, c'était par accident parce que le plan a changé).

Corrigeons le problème à portée de main. SERIALIZABLE corrigera l'incohérence que vous voyez, car elle garantit que vos transactions se comportent comme si elles étaient exécutées de façon unique. De manière équivalente, ils se comportent comme s'ils s'exécutaient instantanément.

Vous obtiendrez des blocages. Si vous êtes d'accord avec une boucle de nouvelle tentative, vous avez terminé à ce stade.

Si vous souhaitez investir plus de temps, appliquez des conseils de verrouillage pour forcer l'accès exclusif aux données pertinentes:

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
)

Vous verrez maintenant une simultanéité réduite. Cela pourrait être tout à fait correct en fonction de votre charge.

La nature même de votre problème rend difficile l'accès simultané. Si vous avez besoin d'une solution pour cela, nous aurions besoin d'appliquer des techniques plus invasives.

Vous pouvez simplifier un peu la MISE À JOUR:

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

Cela supprime une jointure inutile.

18
usr

Ci-dessous est un exemple d'une instruction UPDATE qui incrémente atomiquement une valeur de compteur

-- 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
2
Ries Vriend