Является ли MERGE с OUTPUT лучшей практикой, чем условный INSERT и SELECT?

12

Мы часто сталкиваемся с ситуацией «Если не существует, вставьте». Блог Дэна Гусмана содержит отличное исследование о том, как сделать этот процесс безопасным.

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

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

Ответы:

8

Поскольку вы используете последовательность, вы можете использовать ту же функцию СЛЕДУЮЩЕЕ ЗНАЧЕНИЕ ДЛЯ - которую вы уже использовали в Ограничении по умолчанию в 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и т. Д. Это в значительной степени гарантированно сработает.

Обоснование этого подхода:

  1. Если у вас достаточно выполнений этой процедуры, так что вам нужно беспокоиться о столкновениях, то вам не нужно:
    1. предпринять больше шагов, чем необходимо
    2. удерживать блокировки на любых ресурсах дольше, чем необходимо
  2. Поскольку коллизии могут происходить только при появлении новых записей (новых записей, поданных в одно и то же время ), частота попадания в 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в сценарии, как представлено в исходном вопросе.

Да, я признаю, что я предвзят, хотя и справедливо

  1. Для человека невозможно быть предвзятым, по крайней мере, до некоторой степени, и я стараюсь держать его как минимум,
  2. Приведенный пример был упрощенным, но это было в иллюстративных целях передать поведение, не усложняя его. Подразумевать чрезмерную частоту не планировалось, хотя я понимаю, что я также явно не утверждал иначе, и это можно было бы прочитать как означающее большую проблему, чем на самом деле. Я постараюсь уточнить это ниже.
  3. Я также включил пример блокировки диапазона между двумя существующими ключами (второй набор блоков «Query tab 1» и «Query tab 2»).
  4. Я нашел (и добровольно предложил) «скрытую стоимость» моего подхода, состоящую из четырех дополнительных записей в журнале 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часто встречаться. Это могут быть только ситуации, когда два сеанса,ItemNameINSERT...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.

Далее давайте рассмотрим два сценария и то, как каждый подход обрабатывает их:

  1. Все запросы на уникальные значения ключа:

    В этом случае CATCHблок в моем предложении никогда не вводится, следовательно, нет «проблемы» (т.е. 4 записи журнала и время, необходимое для этого). Но в подходе «сериализации», даже если все вставки уникальны, всегда будет некоторый потенциал для блокировки других вставок в том же диапазоне (хотя и не очень долго).

  2. Частота запросов на одно и то же значение ключа одновременно:

    В этом случае - очень низкая степень уникальности с точки зрения входящих запросов на несуществующие значения ключа - CATCHблок в моем предложении будет вводиться регулярно. Результатом этого будет то, что при каждой неудачной вставке потребуется автоматический откат и запись 4 записей в журнал транзакций, что каждый раз приводит к небольшому снижению производительности. Но общая операция никогда не должна выходить из строя (по крайней мере, не из-за этого).

    (Была проблема с предыдущей версией «обновленного» подхода, которая позволяла ему страдать от взаимоблокировок. Для updlockрешения этой проблемы была добавлена ​​подсказка, и она больше не получает взаимоблокировки.)НО, в «сериализуемом» подходе (даже в обновленной, оптимизированной версии) операция будет тупиковой. Почему? Потому что serializableповедение предотвращает только INSERTоперации в диапазоне, который был прочитан и, следовательно, заблокирован; это не мешает SELECTоперациям в этом диапазоне.

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

Как и во многих / большинстве дискуссий, касающихся производительности, из-за того, что на результат влияет так много факторов, единственный способ по-настоящему понять, как что-то будет работать, - это опробовать его в целевой среде, в которой оно будет работать. На этом этапе это не будет вопросом мнения :).

Соломон Руцкий
источник
7

Обновленный ответ


Ответ на @srutzky

Другая, хотя и незначительная, проблема с этим подходом «сериализуемая транзакция + предложение OUTPUT» заключается в том, что предложение OUTPUT (в его нынешнем использовании) отправляет данные обратно как набор результатов. Набор результатов требует больше затрат (вероятно, с обеих сторон: в SQL Server для управления внутренним курсором и на уровне приложения для управления объектом DataReader), чем в простом параметре OUTPUT. Учитывая, что мы имеем дело только с одним скалярным значением, и что предполагается высокая частота выполнения, эта дополнительная нагрузка на набор результатов, вероятно, складывается.

Я согласен, и по тем же причинам я использую выходные параметры, когда разумно . Это была моя ошибка - не использовать выходной параметр в моем первоначальном ответе, я был ленивым.

Вот пересмотренная процедура с использованием выходного параметра, дополнительные оптимизации, наряду с , next value forчто @srutzky объясняет в своем ответе :

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

примечание об обновлении : Включение updlockс помощью выбора позволит захватить соответствующие блокировки в этом сценарии. Спасибо @srutzky, который указал, что это может вызвать взаимоблокировку только при использовании serializableна select.

Примечание: это может быть не так, но если это возможно, процедура будет вызываться со значением для @vValueId, include set @vValueId = null;after set xact_abort on;, в противном случае ее можно удалить.


В отношении примеров @ srutzky поведения блокировки диапазона ключей:

@srutzky использует только одно значение в своей таблице и блокирует ключ "next" / "infinity" для своих тестов, чтобы проиллюстрировать блокировку диапазона клавиш. Хотя его тесты иллюстрируют, что происходит в этих ситуациях, я полагаю, что способ представления информации может привести к ложным предположениям о количестве блокировок, которые можно ожидать при использовании serializableв сценарии, как представлено в исходном вопросе.

Несмотря на то, что я воспринимаю предвзятость (возможно, ложную) в том, как он представляет свое объяснение и примеры блокировки диапазона клавиш, они все еще верны.


После дальнейших исследований я нашел особенно уместную статью в блоге Майкла Дж. Сварта от 2011 года: « Разрушение мифов: одновременное обновление / вставка решений» . В нем он проверяет несколько методов на точность и параллелизм. Метод 4: Повышенная изоляция + точная настройка блокировок основаны на публикации Сэма Саффрона « Шаблон вставки или обновления для SQL Server» и единственном методе в первоначальном тесте, который соответствует его ожиданиям (к которому позднее присоединился merge with (holdlock)).

В феврале 2016 года Майкл Дж. Сварт опубликовал « Гадкий прагматизм для победы» . В этом посте он рассказывает о некоторых дополнительных настройках, которые он внес в свои процедуры шафрана для уменьшения блокировки (которые я включил в процедуру выше).

После внесения этих изменений Майкл не был счастлив, что его процедура стала выглядеть более сложной, и посоветовался с коллегой по имени Крис. Крис прочитал все оригинальные посты Mythbusters, прочитал все комментарии и спросил о шаблоне @ gbn TRY CATCH JFDI . Этот шаблон похож на ответ @ srutzky и является решением, которое Майкл в конечном итоге использовал в этом случае.

Майкл Дж Сварт:

Вчера я изменил свое мнение о лучшем способе параллелизма. Я описываю несколько методов в Mythbusting: одновременное обновление / вставка решений. Мой предпочтительный метод заключается в повышении уровня изоляции и точной настройки замков.

По крайней мере, это было моим предпочтением. Недавно я изменил свой подход, чтобы использовать метод, предложенный gbn в комментариях. Он описывает свой метод как «шаблон TRF CATCH JFDI». Обычно я избегаю подобных решений. Существует практическое правило, согласно которому разработчики не должны полагаться на обнаружение ошибок или исключений для потока управления. Но вчера я нарушил это правило.

Кстати, мне нравится описание gbn для шаблона «JFDI». Это напоминает мне о мотивационном видео Шиа Лабеуфа.


На мой взгляд, оба решения жизнеспособны. Хотя я все еще предпочитаю повышать уровень изоляции и настраивать блокировки, ответ @ srutzky также действителен и может быть, а может и не быть более эффективным в вашей конкретной ситуации.

Возможно, в будущем я тоже приду к тому же выводу, что и Майкл Дж. Сварт, но я просто еще не там.


Это не мое предпочтение, но вот как могла бы выглядеть моя адаптация адаптации Майкла Дж. Стюарта к процедуре @ Gbn Try Catch JFDI :

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

Если вы вставляете новые значения чаще, чем выбираете существующие, это может быть более производительным, чем версия @ srutzky . В противном случае я бы предпочел версию @ srutzky этой.

Комментарии Аарона Бертранда к сообщению Майкла Дж. Сварта о соответствующих тестах, которые он провел, привели к этому обмену. Выдержка из раздела комментариев на тему « Гадкий прагматизм ради победы» :

Иногда, однако, JFDI приводит к снижению производительности в целом, в зависимости от того, какой процент вызовов завершился неудачно. Повышение исключений имеет значительные накладные расходы. Я показал это в нескольких постах:

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/

Комментарий Аарона Бертрана - 11 февраля 2016 года @ 11:49

и ответ:

Ты прав, Аарон, и мы это проверили.

Получается, что в нашем случае процент неудачных вызовов был равен 0 (при округлении до ближайшего процента).

Я думаю, что вы иллюстрируете точку зрения, что, насколько это возможно, оценивайте вещи в каждом конкретном случае, следуя практическим правилам.

Именно поэтому мы добавили необязательное условие WHERE NOT EXISTS.

Комментарий Майкла Дж. Сварта - 11 февраля 2016 года @ 11:57


Новые ссылки:


Оригинальный ответ


Я по-прежнему предпочитаю подход « Сэм Шафран» против использования merge, особенно когда речь идет об одном ряду.

Я бы адаптировал этот метод upsert к такой ситуации:

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

Я был бы согласен с вашим наименованием, а так serializableже, как holdlock, выбрать один и быть последовательным в его использовании. Я склонен использовать, serializableпотому что это то же имя, что и при указании set transaction isolation level serializable.

При использовании serializableили holdlockблокировка диапазона берется на основании значения, @vNameкоторое заставляет любые другие операции ждать, если они выбирают или вставляют значения, dbo.NameLookupкоторые включают значение в whereпредложении.

Чтобы блокировка диапазона работала правильно, в ItemNameстолбце должен быть индекс, который также применяется при использовании merge.


Вот как будет выглядеть процедура, в основном следуя инструкциям Erland Sommarskog для обработки ошибок , используя throw. Если throwэто не то, как вы выявляете свои ошибки, измените его, чтобы он соответствовал остальным процедурам:

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

Подводя итог, что происходит в процедуре выше: set nocount on; set xact_abort on;как вы всегда делаете , то если наша входная переменная is nullили пусто, select id = cast(null as int)как результат. Если оно не пустое или пустое, то получите Idпеременную для нашей переменной, удерживая это место, если его там нет. Если Idесть, отправьте его. Если его там нет, вставьте его и отправьте это новое Id.

Между тем, другие вызовы этой процедуры, пытающиеся найти Id для того же значения, будут ждать до завершения первой транзакции, а затем выбрать и вернуть ее. Другие вызовы этой процедуры или другие операторы, ищущие другие значения, будут продолжены, потому что это не мешает.

Хотя я согласен с @srutzky в том, что вы можете обрабатывать коллизии и проглатывать исключения для такого рода проблем, я лично предпочитаю попробовать и адаптировать решение, чтобы избежать этого, когда это возможно. В этом случае я не чувствую, что использование блокировок serializable- это сложный подход, и я был бы уверен, что он хорошо справится с высоким параллелизмом.

Цитата из документации сервера sql на таблицу подсказок serializable/holdlock :

SERIALIZABLE

Эквивалент HOLDLOCK. Делает общие блокировки более строгими, удерживая их до завершения транзакции, вместо того, чтобы снимать общую блокировку, как только требуемая таблица или страница данных больше не нужны, независимо от того, была ли транзакция завершена или нет. Сканирование выполняется с той же семантикой, что и транзакция, выполняемая на уровне изоляции SERIALIZABLE. Дополнительные сведения об уровнях изоляции см. В разделе «УСТАНОВКА УРОВНЯ ИЗОЛЯЦИИ» (Transact-SQL).

Цитата из документации сервера sql об уровне изоляции транзакцийserializable

SERIALIZABLE Определяет следующее:

  • Операторы не могут читать данные, которые были изменены, но еще не зафиксированы другими транзакциями.

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

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


Ссылки, связанные с решением выше:

MERGEимеет пятнистую историю, и кажется, что нужно больше возиться, чтобы убедиться, что код ведет себя так, как вы хотите при всем этом синтаксисе. Соответствующие mergeстатьи:

Одна из последних ссылок, Кендра Литтл, сделала грубое сравнение с « mergeпротив»insert with left join , с оговоркой, где она говорит: «Я не проводил тщательное нагрузочное тестирование по этому вопросу», но это все еще хорошее чтение.

SqlZim
источник