Нарушение ограничения UNIQUE KEY для INSERT WHERE COUNT (*) = 0 на SQL Server 2005

Я вставляю в базу данных SQL из нескольких процессов. Вероятно, что процессы иногда пытаются вставить дубликаты данных в таблицу. Я попытался написать запрос таким образом, чтобы обрабатывать дубликаты, но я все равно получаю:

System.Data.SqlClient.SqlException: Violation of UNIQUE KEY constraint 'UK1_MyTable'. Cannot insert duplicate key in object 'dbo.MyTable'.
The statement has been terminated.

Мой запрос выглядит примерно так:

INSERT INTO MyTable (FieldA, FieldB, FieldC)
SELECT FieldA='AValue', FieldB='BValue', FieldC='CValue'
WHERE (SELECT COUNT(*) FROM MyTable WHERE FieldA='AValue' AND FieldB='BValue' AND FieldC='CValue' ) = 0

Ограничение "UK1_MyConstraint" говорит, что в MyTable комбинация из трех полей должна быть уникальной.

Мои вопросы:

  • Почему это не работает?
  • Какую модификацию мне нужно сделать, чтобы исключить исключение из-за нарушения ограничения?

Обратите внимание, что я знаю, что существуют другие подходы к решению исходной проблемы "INSERT if not exists", например (в резюме):

  • Использование TRY CATCH
  • ЕСЛИ НЕ СУЩЕСТВУЕТ ВСТАВИТЬ (внутри транзакции с сериализуемой изоляцией)

Должен ли я использовать один из подходов?

Изменить 1 SQL для создания таблицы:

CREATE TABLE [dbo].[MyTable](
  [Id] [bigint] IDENTITY(1,1) NOT NULL,
  [FieldA] [bigint] NOT NULL,
  [FieldB] [int] NOT NULL,
  [FieldC] [char](3) NULL,
  [FieldD] [float] NULL,
  CONSTRAINT [PK_MyTable] PRIMARY KEY NONCLUSTERED 
  (
    [Id] ASC
  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON),
  CONSTRAINT [UK1_MyTable] UNIQUE NONCLUSTERED 
  (
    [FieldA] ASC,
    [FieldB] ASC,
    [FieldC] ASC
  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
)

Изменить 2 Решение:

Просто обновить это - я решил использовать реализацию JFDI, предложенную в связанном вопросе (ссылка). Хотя мне все еще интересно, почему исходная реализация не работает.

Ответ 1

Почему это не работает?

Я считаю, что поведение SQL Server по умолчанию заключается в том, чтобы освобождать блокировки совместного доступа, как только они больше не нужны. Ваш подзапрос приведет к недолговечной общей (S) блокировке в таблице, которая будет выпущена сразу после завершения суб-запроса.

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

Какую модификацию мне нужно сделать, чтобы исключить исключение из-за нарушения ограничения?

Добавление подсказки HOLDLOCK к вашему подзапросу даст указание SQL Server удерживать блокировку до завершения транзакции. (В вашем случае это неявная транзакция.) Подсказка HOLDLOCK эквивалентна подсказке SERIALIZABLE, которая сама по себе эквивалентна сериализуемому уровню изоляции транзакций, который вы ссылаетесь в своем списке "других подходов".

Подсказка HOLDLOCK будет достаточной для сохранения блокировки S и предотвращения параллельной транзакции от вставки строки, против которой вы защищаете. Однако вы, скорее всего, найдете свою уникальную ошибку нарушения ключа, замененную тупиковыми ошибками, происходящими на той же частоте.

Если вы сохраняете только блокировку S в таблице, рассмотрите гонку между двумя одновременными попытками вставить одну и ту же строку, продолжая в стоп-стоп - оба успеха в приобретении блокировки S на таблице, но ни одна из них не может преуспеть в получение блокировки Exclusive (X), требуемой для выполнения вставки.

К счастью, для этого точного сценария существует другой тип блокировки, называемый блокировкой Update (U). Блокировка U идентична блокировке S со следующей разницей: в то время как одновременное одновременное одновременное удерживание нескольких S-замков на одном и том же ресурсе может одновременно удерживаться только одной блокировкой U. (Говоря иначе, в то время как S-замки совместимы друг с другом (т.е. Могут сосуществовать без конфликтов), блокировки U несовместимы друг с другом, но могут сосуществовать рядом с S-замками, а далее по спектру блокировки Exclusive (X) не являются совместим с S или U замками)

Вы можете обновить неявный S-блокировку вашего подзапроса до блокировки U с помощью подсказки UPDLOCK.

Две параллельные попытки вставить одну и ту же строку в таблицу теперь будут сериализованы в начале операции select, поскольку она приобретает (и удерживает) блокировку U, которая несовместима с другой блокировкой U от попытки одновременной вставки.

значения NULL

Отдельная проблема может возникнуть из-за того, что FieldC допускает значения NULL.

Если ANSI_NULLS включен (по умолчанию), проверка равенства FieldC=NULL вернет значение false даже в том случае, если FieldC равен NULL (вы должны использовать оператор IS NULL для проверки нулевого значения, когда ANSI_NULLS включен). Поскольку FieldC имеет значение NULL, ваша двойная проверка не будет работать при вставке значения NULL.

Чтобы правильно обрабатывать нули, вам нужно будет изменить подзапрос EXISTS, чтобы использовать оператор IS NULL, а не =, когда вставлено значение NULL. (Или вы можете изменить таблицу, чтобы запретить NULL во всех соответствующих столбцах.)

Ссылки на электронные книги SQL Server

Ответ 2

RE: "Мне все еще интересно, почему исходная реализация не работает".

Почему это сработает?

Что необходимо для предотвращения чередования двух параллельных транзакций следующим образом?

Tran A                                Tran B
---------------------------------------------
SELECT COUNT(*)...
                                  SELECT COUNT(*)...
INSERT ....
                                  INSERT... (duplicate key violation).

Единственные временные конфликтующие блокировки будут выполняться на этапе Insert.

Чтобы увидеть это в SQL Profiler

Создать таблицу Script

create table MyTable
(
FieldA int NOT NULL, 
FieldB int NOT NULL, 
FieldC int NOT NULL
)
create unique nonclustered index ix on  MyTable(FieldA, FieldB, FieldC)

Затем вставьте нижеследующее в два разных окна SSMS. Обратите внимание на spids соединений (x и y) и настройте трассировку SQL Profiler, фиксируя события блокировки и сообщения об ошибках пользователя. Примените фильтры spid = x или y и severity = 0, а затем выполните оба сценария.

Вставить Script

DECLARE @FieldA INT, @FieldB INT, @FieldC INT
SET NOCOUNT ON
SET CONTEXT_INFO 0x696E736572742074657374

BEGIN TRY
WHILE 1=1
    BEGIN

        SET @FieldA=( (CAST(GETDATE() AS FLOAT) - FLOOR(CAST(GETDATE() AS FLOAT))) * 24 * 60 * 60 * 300)
        SET @FieldB = @FieldA
        SET @FieldC = @FieldA

        RAISERROR('beginning insert',0,1) WITH NOWAIT
        INSERT INTO MyTable (FieldA, FieldB, FieldC)
        SELECT [email protected], [email protected], [email protected]
        WHERE (SELECT COUNT(*) FROM MyTable WHERE [email protected] AND [email protected] AND [email protected] ) = 0
    END
END TRY
BEGIN CATCH
    DECLARE @message VARCHAR(500)
    SELECT @message = 'in catch block ' + ERROR_MESSAGE()
    RAISERROR(@message,0,1) WITH NOWAIT
    DECLARE @killspid VARCHAR(10) 
    SELECT @killspid = 'kill ' +CAST(SPID AS VARCHAR(4)) FROM sys.sysprocesses WHERE [email protected]@SPID AND CONTEXT_INFO = (SELECT CONTEXT_INFO FROM sys.sysprocesses WHERE [email protected]@SPID)
    EXEC ( @killspid )
END CATCH

Ответ 3

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