Я вставляю в базу данных 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 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
Ответ 3
Сверху моей головы я чувствую, что один или несколько из этих столбцов принимают нули. Я хотел бы видеть инструкцию create для таблицы, включая ограничение.