Поскольку вы используете последовательность, вы можете использовать ту же функцию СЛЕДУЮЩЕЕ ЗНАЧЕНИЕ ДЛЯ - которую вы уже использовали в Ограничении по умолчанию в Id
поле Первичный ключ - чтобы Id
заранее сгенерировать новое значение. Генерация значения вначале означает, что вам не нужно беспокоиться о его отсутствии SCOPE_IDENTITY
, а затем означает, что вам не нужно ни OUTPUT
предложение, ни дополнительные операции SELECT
для получения нового значения; у вас будет значение, прежде чем вы сделаете INSERT
, и вам даже не нужно возиться SET IDENTITY INSERT ON / OFF
:-)
Так что это берет на себя часть общей ситуации. Другая часть обрабатывает проблему одновременности двух процессов, в то же самое время, не находя существующую строку для точно такой же строки, и продолжая с INSERT
. Обеспокоенность заключается в том, чтобы избежать возможного нарушения Уникальных ограничений.
Один из способов решения этих типов проблем параллелизма - заставить эту конкретную операцию быть однопоточной. Способ сделать это - использовать блокировки приложений (которые работают между сеансами). Хотя они эффективны, они могут быть немного сложными в такой ситуации, когда частота столкновений, вероятно, довольно низкая.
Другой способ справиться с коллизиями - признать, что они иногда случаются, и справиться с ними, а не пытаться их избегать. Используя TRY...CATCH
конструкцию, вы можете эффективно перехватить конкретную ошибку (в данном случае: «нарушение уникального ограничения», Msg 2601) и повторно выполнить ее, SELECT
чтобы получить Id
значение, поскольку мы знаем, что оно теперь существует из-за нахождения в CATCH
блоке с этим конкретным ошибка. Другие ошибки могут быть обработаны типичным RAISERROR
/ RETURN
или THROW
способом.
Настройка теста: последовательность, таблица и уникальный индекс
USE [tempdb];
CREATE SEQUENCE dbo.MagicNumber
AS INT
START WITH 1
INCREMENT BY 1;
CREATE TABLE dbo.NameLookup
(
[Id] INT NOT NULL
CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
[ItemName] NVARCHAR(50) NOT NULL
);
CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
ON dbo.NameLookup ([ItemName]);
GO
Настройка теста: хранимая процедура
CREATE PROCEDURE dbo.GetOrInsertName
(
@SomeName NVARCHAR(50),
@ID INT OUTPUT,
@TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;
BEGIN TRY
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName
AND @TestRaceCondition = 0;
IF (@ID IS NULL)
BEGIN
SET @ID = NEXT VALUE FOR dbo.MagicNumber;
INSERT INTO dbo.NameLookup ([Id], [ItemName])
VALUES (@ID, @SomeName);
END;
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
BEGIN
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName;
END;
ELSE
BEGIN
;THROW; -- SQL Server 2012 or newer
/*
DECLARE @ErrorNumber INT = ERROR_NUMBER(),
@ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
RETURN;
*/
END;
END CATCH;
GO
Тест
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT,
@TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO
Вопрос от ОП
Почему это лучше, чем MERGE
? Не получу ли я такую же функциональность без TRY
использования WHERE NOT EXISTS
пункта?
MERGE
имеет различные "проблемы" (несколько ссылок связаны в ответе @ SqlZim, поэтому нет необходимости дублировать эту информацию здесь). И в этом подходе нет дополнительной блокировки (меньше конфликтов), поэтому он должен быть лучше при параллельности. При таком подходе вы никогда не получите нарушение уникальных ограничений, все без такового HOLDLOCK
и т. Д. Это в значительной степени гарантированно сработает.
Обоснование этого подхода:
- Если у вас достаточно выполнений этой процедуры, так что вам нужно беспокоиться о столкновениях, то вам не нужно:
- предпринять больше шагов, чем необходимо
- удерживать блокировки на любых ресурсах дольше, чем необходимо
- Поскольку коллизии могут происходить только при появлении новых записей (новых записей, поданных в одно и то же время ), частота попадания в
CATCH
блок в первую очередь будет довольно низкой. Имеет больше смысла оптимизировать код, который будет выполняться в 99% случаев вместо кода, который будет выполняться в 1% случаев (если нет затрат на оптимизацию обоих, но здесь это не так).
Комментарий от ответа @ SqlZim (выделение добавлено)
Я лично предпочитаю попробовать и адаптировать решение, чтобы избежать этого, когда это возможно . В этом случае я не чувствую, что использование блокировок serializable
- это сложный подход, и я был бы уверен, что он хорошо справится с высоким параллелизмом.
Я согласился бы с этим первым предложением, если бы в него были внесены поправки с указанием «и _при благоразумии». То, что что-то технически возможно, не означает, что ситуация (т.е. предполагаемый вариант использования) будет выиграна.
Проблема, которую я вижу с этим подходом, заключается в том, что он блокирует больше, чем предлагается. Важно перечитать процитированную документацию на «сериализуемость», в частности следующее (выделение добавлено):
- Другие транзакции не могут вставлять новые строки со значениями ключей, которые попадают в диапазон ключей, считываемых какими-либо инструкциями в текущей транзакции, пока текущая транзакция не завершится.
Теперь вот комментарий в примере кода:
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
Оперативное слово там - «диапазон». Используемая блокировка зависит не только от значения в @vName
, но, точнее, от диапазона, начинающегося сместо, куда должно идти это новое значение (т. е. между существующими значениями ключа по обе стороны от того, где подходит новое значение), но не само значение. Это означает, что другие процессы будут заблокированы от вставки новых значений, в зависимости от значений, которые в настоящее время ищутся. Если поиск выполняется в верхней части диапазона, вставка чего-либо, что могло бы занять ту же самую позицию, будет заблокирована. Например, если существуют значения «a», «b» и «d», то, если один процесс выполняет SELECT для «f», будет невозможно вставить значения «g» или даже «e» ( так как любой из тех, кто придет сразу после "d"). Но вставка значения «c» будет возможна, поскольку она не будет помещена в «зарезервированный» диапазон.
Следующий пример должен иллюстрировать это поведение:
(На вкладке запроса (т.е. сеанс) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');
BEGIN TRAN;
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'test8';
--ROLLBACK;
(На вкладке запроса (т.е. сеанс) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Аналогично, если значение «C» существует и значение «A» выбирается (и, следовательно, блокируется), вы можете вставить значение «D», но не значение «B»:
(На вкладке запроса (т.е. сеанс) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');
BEGIN TRAN
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'testA';
--ROLLBACK;
(На вкладке запроса (т.е. сеанс) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Чтобы быть справедливым, в моем предложенном подходе, когда есть исключение, в журнале транзакций будет 4 записи, которых не будет в этом подходе «сериализуемой транзакции». НО, как я уже говорил выше, если исключение происходит 1% (или даже 5%) времени, это оказывает гораздо меньшее влияние, чем гораздо более вероятный случай первоначального SELECT, временно блокирующего операции INSERT.
Другая, хотя и незначительная, проблема с этим подходом «сериализуемая транзакция + предложение OUTPUT» заключается в том, что OUTPUT
предложение (в его нынешнем использовании) отправляет данные обратно как набор результатов. Набор результатов требует больше накладных расходов (вероятно, с обеих сторон: в SQL Server для управления внутренним курсором и на уровне приложения для управления объектом DataReader), чем в виде простого OUTPUT
параметра. Учитывая, что мы имеем дело только с одним скалярным значением, и что предполагается высокая частота выполнения, эта дополнительная нагрузка на набор результатов, вероятно, складывается.
Хотя это OUTPUT
предложение можно использовать таким образом, чтобы возвращать OUTPUT
параметр, для этого потребуются дополнительные шаги для создания временной таблицы или табличной переменной, а затем выбора значения из этой временной таблицы / табличной переменной в OUTPUT
параметре.
Дополнительное разъяснение: Ответ на ответ @ SqlZim (обновленный ответ) на мой Ответ на ответ @ SqlZim (в исходном ответе) на мое утверждение относительно параллелизма и производительности ;-)
Извините, если эта часть немного длинна, но на данный момент мы просто до нюансов двух подходов.
Я полагаю, что способ представления информации может привести к ложным предположениям о количестве блокировок, которые можно ожидать при использовании serializable
в сценарии, как представлено в исходном вопросе.
Да, я признаю, что я предвзят, хотя и справедливо
- Для человека невозможно быть предвзятым, по крайней мере, до некоторой степени, и я стараюсь держать его как минимум,
- Приведенный пример был упрощенным, но это было в иллюстративных целях передать поведение, не усложняя его. Подразумевать чрезмерную частоту не планировалось, хотя я понимаю, что я также явно не утверждал иначе, и это можно было бы прочитать как означающее большую проблему, чем на самом деле. Я постараюсь уточнить это ниже.
- Я также включил пример блокировки диапазона между двумя существующими ключами (второй набор блоков «Query tab 1» и «Query tab 2»).
- Я нашел (и добровольно предложил) «скрытую стоимость» моего подхода, состоящую из четырех дополнительных записей в журнале Tran каждый раз, когда происходит
INSERT
сбой из-за нарушения уникального ограничения. Я не видел, что упомянуто ни в одном из других ответов / сообщений.
Относительно подхода @ gbn «JFDI», поста Майкла Дж. Сварта «Гадкий прагматизм для победы» и комментария Аарона Бертранда к посту Майкла (относительно его тестов, показывающих, какие сценарии снизили производительность), и вашего комментария к вашей «адаптации Майкла Дж. Адаптация Стюартом процедуры Try Catch JFDI в @ gbn:
Если вы вставляете новые значения чаще, чем выбираете существующие, это может быть более производительным, чем версия @ srutzky. В противном случае я бы предпочел версию @ srutzky этой.
Что касается обсуждения gbn / Michael / Aaron, связанного с подходом "JFDI", было бы неправильно приравнивать мое предложение к подходу gbn "JFDI". Из-за характера операции «Получить или вставить» существует явная необходимость сделать это SELECT
для получения ID
значения для существующих записей. Этот SELECT действует как IF EXISTS
проверка, что делает этот подход более приравниваемым к варианту "CheckTryCatch" тестов Аарона. Переписанный код Майкла (и ваша последняя адаптация адаптации Майкла) также включает в себя WHERE NOT EXISTS
, чтобы сначала выполнить ту же проверку. Следовательно, мое предложение (вместе с окончательным кодом Майкла и вашей адаптацией его окончательного кода) на самом деле не будет так CATCH
часто встречаться. Это могут быть только ситуации, когда два сеанса,ItemName
INSERT...SELECT
в один и тот же момент, так что оба сеанса получают «истину» для WHERE NOT EXISTS
одного и того же момента и, таким образом, оба пытаются сделать это INSERT
в один и тот же момент. Этот очень специфический сценарий происходит гораздо реже, чем выбор существующего ItemName
или вставка нового, ItemName
когда никакой другой процесс не пытается сделать это в тот же момент .
С учетом всего вышесказанного: почему я предпочитаю свой подход?
Во-первых, давайте посмотрим, какая блокировка происходит в «сериализуемом» подходе. Как упомянуто выше, «диапазон», который блокируется, зависит от существующих значений ключа по обе стороны от того, где будет соответствовать новое значение ключа. Начало или конец диапазона также может быть началом или концом индекса, соответственно, если в этом направлении не существует ключевого значения. Предположим, у нас есть следующий индекс и ключи ( ^
представляет начало индекса, а $
представляет его конец):
Range #: |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value: ^ C F J $
Если сеанс 55 пытается вставить значение ключа:
A
, тогда диапазон # 1 (от ^
до C
) блокируется: сеанс 56 не может вставить значение B
, даже если он уникален и действителен (пока). Но сессия 56 может вставить значения D
, G
и M
.
D
, тогда диапазон # 2 (от C
до F
) блокируется: сеанс 56 не может вставить значение E
(пока). Но сессия 56 может вставить значения A
, G
и M
.
M
, тогда диапазон # 4 (от J
до $
) заблокирован: сеанс 56 не может вставить значение X
(пока). Но сессия 56 может вставить значения A
, D
и G
.
По мере добавления большего количества ключевых значений диапазоны между ключевыми значениями становятся более узкими, что снижает вероятность / частоту одновременного ввода нескольких значений в одном и том же диапазоне. По общему признанию, это не главная проблема, и к счастью, это, кажется, проблема, которая фактически уменьшается со временем.
Проблема с моим подходом была описана выше: это происходит только тогда, когда два сеанса пытаются вставить одно и то же значение ключа одновременно. В этом отношении все сводится к тому, что имеет более высокую вероятность возникновения: два разных, но близких, значения ключа предпринимаются в одно и то же время, или одно и то же значение ключа вводится в одно и то же время? Я полагаю, что ответ заключается в структуре приложения, выполняющего вставки, но, вообще говоря, я бы предположил, что более вероятно, что будут вставлены два разных значения, которые, как оказалось, совместно используют один и тот же диапазон. Но единственный способ узнать это - это протестировать обе системы OP.
Далее давайте рассмотрим два сценария и то, как каждый подход обрабатывает их:
Все запросы на уникальные значения ключа:
В этом случае CATCH
блок в моем предложении никогда не вводится, следовательно, нет «проблемы» (т.е. 4 записи журнала и время, необходимое для этого). Но в подходе «сериализации», даже если все вставки уникальны, всегда будет некоторый потенциал для блокировки других вставок в том же диапазоне (хотя и не очень долго).
Частота запросов на одно и то же значение ключа одновременно:
В этом случае - очень низкая степень уникальности с точки зрения входящих запросов на несуществующие значения ключа - CATCH
блок в моем предложении будет вводиться регулярно. Результатом этого будет то, что при каждой неудачной вставке потребуется автоматический откат и запись 4 записей в журнал транзакций, что каждый раз приводит к небольшому снижению производительности. Но общая операция никогда не должна выходить из строя (по крайней мере, не из-за этого).
(Была проблема с предыдущей версией «обновленного» подхода, которая позволяла ему страдать от взаимоблокировок. Для updlock
решения этой проблемы была добавлена подсказка, и она больше не получает взаимоблокировки.)НО, в «сериализуемом» подходе (даже в обновленной, оптимизированной версии) операция будет тупиковой. Почему? Потому что serializable
поведение предотвращает только INSERT
операции в диапазоне, который был прочитан и, следовательно, заблокирован; это не мешает SELECT
операциям в этом диапазоне.
serializable
Подход, в этом случае, казалось бы , не имеют каких - либо дополнительных накладных расходов, и может выполнять несколько лучше , чем то , что я предлагаю.
Как и во многих / большинстве дискуссий, касающихся производительности, из-за того, что на результат влияет так много факторов, единственный способ по-настоящему понять, как что-то будет работать, - это опробовать его в целевой среде, в которой оно будет работать. На этом этапе это не будет вопросом мнения :).