Мы часто сталкиваемся с ситуацией «Если не существует, вставьте». Блог Дэна Гусмана содержит отличное исследование о том, как сделать этот процесс безопасным.
У меня есть базовая таблица, которая просто каталогизирует строку в целое число из SEQUENCE
. В хранимой процедуре мне нужно либо получить целочисленный ключ для значения, если оно существует, либо INSERT
затем получить полученное значение. Для dbo.NameLookup.ItemName
столбца существует ограничение уникальности, поэтому целостность данных не подвергается риску, но я не хочу встречать исключения.
Это не IDENTITY
так, я не могу получить, SCOPE_IDENTITY
и значение может быть NULL
в определенных случаях.
В моей ситуации мне нужно иметь дело только с INSERT
безопасностью на столе, поэтому я пытаюсь решить, лучше ли это использовать MERGE
следующим образом:
SET NOCOUNT, XACT_ABORT ON;
DECLARE @vValueId INT
DECLARE @inserted AS TABLE (Id INT NOT NULL)
MERGE
dbo.NameLookup WITH (HOLDLOCK) AS f
USING
(SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
ON f.ItemName= new_item.val
WHEN MATCHED THEN
UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
INSERT
(ItemName)
VALUES
(@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s
Я мог бы сделать это без использования MERGE
только условного INSERT
выражения, за которым следует, SELECT
я думаю, что этот второй подход более понятен для читателя, но я не уверен, что это «лучшая» практика
SET NOCOUNT, XACT_ABORT ON;
INSERT INTO
dbo.NameLookup (ItemName)
SELECT
@vName
WHERE
NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)
DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName
Или, возможно, есть другой лучший способ, который я не рассматривал
Я сделал поиск и ссылки на другие вопросы. Это: /programming/5288283/sql-server-insert-if-not-exists-best-practice - наиболее подходящий вариант, который я могу найти, но он не очень подходит для моего варианта использования. Другие вопросы к IF NOT EXISTS() THEN
подходу, которые я не считаю приемлемыми.
Ответы:
Поскольку вы используете последовательность, вы можете использовать ту же функцию СЛЕДУЮЩЕЕ ЗНАЧЕНИЕ ДЛЯ - которую вы уже использовали в Ограничении по умолчанию в
Id
поле Первичный ключ - чтобыId
заранее сгенерировать новое значение. Генерация значения вначале означает, что вам не нужно беспокоиться о его отсутствииSCOPE_IDENTITY
, а затем означает, что вам не нужно ниOUTPUT
предложение, ни дополнительные операцииSELECT
для получения нового значения; у вас будет значение, прежде чем вы сделаетеINSERT
, и вам даже не нужно возитьсяSET IDENTITY INSERT ON / OFF
:-)Так что это берет на себя часть общей ситуации. Другая часть обрабатывает проблему одновременности двух процессов, в то же самое время, не находя существующую строку для точно такой же строки, и продолжая с
INSERT
. Обеспокоенность заключается в том, чтобы избежать возможного нарушения Уникальных ограничений.Один из способов решения этих типов проблем параллелизма - заставить эту конкретную операцию быть однопоточной. Способ сделать это - использовать блокировки приложений (которые работают между сеансами). Хотя они эффективны, они могут быть немного сложными в такой ситуации, когда частота столкновений, вероятно, довольно низкая.
Другой способ справиться с коллизиями - признать, что они иногда случаются, и справиться с ними, а не пытаться их избегать. Используя
TRY...CATCH
конструкцию, вы можете эффективно перехватить конкретную ошибку (в данном случае: «нарушение уникального ограничения», Msg 2601) и повторно выполнить ее,SELECT
чтобы получитьId
значение, поскольку мы знаем, что оно теперь существует из-за нахождения вCATCH
блоке с этим конкретным ошибка. Другие ошибки могут быть обработаны типичнымRAISERROR
/RETURN
илиTHROW
способом.Настройка теста: последовательность, таблица и уникальный индекс
Настройка теста: хранимая процедура
Тест
Вопрос от ОП
MERGE
имеет различные "проблемы" (несколько ссылок связаны в ответе @ SqlZim, поэтому нет необходимости дублировать эту информацию здесь). И в этом подходе нет дополнительной блокировки (меньше конфликтов), поэтому он должен быть лучше при параллельности. При таком подходе вы никогда не получите нарушение уникальных ограничений, все без таковогоHOLDLOCK
и т. Д. Это в значительной степени гарантированно сработает.Обоснование этого подхода:
CATCH
блок в первую очередь будет довольно низкой. Имеет больше смысла оптимизировать код, который будет выполняться в 99% случаев вместо кода, который будет выполняться в 1% случаев (если нет затрат на оптимизацию обоих, но здесь это не так).Комментарий от ответа @ SqlZim (выделение добавлено)
Я согласился бы с этим первым предложением, если бы в него были внесены поправки с указанием «и _при благоразумии». То, что что-то технически возможно, не означает, что ситуация (т.е. предполагаемый вариант использования) будет выиграна.
Проблема, которую я вижу с этим подходом, заключается в том, что он блокирует больше, чем предлагается. Важно перечитать процитированную документацию на «сериализуемость», в частности следующее (выделение добавлено):
Теперь вот комментарий в примере кода:
Оперативное слово там - «диапазон». Используемая блокировка зависит не только от значения в
@vName
, но, точнее, от диапазона, начинающегося сместо, куда должно идти это новое значение (т. е. между существующими значениями ключа по обе стороны от того, где подходит новое значение), но не само значение. Это означает, что другие процессы будут заблокированы от вставки новых значений, в зависимости от значений, которые в настоящее время ищутся. Если поиск выполняется в верхней части диапазона, вставка чего-либо, что могло бы занять ту же самую позицию, будет заблокирована. Например, если существуют значения «a», «b» и «d», то, если один процесс выполняет SELECT для «f», будет невозможно вставить значения «g» или даже «e» ( так как любой из тех, кто придет сразу после "d"). Но вставка значения «c» будет возможна, поскольку она не будет помещена в «зарезервированный» диапазон.Следующий пример должен иллюстрировать это поведение:
(На вкладке запроса (т.е. сеанс) # 1)
(На вкладке запроса (т.е. сеанс) # 2)
Аналогично, если значение «C» существует и значение «A» выбирается (и, следовательно, блокируется), вы можете вставить значение «D», но не значение «B»:
(На вкладке запроса (т.е. сеанс) # 1)
(На вкладке запроса (т.е. сеанс) # 2)
Чтобы быть справедливым, в моем предложенном подходе, когда есть исключение, в журнале транзакций будет 4 записи, которых не будет в этом подходе «сериализуемой транзакции». НО, как я уже говорил выше, если исключение происходит 1% (или даже 5%) времени, это оказывает гораздо меньшее влияние, чем гораздо более вероятный случай первоначального SELECT, временно блокирующего операции INSERT.
Другая, хотя и незначительная, проблема с этим подходом «сериализуемая транзакция + предложение OUTPUT» заключается в том, что
OUTPUT
предложение (в его нынешнем использовании) отправляет данные обратно как набор результатов. Набор результатов требует больше накладных расходов (вероятно, с обеих сторон: в SQL Server для управления внутренним курсором и на уровне приложения для управления объектом DataReader), чем в виде простогоOUTPUT
параметра. Учитывая, что мы имеем дело только с одним скалярным значением, и что предполагается высокая частота выполнения, эта дополнительная нагрузка на набор результатов, вероятно, складывается.Хотя это
OUTPUT
предложение можно использовать таким образом, чтобы возвращатьOUTPUT
параметр, для этого потребуются дополнительные шаги для создания временной таблицы или табличной переменной, а затем выбора значения из этой временной таблицы / табличной переменной вOUTPUT
параметре.Дополнительное разъяснение: Ответ на ответ @ SqlZim (обновленный ответ) на мой Ответ на ответ @ SqlZim (в исходном ответе) на мое утверждение относительно параллелизма и производительности ;-)
Извините, если эта часть немного длинна, но на данный момент мы просто до нюансов двух подходов.
Да, я признаю, что я предвзят, хотя и справедливо
INSERT
сбой из-за нарушения уникального ограничения. Я не видел, что упомянуто ни в одном из других ответов / сообщений.Относительно подхода @ gbn «JFDI», поста Майкла Дж. Сварта «Гадкий прагматизм для победы» и комментария Аарона Бертранда к посту Майкла (относительно его тестов, показывающих, какие сценарии снизили производительность), и вашего комментария к вашей «адаптации Майкла Дж. Адаптация Стюартом процедуры Try Catch JFDI в @ gbn:
Что касается обсуждения 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
когда никакой другой процесс не пытается сделать это в тот же момент .С учетом всего вышесказанного: почему я предпочитаю свой подход?
Во-первых, давайте посмотрим, какая блокировка происходит в «сериализуемом» подходе. Как упомянуто выше, «диапазон», который блокируется, зависит от существующих значений ключа по обе стороны от того, где будет соответствовать новое значение ключа. Начало или конец диапазона также может быть началом или концом индекса, соответственно, если в этом направлении не существует ключевого значения. Предположим, у нас есть следующий индекс и ключи (
^
представляет начало индекса, а$
представляет его конец):Если сеанс 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
Подход, в этом случае, казалось бы , не имеют каких - либо дополнительных накладных расходов, и может выполнять несколько лучше , чем то , что я предлагаю.Как и во многих / большинстве дискуссий, касающихся производительности, из-за того, что на результат влияет так много факторов, единственный способ по-настоящему понять, как что-то будет работать, - это опробовать его в целевой среде, в которой оно будет работать. На этом этапе это не будет вопросом мнения :).
источник
Обновленный ответ
Ответ на @srutzky
Я согласен, и по тем же причинам я использую выходные параметры, когда разумно . Это была моя ошибка - не использовать выходной параметр в моем первоначальном ответе, я был ленивым.
Вот пересмотренная процедура с использованием выходного параметра, дополнительные оптимизации, наряду с ,
next value for
что @srutzky объясняет в своем ответе :примечание об обновлении : Включение
updlock
с помощью выбора позволит захватить соответствующие блокировки в этом сценарии. Спасибо @srutzky, который указал, что это может вызвать взаимоблокировку только при использованииserializable
наselect
.Примечание: это может быть не так, но если это возможно, процедура будет вызываться со значением для
@vValueId
, includeset @vValueId = null;
afterset xact_abort on;
, в противном случае ее можно удалить.В отношении примеров @ srutzky поведения блокировки диапазона ключей:
@srutzky использует только одно значение в своей таблице и блокирует ключ "next" / "infinity" для своих тестов, чтобы проиллюстрировать блокировку диапазона клавиш. Хотя его тесты иллюстрируют, что происходит в этих ситуациях, я полагаю, что способ представления информации может привести к ложным предположениям о количестве блокировок, которые можно ожидать при использовании
serializable
в сценарии, как представлено в исходном вопросе.Несмотря на то, что я воспринимаю предвзятость (возможно, ложную) в том, как он представляет свое объяснение и примеры блокировки диапазона клавиш, они все еще верны.
После дальнейших исследований я нашел особенно уместную статью в блоге Майкла Дж. Сварта от 2011 года: « Разрушение мифов: одновременное обновление / вставка решений» . В нем он проверяет несколько методов на точность и параллелизм. Метод 4: Повышенная изоляция + точная настройка блокировок основаны на публикации Сэма Саффрона « Шаблон вставки или обновления для SQL Server» и единственном методе в первоначальном тесте, который соответствует его ожиданиям (к которому позднее присоединился
merge with (holdlock)
).В феврале 2016 года Майкл Дж. Сварт опубликовал « Гадкий прагматизм для победы» . В этом посте он рассказывает о некоторых дополнительных настройках, которые он внес в свои процедуры шафрана для уменьшения блокировки (которые я включил в процедуру выше).
После внесения этих изменений Майкл не был счастлив, что его процедура стала выглядеть более сложной, и посоветовался с коллегой по имени Крис. Крис прочитал все оригинальные посты Mythbusters, прочитал все комментарии и спросил о шаблоне @ gbn TRY CATCH JFDI . Этот шаблон похож на ответ @ srutzky и является решением, которое Майкл в конечном итоге использовал в этом случае.
Майкл Дж Сварт:
На мой взгляд, оба решения жизнеспособны. Хотя я все еще предпочитаю повышать уровень изоляции и настраивать блокировки, ответ @ srutzky также действителен и может быть, а может и не быть более эффективным в вашей конкретной ситуации.
Возможно, в будущем я тоже приду к тому же выводу, что и Майкл Дж. Сварт, но я просто еще не там.
Это не мое предпочтение, но вот как могла бы выглядеть моя адаптация адаптации Майкла Дж. Стюарта к процедуре @ Gbn Try Catch JFDI :
Если вы вставляете новые значения чаще, чем выбираете существующие, это может быть более производительным, чем версия @ srutzky . В противном случае я бы предпочел версию @ srutzky этой.
Комментарии Аарона Бертранда к сообщению Майкла Дж. Сварта о соответствующих тестах, которые он провел, привели к этому обмену. Выдержка из раздела комментариев на тему « Гадкий прагматизм ради победы» :
и ответ:
Новые ссылки:
Оригинальный ответ
Я по-прежнему предпочитаю подход « Сэм Шафран» против использования
merge
, особенно когда речь идет об одном ряду.Я бы адаптировал этот метод upsert к такой ситуации:
Я был бы согласен с вашим наименованием, а так
serializable
же, какholdlock
, выбрать один и быть последовательным в его использовании. Я склонен использовать,serializable
потому что это то же имя, что и при указанииset transaction isolation level serializable
.При использовании
serializable
илиholdlock
блокировка диапазона берется на основании значения,@vName
которое заставляет любые другие операции ждать, если они выбирают или вставляют значения,dbo.NameLookup
которые включают значение вwhere
предложении.Чтобы блокировка диапазона работала правильно, в
ItemName
столбце должен быть индекс, который также применяется при использованииmerge
.Вот как будет выглядеть процедура, в основном следуя инструкциям Erland Sommarskog для обработки ошибок , используя
throw
. Еслиthrow
это не то, как вы выявляете свои ошибки, измените его, чтобы он соответствовал остальным процедурам:Подводя итог, что происходит в процедуре выше:
set nocount on; set xact_abort on;
как вы всегда делаете , то если наша входная переменнаяis null
или пусто,select id = cast(null as int)
как результат. Если оно не пустое или пустое, то получитеId
переменную для нашей переменной, удерживая это место, если его там нет. ЕслиId
есть, отправьте его. Если его там нет, вставьте его и отправьте это новоеId
.Между тем, другие вызовы этой процедуры, пытающиеся найти Id для того же значения, будут ждать до завершения первой транзакции, а затем выбрать и вернуть ее. Другие вызовы этой процедуры или другие операторы, ищущие другие значения, будут продолжены, потому что это не мешает.
Хотя я согласен с @srutzky в том, что вы можете обрабатывать коллизии и проглатывать исключения для такого рода проблем, я лично предпочитаю попробовать и адаптировать решение, чтобы избежать этого, когда это возможно. В этом случае я не чувствую, что использование блокировок
serializable
- это сложный подход, и я был бы уверен, что он хорошо справится с высоким параллелизмом.Цитата из документации сервера sql на таблицу подсказок
serializable
/holdlock
:Цитата из документации сервера sql об уровне изоляции транзакций
serializable
Ссылки, связанные с решением выше:
Вставить или обновить шаблон для Sql Server - Сэм Саффрон
Документация по сериализуемым и другим табличным подсказкам - MSDN
Обработка ошибок и транзакций в SQL Server. Часть первая. Обработка ошибок Jumpstart - Erland Sommarskog
Совет Эрланда Соммарскога относительно @@ rowcount (в этом случае я не следовал).
MERGE
имеет пятнистую историю, и кажется, что нужно больше возиться, чтобы убедиться, что код ведет себя так, как вы хотите при всем этом синтаксисе. Соответствующиеmerge
статьи:Интересная ошибка MERGE - Пол Уайт
Условия гонки UPSERT слияния - sqlteam
Будьте осторожны с оператором MERGE в SQL Server - Аарон Бертран
Могу ли я оптимизировать это заявление о слиянии - Аарон Бертран
Если вы используете индексированные представления и MERGE, пожалуйста, прочитайте это! - Аарон Бертран
Одна из последних ссылок, Кендра Литтл, сделала грубое сравнение с «
merge
против»insert with left join
, с оговоркой, где она говорит: «Я не проводил тщательное нагрузочное тестирование по этому вопросу», но это все еще хорошее чтение.источник