Is a single SQL Server statement atomic and sequential?

Is the expression in SQL Server ACID ?

What do I mean by this

Given one T-SQL statement not enclosed in BEGIN TRANSACTION / COMMIT TRANSACTION , the actions of this statement are:

  • Atomic : either all of its data modifications are executed, or none of them are executed.
  • Agreed . Upon completion, the transaction should leave all data in a consistent state.
  • Isolated . Modifications performed by concurrent transactions must be isolated from changes made by any other concurrent transactions.
  • Durable : after a transaction is completed, its effects are permanently installed in the system.

The reason I ask

I have one statement in a live system that apparently violates the query rules.

Actually my T-SQL statement:

 --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 currently unbooked AND Slots.Available > 0 --only book it if there empty slots ORDER BY t2.CreatedDate) 

Note But a simpler conceptual option might be:

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

In both of these statements, note that they are single statements ( UPDATE...SET...WHERE ).

There are cases when the wrong transaction is “booked”; he actually picks a later transaction. After looking at it for 16 hours, I'm at a standstill. As if SQL Server was just breaking the rules.

Interestingly, if the results of the Slots view change before the update occurs? What if SQL Server does not hold SHARED in transactions on this date ? Is it possible that one operator may be inconsistent?

So I decided to check it out

I decided to check if the results of the subqueries or internal operations are not consistent. I created a simple table with one int column:

 CREATE TABLE CountingNumbers ( Value int PRIMARY KEY NOT NULL ) 

Out of several closed loop connections, I call one T-SQL expression :

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

In other words, pseudo code:

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

And after a few seconds I get:

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

Are statements atomic?

The fact that one statement was not an atom makes me wonder if single statements are atomic.

Or there is a more subtle statement definition that differs from (for example) in what SQL Server considers an expression:

enter image description here

Does this basically mean that SQL Server statements are not atomic in a single T-SQL statement?

And if one operator is atomic, what explains the key violation?

From stored procedure

Instead of remote clients opening n connections, I tried it with a stored procedure:

 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' 

and opened 5 tabs in SSMS, pressed F5 in each and watched how they too violated the 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. 

Thus, the refusal does not depend on ADO, ADO.net or on any of the above.

For 15 years, I have been working on the assumption that one statement in SQL Server is consistent; only

What about the INSULATION LEVEL of OPERATION xxx?

For different variants of an executable SQL package:

  • default (read read) : key violation

     INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers 
  • default value (read read), explicit transaction : no error

     BEGIN TRANSACTION INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers COMMIT TRANSACTION 
  • serializable : dead end

     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 (after changing the database to enable highlighting): key violation

     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)
  • Default Transaction Isolation Level ( READ COMMITTED )

Every request I ever wrote is broken

This, of course, changes the situation. Every update I've ever written is fundamentally broken. For example:.

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

Incorrect value; because after MAX and before UPDATE another invoice can be inserted. Or an example from 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); 

Without exclusive holdlocks, SalesYTD is incorrect.

How could I do all these years.

+43
sql-server sql-server-2008-r2
Jan 30 '14 at 22:26
source share
2 answers

I worked on the assumption that a single statement in SQL Server is consistent

This assumption is incorrect. The following two transactions have identical locking semantics:

 STATEMENT BEGIN TRAN; STATEMENT; COMMIT 

No difference. Single statements and auto commands do not change anything.

Thus, combining all the logic into one operator does not help (if so, it was an accident because the plan changed).

Let me fix this problem. SERIALIZABLE captures the inconsistency that you see because it ensures that your transactions behave as if they were running single-threaded. Equivalently, they behave as if they are executed instantly.

You will get dead ends. If you are OK with the repeat loop, you are done at that point.

If you want to invest more time, use blocking tips to force exclusive access to the relevant data:

 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 ) 

You will now see reduced concurrency. This can be perfectly normal depending on your workload.

The very nature of your problem makes achieving concurrency difficult. If you need a solution for this, we need to use more invasive methods.

You can simplify UPDATE a bit:

 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 

This eliminates unnecessary connection.

+11
Jan 30 '14 at 10:50
source share
— -

The following is an example of an UPDATE statement that increments a counter value atomically

 -- 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
Jul 30 '15 at 21:09
source share



All Articles