Нарушение ограничения UNIQUE KEY для INSERT WHERE COUNT (*) = 0 в SQL Server 2005 - PullRequest
21 голосов
/ 24 сентября 2010

Я вставляю в базу данных 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. Какую модификацию я могу сделать?нужно сделать так, чтобы не было шансов на исключение из-за нарушения ограничения?

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

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

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

Редактировать 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 ]

48 голосов
/ 25 сентября 2010

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

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

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

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

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

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

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

К счастью, существует другой тип блокировки дляэтот точный сценарий называется блокировкой обновления (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 Books

4 голосов
/ 25 сентября 2010

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
1 голос
/ 24 сентября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...