Нарушение ограничения 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 комбинация из 3 полей должна быть уникальной.

Мои вопросы:

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

Обратите внимание, что я знаю, что есть другие подходы к решению исходной проблемы "ВСТАВИТЬ, если не существует", такие как (в итоге):

  • Использование TRY CATCH
  • IF NOT EXIST INSERT (внутри транзакции с сериализуемой изоляцией)

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

Редактировать 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", предложенную в связанном вопросе ( ссылка). Хотя мне все еще интересно, почему оригинальная реализация не работает.

3 ответа

Решение

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

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

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

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

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

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

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

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

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

Две одновременные попытки вставить одну и ту же строку в таблицу теперь будут сериализованы в начальном операторе выбора, поскольку это получает (и удерживает) блокировку 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

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

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

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

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

Единственный раз, когда будут приняты конфликтующие блокировки, находится на Insert этап.

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

Создать табличный скрипт

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. Запишите спиды соединений (x и y) и настройте трассировку SQL Profiler, фиксирующую события блокировки и сообщения об ошибках пользователя. Примените фильтры spid=x или y и severity = 0, а затем выполните оба сценария.

Вставить скрипт

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 FieldA=@FieldA, FieldB=@FieldB, FieldC=@FieldC
        WHERE (SELECT COUNT(*) FROM MyTable WHERE FieldA=@FieldA AND FieldB=@FieldB AND FieldC=@FieldC ) = 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 SPID!=@@SPID AND CONTEXT_INFO = (SELECT CONTEXT_INFO FROM sys.sysprocesses WHERE SPID=@@SPID)
    EXEC ( @killspid )
END CATCH

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

Другие вопросы по тегам