Является ли один оператор SQL Server атомарным и согласованным?


это оператор в SQL Server ACID?

что я имею в виду

учитывая один оператор T-SQL, не завернутый в BEGIN TRANSACTION/COMMIT TRANSACTION, являются ли действия этого утверждения:

  • Atomic: либо все его изменения данных выполняются, либо ни один из них не выполняется.
  • последовательный: после завершения транзакции все данные должны быть согласованы государство.
  • изоляции: изменения, внесенные параллельными транзакциями, должны быть изолированы от изменений, внесенных другими параллельными транзакциями.
  • прочный: после завершения транзакции ее эффекты постоянно присутствуют в системе.

почему я спрашиваю

у меня есть один оператор в живой системе, который, как представляется, нарушает правила запроса.

In эффект мой оператор T-SQL:

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

Примечание: но более простой концептуальный вариант может быть:

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

в обоих этих утверждениях обратите внимание, что они являются одиночными утверждениями (UPDATE...SET...WHERE).

бывают случаи, когда происходит неправильная транзакция "заказали"; это на самом деле выбор позже сделки. После того, как я смотрел на это в течение 16 часов, я в тупике. Это как будто SQL Server просто нарушает правила.

я задавался вопросом, что если результаты Slots вид меняется до обновления происходит? Что делать, если SQL Server не удерживает SHARED замки на сделки на дата? Возможно ли, что одно утверждение может быть непоследовательным?

поэтому я решил проверить его

я решил проверить, являются ли результаты подзапросов или внутренних операций несовместимыми. Я создал простую таблицу с одним int:

CREATE TABLE CountingNumbers (
   Value int PRIMARY KEY NOT NULL
)

из нескольких соединений, в плотном цикле, я называю один оператор T-SQL:

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

другими словами псевдо-код:

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

и в течение нескольких секунд я получаю:

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

являются ли утверждения атомарными?

тот факт, что одно утверждение не было атомарным, заставляет меня задаться вопросом, являются ли отдельные утверждения атомарными?

или есть еще тонкие определение сообщении, что отличается от (например) того, что SQL Server считает оператором:

означает ли это принципиально, что в пределах одного оператора T-SQL операторы SQL Server не являются атомарными?

и если один оператор является атомарным, что объясняет ключевое нарушение?

из хранимой процедуры

, а не открытие удаленного клиента n соединения, я попробовал это с помощью хранимой процедуры:

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'

и открыл 5 вкладок в SSMS, нажал F5 в каждой, и смотрел как они тоже нарушили кислоту:

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.

таким образом, отказ не зависит от ADO, ADO.net или ничего из вышеперечисленного.

в течение 15 лет я работал в предположении, что один оператор в SQL Server является последовательным; и только

как насчет уровня изоляции транзакций ХХХ?

для различных вариантов выполнения пакета SQL:

  • default (read committed): ключевые нарушения

    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    
  • default (read committed), явная транзакция:ошибка ключевые нарушении

    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    
  • сериализуемые: взаимоблокировки

    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
    
  • снимок (после изменения базы данных для включения изоляции моментальных снимков): нарушение ключа

    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
    

бонус

  • Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64)
  • уровень изоляции транзакций по умолчанию (READ COMMITTED)

оказывается, каждый запрос, который я когда-либо писал, сломан

это конечно все меняет. Каждое заявление об обновлении, которое я когда-либо писал, принципиально нарушено. Например:

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

неверное значение, потому что другой счет мог быть вставлен после MAX и до UPDATE. Или пример из бол:

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

без эксклюзивных удерживающих устройств,SalesYTD - это неправильно.

как я был в состоянии делать все эти годы.

2 53

2 ответа:

я работал в предположении, что один оператор в SQL Server является последовательным

это предположение неверно. Следующие две транзакции имеют одинаковую семантику блокировки:

STATEMENT

BEGIN TRAN; STATEMENT; COMMIT

никакой разницы. Один заявления и автоматически совершает ничего не меняют.

давайте исправим проблема под рукой. SERIALIZABLE исправит несоответствие, которое вы видите, потому что это гарантирует, что ваши транзакции ведут себя так, как если бы они выполнялись однопоточно. Эквивалентно, они ведут себя так, как будто они выполняются мгновенно.

вы будете получать тупики. Если вы в порядке с циклом повтора, вы закончили на этом этапе.

если вы хотите инвестировать больше времени, применять подсказки в силу монопольного доступа к соответствующим данным:

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
)

теперь вы увидите уменьшенный совпадение. Это может быть совершенно нормально, в зависимости от вашей нагрузки.

сама природа вашей проблемы затрудняет достижение параллелизма. Если вам требуется решение для этого, нам нужно будет применить более инвазивные методы.

вы можете немного упростить обновление:

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

Это избавляет от одного ненужного соединения.

Ниже приведен пример инструкции UPDATE, которая увеличивает значение счетчика атомарно

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