Czy scalenie z WYJŚCIEM jest lepszą praktyką niż warunkowe WSTAW i WYBIERZ?

12

Często spotykamy się z sytuacją „Jeśli nie istnieje, wstaw”. Blog Dana Guzmana zawiera doskonałe informacje na temat tego, jak sprawić, by ten proces był bezpieczny.

Mam podstawową tabelę, która po prostu kataloguje ciąg do liczby całkowitej z SEQUENCE. W procedurze przechowywanej muszę uzyskać klucz liczby całkowitej dla wartości, jeśli istnieje, lub INSERTuzyskać wartość wynikową. dbo.NameLookup.ItemNameKolumna ma wyjątkowość, więc integralność danych nie jest zagrożona, ale nie chcę napotykać wyjątków.

To nie jest IDENTITYtak, że nie mogę dostać, SCOPE_IDENTITYa wartość może być NULLw niektórych przypadkach.

W mojej sytuacji mam do czynienia tylko z INSERTbezpieczeństwem na stole, więc staram się zdecydować, czy lepiej zastosować MERGEtakie rozwiązanie:

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

Mógłbym to zrobić bez użycia MERGEwarunku, INSERTpo którym następuje SELECT myślenie, że to drugie podejście jest czytelniejsze dla czytelnika, ale nie jestem przekonany, że to „lepsza” praktyka

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

A może jest inny lepszy sposób, którego nie rozważałem

Przeszukałem i odniosłem się do innych pytań. Ten: /programming/5288283/sql-server-insert-if-not-exists-best-practice jest najbardziej odpowiedni, jaki mogłem znaleźć, ale wydaje się, że nie ma on zastosowania do mojego przypadku użycia. Inne pytania do IF NOT EXISTS() THENpodejścia, które nie uważam za dopuszczalne.

Mateusz
źródło
Czy próbowałeś eksperymentować z tabelami większymi niż bufor, miałem doświadczenia, w których wydajność scalania spada, gdy tabela osiągnie określony rozmiar.
paclyely

Odpowiedzi:

8

Ponieważ używasz Sekwencji, możesz użyć tej samej NASTĘPNEJ WARTOŚCI DLA - którą masz już w Ograniczeniu domyślnym w Idpolu Klucz podstawowy - aby wygenerować nową Idwartość z wyprzedzeniem. Generowanie wartości w pierwszej kolejności oznacza, że ​​nie musisz się martwić o jej brak SCOPE_IDENTITY, co oznacza, że ​​nie potrzebujesz ani OUTPUTklauzuli, ani robienia dodatkowych, SELECTaby uzyskać nową wartość; będziesz miał wartość, zanim to zrobisz INSERT, i nawet nie musisz zadzierać z SET IDENTITY INSERT ON / OFF:-)

To zajmuje część ogólnej sytuacji. Druga część to obsługa problemu dwóch procesów jednocześnie, nie znajdowanie istniejącego wiersza dla dokładnie tego samego łańcucha i kontynuowanie INSERT. Chodzi o uniknięcie naruszenia unikatowego ograniczenia, które mogłoby wystąpić.

Jednym ze sposobów radzenia sobie z tego rodzaju problemami współbieżności jest wymuszenie, aby ta konkretna operacja była jednowątkowa. Można to zrobić za pomocą blokad aplikacji (które działają między sesjami). Choć są skuteczne, mogą być nieco ciężkie w sytuacji takiej jak ta, w której częstotliwość kolizji jest prawdopodobnie dość niska.

Innym sposobem radzenia sobie z kolizjami jest zaakceptowanie, że czasami się zdarzają, i radzenie sobie z nimi, a nie próba ich uniknięcia. Korzystając z TRY...CATCHkonstrukcji, możesz skutecznie wychwycić konkretny błąd (w tym przypadku: „unikalne naruszenie ograniczenia”, Msg 2601) i ponownie wykonać, SELECTaby uzyskać Idwartość, ponieważ wiemy, że teraz istnieje, ponieważ jest w CATCHbloku z tym konkretnym błąd. Inne błędy mogą być obsługiwane w typowym RAISERROR/ RETURNlub THROWsposób.

Konfiguracja testu: Sekwencja, Tabela i Indeks unikalny

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

Konfiguracja testu: procedura przechowywana

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

Test

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

Pytanie od OP

Dlaczego to jest lepsze niż MERGE? Czy nie uzyskam tej samej funkcjonalności bez TRYużycia WHERE NOT EXISTSklauzuli?

MERGEma różne „problemy” (kilka referencji jest powiązanych w odpowiedzi @ SqlZim, więc nie ma potrzeby kopiowania tych informacji tutaj). I w tym podejściu nie ma dodatkowego blokowania (mniej rywalizacji), więc powinno być lepiej w przypadku współbieżności. Dzięki takiemu podejściu nigdy nie otrzymasz naruszenia Unikalnego Ograniczenia, wszystko bez żadnego HOLDLOCK, itp. Jest prawie pewne, że zadziała.

Uzasadnieniem tego podejścia jest:

  1. Jeśli masz wystarczającą liczbę wykonań tej procedury, więc musisz się martwić o kolizje, nie chcesz:
    1. podejmij więcej kroków niż jest to konieczne
    2. przytrzymaj blokady wszystkich zasobów dłużej niż to konieczne
  2. Ponieważ kolizje mogą się zdarzyć tylko w przypadku nowych wpisów (nowe wpisy przesłane dokładnie w tym samym czasie ), częstotliwość wpadania do CATCHbloku w pierwszej kolejności będzie dość niska. Bardziej sensowne jest zoptymalizowanie kodu, który będzie działał przez 99% czasu, zamiast kodu, który będzie działał przez 1% czasu (chyba że nie ma kosztów optymalizacji obu, ale tak nie jest w tym przypadku).

Komentarz z odpowiedzi @ SqlZim (wyróżnienie dodane)

Ja osobiście wolę próbować dostosować rozwiązanie, aby w miarę możliwości unikać tego . W tym przypadku nie wydaje mi się, aby używanie blokad z serializablebyło ciężkim podejściem i byłbym pewien, że dobrze poradziłby sobie z wysoką współbieżnością.

Zgodziłbym się z tym pierwszym zdaniem, gdyby zostało zmienione w taki sposób, aby zawierało sformułowanie „i - gdy jest ostrożny”. To, że coś jest technicznie możliwe, nie oznacza, że ​​sytuacja (tj. Zamierzony przypadek użycia) byłaby z tego korzystna.

Problem, który widzę w tym podejściu, polega na tym, że blokuje on więcej niż sugeruje się. Ważne jest, aby ponownie przeczytać cytowaną dokumentację dotyczącą „serializowalnego”, w szczególności następujące (podkreślenie dodane):

  • Inne transakcje nie mogą wstawiać nowych wierszy z wartościami kluczy, które mieszczą się w zakresie kluczy odczytanych przez dowolne instrukcje w bieżącej transakcji, dopóki bieżąca transakcja się nie zakończy.

Oto komentarz w przykładowym kodzie:

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */

Słowo operacyjne to „zasięg”. Blokada jest podejmowana nie tylko od wartości w @vName, ale dokładniej od zakresu odmiejsce, w którym powinna iść ta nowa wartość (tj. między istniejącymi kluczowymi wartościami po obu stronach miejsca, w którym mieści się nowa wartość), ale nie sama wartość. Oznacza to, że inne procesy będą blokowane przed wstawianiem nowych wartości, w zależności od aktualnie badanych wartości. Jeśli wyszukiwanie odbywa się u góry zakresu, wstawianie wszystkiego, co mogłoby zajmować tę samą pozycję, zostanie zablokowane. Na przykład, jeśli istnieją wartości „a”, „b” i „d”, to jeśli jeden proces wykonuje WYBÓR na „f”, wówczas nie będzie można wstawić wartości „g”, a nawet „e” ( ponieważ którykolwiek z nich pojawi się natychmiast po „d”). Jednak wstawienie wartości „c” będzie możliwe, ponieważ nie zostanie ona umieszczona w zakresie „zarezerwowanym”.

Poniższy przykład powinien zilustrować to zachowanie:

(Na karcie zapytania (tj. Sesja) # 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;

(Na karcie zapytania (tj. Sesja) # 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

Podobnie, jeśli istnieje wartość „C”, a wartość „A” jest wybierana (a zatem blokowana), wówczas można wstawić wartość „D”, ale nie wartość „B”:

(Na karcie zapytania (tj. Sesja) # 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;

(Na karcie zapytania (tj. Sesja) # 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

Szczerze mówiąc, w moim sugerowanym podejściu, w przypadku wyjątku, w Dzienniku transakcji pojawią się 4 wpisy, które nie pojawią się w tym podejściu do „transakcji możliwej do serializacji”. ALE, jak powiedziałem powyżej, jeśli wyjątek zdarzy się 1% (lub nawet 5%) czasu, będzie to miało o wiele mniejszy wpływ niż o wiele bardziej prawdopodobny przypadek początkowego SELECT blokującego tymczasowo operacje WSTAWIANIA.

Innym, aczkolwiek niewielkim, problemem związanym z tym podejściem „możliwa do serializacji transakcja + klauzula OUTPUT” jest to, że OUTPUTklauzula (w obecnym użyciu) odsyła dane jako zestaw wyników. Zestaw wyników wymaga większego obciążenia (prawdopodobnie po obu stronach: w SQL Server do zarządzania wewnętrznym kursorem oraz w warstwie aplikacji do zarządzania obiektem DataReader) niż prosty OUTPUTparametr. Biorąc pod uwagę, że mamy do czynienia tylko z jedną wartością skalarną i że założeniem jest wysoka częstotliwość wykonywania, ten dodatkowy narzut zestawu wyników prawdopodobnie się sumuje.

Chociaż OUTPUTklauzula może być użyta w taki sposób, aby zwrócić OUTPUTparametr, wymagałoby to dodatkowych kroków w celu utworzenia tabeli tymczasowej lub zmiennej tabeli, a następnie wybrania wartości z tej tabeli temp / zmiennej tabeli do OUTPUTparametru.

Dalsze wyjaśnienia: Odpowiedź na odpowiedź @ SqlZim (zaktualizowana odpowiedź) na moją odpowiedź na odpowiedź @ SqlZim (w oryginalnej odpowiedzi) na moje oświadczenie dotyczące współbieżności i wydajności ;-)

Przepraszam, jeśli ta część jest trochę za długa, ale w tym momencie jesteśmy po prostu na niuansach obu podejść.

Uważam, że sposób, w jaki informacje są prezentowane, może prowadzić do fałszywych założeń dotyczących wielkości blokady, jakiej można się spodziewać podczas korzystania serializableze scenariusza przedstawionego w pierwotnym pytaniu.

Tak, przyznaję, że jestem stronniczy, choć szczerze mówiąc:

  1. Niemożliwe jest, aby człowiek nie był stronniczy, przynajmniej w niewielkim stopniu, a ja staram się zachować go na minimalnym poziomie,
  2. Podany przykład był uproszczony, ale miał on na celu zilustrowanie zachowania bez nadmiernego komplikowania go. Implikowanie nadmiernej częstotliwości nie było zamierzone, chociaż rozumiem, że ja również nie stwierdziłem wprost inaczej i można to odczytać jako sugerujące większy problem niż w rzeczywistości istnieje. Spróbuję wyjaśnić to poniżej.
  3. Dołączyłem również przykład blokowania zakresu między dwoma istniejącymi kluczami (drugi zestaw bloków „Karta zapytania 1” i „Karta zapytania 2”).
  4. Znalazłem (i zgłosiłam się na ochotnika) „ukryty koszt” mojego podejścia, że ​​są to cztery dodatkowe wpisy w Dzienniku Tran za każdym razem, gdy INSERTkończy się niepowodzeniem z powodu naruszenia Unikalnego Ograniczenia. Nie widziałem tego wymienionego w żadnej z innych odpowiedzi / postów.

Jeśli chodzi o podejście „JFDI” @ gbn, post „Ugly Pragmatism For The Win” Michaela J. Swarta oraz komentarz Aarona Bertranda do postu Michaela (dotyczący jego testów pokazujących, które scenariusze zmniejszyły wydajność), a także komentarz na temat „adaptacji Michaela J. Adaptacja Stewarta do procedury JFDI Try Catch @ gbn, stwierdzająca:

Jeśli wstawiasz nowe wartości częściej niż wybierając istniejące wartości, może to być bardziej wydajne niż wersja @ srutzky. W przeciwnym razie wolałbym wersję @ srutzky od tej.

W odniesieniu do dyskusji gbn / Michael / Aaron związanej z podejściem „JFDI” niewłaściwe byłoby zrównanie mojej sugestii z podejściem „JFDI” gbn. Ze względu na charakter operacji „Pobierz lub wstaw” istnieje wyraźna potrzeba wykonania tej czynności, SELECTaby uzyskać IDwartość dla istniejących rekordów. Ten WYBÓR działa jak IF EXISTSsprawdzenie, co czyni to podejście bardziej równoważnym wariantowi „CheckTryCatch” testów Aarona. Ponownie napisany kod Michaela (i twoja ostateczna adaptacja adaptacji Michaela) obejmuje również WHERE NOT EXISTSsprawdzenie tego samego. Dlatego moja sugestia (wraz z ostatecznym kodem Michaela i twoją adaptacją jego końcowego kodu) tak naprawdę nie trafi CATCHtak często. Mogą to być tylko sytuacje, w których dwie sesje,ItemNameINSERT...SELECTdokładnie w tym samym momencie, tak że obie sesje otrzymują „prawdziwą” WHERE NOT EXISTSdokładnie w tym samym momencie, a zatem obie próbują wykonać dokładnie INSERTw tym samym momencie. Ten bardzo specyficzny scenariusz zdarza się znacznie rzadziej niż wybór istniejącego ItemNamelub wstawienie nowego, ItemNamegdy żaden inny proces nie próbuje tego zrobić dokładnie w tym samym momencie .

Z WSZYSTKIMI POWYŻSZYMI UMYSŁAMI: Dlaczego wolę swoje podejście?

Najpierw spójrzmy na to, co blokuje się w podejściu „szeregowalnym”. Jak wspomniano powyżej, „zakres”, który zostaje zablokowany, zależy od istniejących wartości kluczy po obu stronach miejsca, w którym zmieściłaby się nowa wartość klucza. Początkiem lub końcem zakresu może być również odpowiednio początek lub koniec indeksu, jeśli w tym kierunku nie ma żadnej wartości klucza. Załóżmy, że mamy następujący indeks i klucze ( ^reprezentuje początek indeksu, a $reprezentuje jego koniec):

Range #:    |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value:  ^         C         F         J         $

Jeśli sesja 55 spróbuje wstawić kluczową wartość:

  • A, następnie zakres nr 1 (od ^do C) jest zablokowany: sesja 56 nie może wstawić wartości B, nawet jeśli jest unikalna i poprawna (jeszcze). Ale sesja 56 można wstawić wartości D, Goraz M.
  • D, następnie zakres nr 2 (od Cdo F) jest zablokowany: sesja 56 nie może wstawić wartości E(jeszcze). Ale sesja 56 można wstawić wartości A, Goraz M.
  • M, następnie zakres # 4 (od Jdo $) jest zablokowany: sesja 56 nie może wstawić wartości X(jeszcze). Ale sesja 56 można wstawić wartości A, Doraz G.

W miarę dodawania kolejnych kluczowych wartości, zakresy między kluczowymi wartościami stają się węższe, co zmniejsza prawdopodobieństwo / częstość wstawienia wielu wartości w tym samym czasie walcząc o ten sam zakres. Trzeba przyznać, że nie jest to poważny problem i na szczęście wydaje się, że problem ten z czasem maleje.

Problem z moim podejściem został opisany powyżej: dzieje się tak tylko wtedy, gdy dwie sesje próbują jednocześnie wprowadzić tę samą wartość klucza. W związku z tym sprowadza się to do tego, co ma większe prawdopodobieństwo wystąpienia: dwie różne, ale bliskie, wartości klucza są próbowane w tym samym czasie, czy próbowana jest ta sama wartość klucza w tym samym czasie? Przypuszczam, że odpowiedź leży w strukturze aplikacji wykonującej wstawki, ale ogólnie mówiąc, bardziej prawdopodobne byłoby założenie dwóch różnych wartości, które akurat dzielą ten sam zakres. Ale jedynym sposobem, aby naprawdę wiedzieć, byłoby przetestowanie obu w systemie operacyjnym.

Następnie rozważmy dwa scenariusze i sposób, w jaki radzi sobie z nimi każde podejście:

  1. Wszystkie żądania dotyczą unikatowych wartości kluczowych:

    W tym przypadku CATCHblok w mojej sugestii nigdy nie jest wprowadzany, dlatego nie ma „problemu” (tj. 4 wpisów w dzienniku tran i czas potrzebny na to). Ale w podejściu „szeregowalnym”, nawet jeśli wszystkie płytki są unikalne, zawsze będzie istniał potencjał do blokowania innych płytek w tym samym zakresie (choć nie na bardzo długo).

  2. Wysoka częstotliwość żądań dla tej samej wartości klucza w tym samym czasie:

    W tym przypadku - bardzo niski stopień wyjątkowości pod względem przychodzących żądań nieistniejących wartości klucza - CATCHblok w mojej sugestii będzie wprowadzany regularnie. Skutkiem tego będzie to, że każda nieudana wstawka będzie musiała automatycznie przywracać i zapisywać 4 wpisy w Dzienniku transakcji, co jest niewielkim spadkiem wydajności za każdym razem. Ale ogólna operacja nigdy nie powinna zawieść (przynajmniej nie z tego powodu).

    (Wystąpił problem z poprzednią wersją „zaktualizowanego” podejścia, która pozwalała mu cierpieć z powodu zakleszczeń. Dodano updlockwskazówkę, aby rozwiązać ten problem i nie ma już zakleszczeń).ALE w podejściu „serializowalnym” (nawet w zaktualizowanej, zoptymalizowanej wersji) operacja zostanie zakleszczona. Dlaczego? Ponieważ serializablezachowanie zapobiega tylko INSERToperacjom w zakresie, który został odczytany, a zatem zablokowany; nie zapobiega SELECToperacjom w tym zakresie.

    serializablePodejście, w tym przypadku, wydaje się nie mieć dodatkowe obciążenie i może wykonywać nieco lepsze niż to, co ja sugeruję.

Podobnie jak w przypadku wielu / większości dyskusji na temat wydajności, ponieważ istnieje tak wiele czynników, które mogą wpłynąć na wynik, jedynym sposobem, aby naprawdę poczuć, jak coś się stanie, jest wypróbowanie go w docelowym środowisku, w którym będzie działać. W tym momencie nie będzie to kwestia opinii :).

Solomon Rutzky
źródło
7

Zaktualizowana odpowiedź


Odpowiedź na @srutzky

Innym, aczkolwiek niewielkim, problemem związanym z tym podejściem „możliwa do serializacji transakcja + klauzula OUTPUT” jest to, że klauzula OUTPUT (w obecnym użyciu) odsyła dane jako zestaw wyników. Zestaw wyników wymaga większego obciążenia (prawdopodobnie po obu stronach: w SQL Server do zarządzania wewnętrznym kursorem oraz w warstwie aplikacji do zarządzania obiektem DataReader) niż prosty parametr OUTPUT. Biorąc pod uwagę, że mamy do czynienia tylko z jedną wartością skalarną i że założeniem jest wysoka częstotliwość wykonywania, ten dodatkowy narzut zestawu wyników prawdopodobnie się sumuje.

Zgadzam się i z tych samych powodów używam parametrów wyjściowych, gdy jestem ostrożny . To mój błąd, że nie użyłem parametru wyjściowego przy pierwszej odpowiedzi, byłem leniwy.

Oto poprawiony procedura z użyciem parametru wyjściowego, dodatkowe optymalizacje, wraz z next value for, że @srutzky wyjaśnia w swojej odpowiedzi :

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;

Uwaga dotycząca aktualizacji : Dołączenie updlockdo zaznaczenia spowoduje pobranie odpowiednich blokad w tym scenariuszu. Podziękowania dla @srutzky, który zwrócił uwagę, że może to powodować impasy podczas korzystania tylko serializablez Internetu select.

Uwaga: Może tak nie być, ale jeśli to możliwe, procedura zostanie wywołana z wartością for @vValueId, include set @vValueId = null;after set xact_abort on;, w przeciwnym razie można ją usunąć.


W odniesieniu do przykładów @ srutzky dotyczących blokowania zakresu klucza:

@srutzky używa tylko jednej wartości w swojej tabeli i blokuje klawisz „next” / „infinity” w swoich testach, aby zilustrować blokowanie zakresu klawiszy. Podczas gdy jego testy ilustrują to, co dzieje się w takich sytuacjach, uważam, że sposób, w jaki informacje są prezentowane, może prowadzić do fałszywych założeń dotyczących wielkości blokady, jakiej można się spodziewać podczas korzystania serializableze scenariusza przedstawionego w pierwotnym pytaniu.

Mimo że postrzegam błąd (być może fałszywie) w sposobie, w jaki przedstawia swoje wyjaśnienia i przykłady blokowania zakresu klucza, są one nadal poprawne.


Po dalszych badaniach znalazłem szczególnie istotny artykuł na blogu z 2011 r. Autorstwa Michaela J. Swarta: Mythbusting: Concurrent Update / Insert Solutions . W nim testuje wiele metod pod kątem dokładności i współbieżności. Metoda 4: Zwiększona izolacja + precyzyjne strojenie blokad opiera się na postie Sama Saffrona wstawiania lub aktualizacji wzorca dla SQL Server i jest jedyną metodą w oryginalnym teście, która spełniła jego oczekiwania (dołączyły później merge with (holdlock)).

W lutym 2016 roku Michael J. Swart opublikował Ugly Pragmatism For The Win . W tym poście opisuje kilka dodatkowych zmian, które wprowadził do swoich procedur upsert Saffron, aby zmniejszyć blokowanie (które zawarłem w powyższej procedurze).

Po dokonaniu tych zmian Michael nie był zadowolony, że jego procedura zaczęła wyglądać na bardziej skomplikowaną i skonsultował się z kolegą o imieniu Chris. Chris przeczytał wszystkie oryginalne posty z Mythbusters, przeczytał wszystkie komentarze i zapytał o wzór JFDI @ gbn TRY CATCH . Ten wzór jest podobny do odpowiedzi @ srutzky i jest rozwiązaniem, którego Michael użył w tym przypadku.

Michael J Swart:

Wczoraj zmieniłem zdanie na temat najlepszego sposobu współbieżności. Opisuję kilka metod w grze Mythbusting: Jednoczesna aktualizacja / wstawianie rozwiązań. Moją preferowaną metodą jest zwiększenie poziomu izolacji i dostrojenie blokad.

Przynajmniej taka była moja preferencja. Niedawno zmieniłem podejście do korzystania z metody sugerowanej przez gbn w komentarzach. Opisuje swoją metodę jako „TRY CATCH JFDI pattern”. Zwykle unikam takich rozwiązań. Istnieje ogólna zasada, że ​​programiści nie powinni polegać na wyłapywaniu błędów lub wyjątków dotyczących przepływu sterowania. Ale wczoraj złamałem tę praktyczną zasadę.

Nawiasem mówiąc, podoba mi się opis gbn dla wzoru „JFDI”. Przypomina mi motywacyjne wideo Shia Labeouf.


Moim zdaniem oba rozwiązania są realne. Chociaż nadal wolę zwiększyć poziom izolacji i dostroić blokady, odpowiedź @ srutzky jest również ważna i może, ale nie musi, być bardziej wydajna w twojej konkretnej sytuacji.

Być może w przyszłości i ja dojdę do tego samego wniosku, co Michael J. Swart, ale po prostu jeszcze mnie tam nie ma.


To nie jest moje preferencje, ale oto, jak wyglądałoby moje dostosowanie adaptacji JFDI @ gbn przez Michaela J. Stewarta :

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;

Jeśli wstawiasz nowe wartości częściej niż wybierając istniejące wartości, może to być bardziej wydajne niż wersja @ srutzky . W przeciwnym razie wolałbym wersję @ srutzky od tej.

Komentarze Aarona Bertranda na temat postów Michaela J Swarta prowadzą do odpowiednich testów, które przeprowadził i doprowadziły do ​​tej wymiany. Fragment sekcji komentarzy na temat brzydkiego pragmatyzmu dla zwycięzcy :

Czasami jednak JFDI prowadzi do ogólnie gorszej wydajności, w zależności od tego, jaki% połączeń kończy się niepowodzeniem. Zgłaszanie wyjątków wiąże się z dużymi kosztami. Pokazałem to w kilku postach:

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/

Komentarz Aarona Bertranda - 11 lutego 2016 o 11:49

i odpowiedź:

Masz rację, Aaron, a my to przetestowaliśmy.

Okazuje się, że w naszym przypadku procent nieudanych połączeń wynosił 0 (po zaokrągleniu do najbliższego procentu).

Myślę, że zilustrowałeś punkt, w którym w jak największym stopniu możesz oceniać poszczególne przypadki w oparciu o praktyczne zasady.

Właśnie dlatego dodaliśmy niepotrzebnie niezbędną klauzulę GDZIE NIE ISTNIEJE.

Komentarz Michaela J. Swarta - 11 lutego 2016 o 11:57


Nowe linki:


Oryginalna odpowiedź


Nadal wolę podejście upsam Sama Saffrona niż używanie merge, szczególnie w przypadku pojedynczego rzędu.

Dostosowałbym tę metodę upsert do następującej sytuacji:

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;

Byłbym zgodny z twoim nazywaniem i, podobnie serializablejak holdlock, wybierz jedno i konsekwentne w jego użyciu. Zwykle używam, serializableponieważ jest to ta sama nazwa, co przy określaniu set transaction isolation level serializable.

Przy użyciu serializablelub holdlockblokada zakresu jest podejmowana na podstawie wartości, @vNamektóra powoduje, że wszelkie inne operacje czekają, czy wybiorą lub wstawią wartości do dbo.NameLookuptej wartości w whereklauzuli.

Aby blokada zakresu działała poprawnie, ItemNamekolumna musi mieć indeks, który obowiązuje również podczas używania merge.


Oto co procedura będzie wyglądać głównie następujące whitepapers Erland Sommarskog za obsługę błędów , używając throw. Jeśli thrownie jest to sposób zgłaszania błędów, zmień go tak, aby był zgodny z pozostałymi procedurami:

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

Podsumowując, co dzieje się w powyższej procedurze: set nocount on; set xact_abort on;tak jak zawsze to robisz , to jeśli nasza zmienna wejściowa is nulllub pusta, select id = cast(null as int)w wyniku. Jeśli nie jest pusta ani pusta, to weź Idzmienną for, trzymając to miejsce na wypadek, gdyby go nie było. Jeśli Idjest, wyślij go. Jeśli go nie ma, włóż go i wyślij nowy Id.

W międzyczasie inne wywołania tej procedury, próbujące znaleźć identyfikator dla tej samej wartości, poczekają na zakończenie pierwszej transakcji, a następnie ją wybiorą i zwrócą. Inne wywołania tej procedury lub inne instrukcje szukające innych wartości będą kontynuowane, ponieważ ta nie jest przeszkodą.

Chociaż zgadzam się z @srutzky, że możesz poradzić sobie z kolizjami i połknąć wyjątki dla tego rodzaju problemów, ja osobiście wolę spróbować dopasować rozwiązanie, aby uniknąć tego, gdy to możliwe. W tym przypadku nie wydaje mi się, aby używanie blokad z serializablebyło ciężkim podejściem i byłbym pewien, że dobrze poradziłby sobie z wysoką współbieżnością.

Cytat z dokumentacji serwera SQL w tabeli podpowiedzi serializable/holdlock :

SERIALIZOWALNY

Odpowiada HOLDLOCK. Sprawia, że ​​blokady dzielone są bardziej restrykcyjne, trzymając je do czasu zakończenia transakcji, zamiast zwalniania blokady wspólnej, gdy tylko wymagana tabela lub strona danych nie jest już potrzebna, niezależnie od tego, czy transakcja została zakończona, czy nie. Skanowanie jest wykonywane przy użyciu tej samej semantyki, co transakcja działająca na poziomie izolacji SERIALIZABLE. Aby uzyskać więcej informacji na temat poziomów izolacji, zobacz USTAW POZIOM IZOLACJI (Transact-SQL).

Wycena z dokumentacji serwera SQL na poziomie izolacji transakcjiserializable

SERIALIZABLE Określa, co następuje:

  • Wyciągi nie mogą odczytać danych, które zostały zmodyfikowane, ale nie zostały jeszcze zatwierdzone przez inne transakcje.

  • Żadne inne transakcje nie mogą modyfikować danych odczytanych przez bieżącą transakcję do czasu jej zakończenia.

  • Inne transakcje nie mogą wstawiać nowych wierszy z wartościami kluczy, które mieszczą się w zakresie kluczy odczytanych przez dowolne instrukcje w bieżącej transakcji, dopóki bieżąca transakcja się nie zakończy.


Linki związane z powyższym rozwiązaniem:

MERGEma niejednoznaczną historię i wydaje się, że potrzeba więcej czasu, aby upewnić się, że kod zachowuje się tak, jak chcesz, przy całej tej składni. Odpowiednie mergeartykuły:

Ostatnie ogniwo, Kendra Little, przeprowadziła przybliżone porównanie w mergeporównaniuinsert with left join z zastrzeżeniem, w którym powiedziała: „Nie przeprowadziłem dokładnych testów obciążenia”, ale nadal jest to dobra lektura.

SqlZim
źródło