Є 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

Оскільки ви використовуєте послідовність, ви можете використовувати ту саму функцію NEXT VALUE FOR, яку ви вже маєте у полі обмеження за замовчуванням у полі 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. Я також включив приклад блокування діапазону між двома існуючими ключами (другий набір блоків "Вкладка запитів 1" та "Вкладка запитів 2").
  4. Я знайшов (і добровільно) "приховану вартість" мого підходу, що бути чотирма додатковими записами Tran Log щоразу, коли INSERTне вдається через порушення єдиного обмеження. Я не бачив того, що згадується в жодній з інших відповідей / дописів.

Що стосується підходу "JFDI" @ gbn, повідомлення "Потворного прагматизму для виграшу" Майкла Дж. Сватара та коментаря Аарона Бертрана до повідомлення Майкла (щодо його тестів, що показують, які сценарії знизили ефективність), а також ваш коментар щодо вашої "адаптації Майкла Дж. . Адаптація Стюарта в процедурі "Спробуйте зловити 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.

Коли більше ключових значень додаються, діапазони між ключовими значеннями стають вужчими, отже, зменшується ймовірність / частота декількох значень, що вставляються при одночасному бою за один і той же діапазон. Справді, це не основна проблема, і на щастя, здається, це проблема, яка фактично зменшується з часом.

Проблема з моїм підходом була описана вище: це відбувається лише тоді, коли два сеанси намагаються одночасно вставити одне і те ж значення ключа. У цьому відношенні зводиться до того, що більша ймовірність того, що трапиться: одночасно намагаються спробувати два різні, але близькі, ключові значення або одночасно спробувати одне і те ж ключове значення? Я припускаю, що відповідь полягає в структурі додатка, який робить вставки, але, загалом кажучи, я вважаю, що більш ймовірно, що два різні значення, які просто мають спільний діапазон, вставляються. Але єдиним способом дійсно знати було б тестування обох на системі ОП.

Далі розглянемо два сценарії та як кожен підхід обробляє їх:

  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, включити set @vValueId = null;після set xact_abort on;, інакше її можна буде видалити.


Щодо прикладів @ srutzky щодо поведінки блокування ключових діапазонів:

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

Хоча я сприймаю упередженість (можливо помилково) у тому, як він подає своє пояснення та приклади блокування ключових діапазонів, вони все ще є правильними.


Після додаткових досліджень я знайшов особливо актуальну статтю в блозі від Майкла Дж. Суарта: Mythbusting: Паралельне оновлення / вставка рішень . У ньому він випробовує декілька методів на точність та одночасність. Спосіб 4: Підвищена ізоляція + точні настройки блокування засновані на вставці або оновлення шаблону Сам Шафрона для SQL Server і єдиному методі в оригінальному тесті, який відповідає його очікуванням (приєднався пізніше merge with (holdlock)).

У лютому 2016 року Майкл Дж. Суарт виклав некрасивий прагматизм для перемоги . У цій посаді він висвітлює деяку додаткову настройку, яку він здійснив у своїх процедурах, спрямованих на шафран, щоб зменшити блокування (яке я включив у процедуру вище).

Після внесення цих змін Майкл не був задоволений тим, що його процедура починає виглядати складніше, і порадився з колегією на ім'я Кріс. Кріс прочитав усі оригінальні публікації Mythbusters і прочитав усі коментарі та запитав про модель " TRY CATCH JFDI" @ gbn . Ця закономірність схожа на відповідь @ srutzky, і це рішення, яке Майкл в кінцевому підсумку використовував у цьому випадку.

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

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

Принаймні, це було моїм уподобанням. Нещодавно я змінив свій підхід до використання методу, запропонованого gbn у коментарях. Він описує свій метод як "Спробуй CATCH JFDI". Зазвичай я уникаю подібних рішень. Існує головне правило, згідно з яким розробники не повинні покладатися на помилки чи винятки для потоку управління. Але я вчора порушив це правило.

До речі, мені подобається опис gbn для шаблону "JFDI". Це мені нагадує мотиваційне відео Shia Labeouf.


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

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


Це не є моїм уподобанням, але ось, як виглядала моя адаптація Майкла Дж. Стюарта в адаптації процедури JCDI @ gbn @ Gbn :

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 (при округленні до найближчого відсотка).

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

Ось чому ми додали не суворо необхідне положення, де НЕ існує.

Коментар Майкла Дж. Суарта - 11 лютого 2016 р. В 11:57 ранку


Нові посилання:


Оригінальна відповідь


Я все ще віддаю перевагу підходу Sam Saffron upsert порівняно до використання merge, особливо коли йдеться про один ряд.

Я пристосував би цей метод, наприклад, такий:

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.

Тим часом, інші дзвінки до цієї процедури, які намагаються знайти ідентифікатор для того ж значення, зачекають, поки буде здійснена перша транзакція, а потім виберіть і поверніть її. Інші виклики до цієї процедури або інші заяви, які шукають інші значення, продовжуватимуться, оскільки це не в змозі.

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

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

СЕРІАЛЬНО

Еквівалентний HOLDLOCK. Робить спільні блокування більш обмежуючими, утримуючи їх до завершення транзакції, замість того, щоб звільняти загальний замок, як тільки потрібна таблиця або сторінка даних більше не потрібні, незалежно від того, завершена чи ні транзакція. Сканування виконується з тією ж семантикою, що і транзакція, що виконується на рівні ізоляції СЕРІАЛІЗАЦІЙНО. Для отримання додаткової інформації про рівні ізоляції див. НАСТРОЙКИ ІЗОЛЯЦІЇ ІЗОЛЯЦІЇ ТРАНЗАКЦІЇ (Transact-SQL).

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

SERIALIZABLE Вказує наступне:

  • Заяви не можуть читати дані, які були змінені, але ще не здійснені іншими транзакціями.

  • Жодна інша транзакція не може змінювати дані, які були прочитані поточною транзакцією до завершення поточної транзакції.

  • Інші транзакції не можуть вставляти нові рядки з ключовими значеннями, які потрапляли б у діапазон ключів, прочитаних будь-якими висловлюваннями в поточній транзакції до завершення поточної транзакції.


Посилання, пов'язані з рішенням вище:

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

Останнє посилання, Кендра Літл зробила грубе порівняння mergeпорівняноinsert with left join з попереднім застереженням, де вона каже: "Я не робила ретельного тестування навантаження на це", але це все ще добре прочитане.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.