Un MERGE con OUTPUT è una pratica migliore di un INSERT condizionale e SELEZIONA?


12

Incontriamo spesso la situazione "Se non esiste, inserisci". Il blog di Dan Guzman ha un'indagine eccellente su come rendere sicuro questo processo.

Ho una tabella di base che cataloga semplicemente una stringa a un numero intero da a SEQUENCE. In una procedura memorizzata ho bisogno di ottenere la chiave intera per il valore, se esiste, oppure di INSERTottenere il valore risultante. C'è un vincolo di unicità sulla dbo.NameLookup.ItemNamecolonna, quindi l'integrità dei dati non è a rischio ma non voglio incontrare le eccezioni.

Non è un IDENTITYquindi non riesco a ottenere SCOPE_IDENTITYe il valore potrebbe essere NULLin alcuni casi.

Nella mia situazione ho solo a che fare con la INSERTsicurezza sul tavolo, quindi sto cercando di decidere se è meglio usare in MERGEquesto modo:

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

Potrei farlo senza usare MERGEsolo un condizionale INSERTseguito da un SELECT Penso che questo secondo approccio sia più chiaro per il lettore, ma non sono convinto che sia una pratica "migliore"

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

O forse c'è un altro modo migliore che non ho considerato

Ho cercato e fatto riferimento ad altre domande. Questo: /programming/5288283/sql-server-insert-if-not-exists-best-practice è il più appropriato che ho trovato ma non sembra molto applicabile al mio caso d'uso. Altre domande IF NOT EXISTS() THENall'approccio che non ritengo accettabili.


Hai provato a sperimentare tabelle più grandi del tuo buffer, ho avuto esperienze in cui le prestazioni di fusione diminuiscono quando la tabella raggiunge una certa dimensione.
pacificamente il

Risposte:


8

Poiché si utilizza una sequenza, è possibile utilizzare la stessa funzione VALORE SUCCESSIVO - già presente in un vincolo predefinito nel Idcampo Chiave primaria - per generare in Idanticipo un nuovo valore. Generare prima il valore significa che non devi preoccuparti di non avere SCOPE_IDENTITY, il che significa che non hai bisogno né della OUTPUTclausola né di fare un ulteriore SELECTper ottenere il nuovo valore; avrai il valore prima di fare il INSERT, e non dovrai nemmeno fare casini con SET IDENTITY INSERT ON / OFF:-)

In modo che si occupi di parte della situazione generale. L'altra parte sta gestendo il problema di concorrenza di due processi, allo stesso tempo, non trovando una riga esistente per la stessa stringa esatta e procedendo con il INSERT. La preoccupazione è di evitare la violazione del Vincolo unico che potrebbe verificarsi.

Un modo per gestire questi tipi di problemi di concorrenza è quello di forzare questa singola operazione a thread singolo. Il modo per farlo è usando i blocchi dell'applicazione (che funzionano attraverso le sessioni). Sebbene efficaci, possono essere un po 'pesanti per una situazione come questa in cui la frequenza delle collisioni è probabilmente abbastanza bassa.

L'altro modo di gestire le collisioni è accettare che a volte si verifichino e gestirle anziché cercare di evitarle. Usando il TRY...CATCHcostrutto, puoi effettivamente intercettare un errore specifico (in questo caso: "violazione del vincolo univoco", messaggio 2601) e rieseguire SELECTil Idvalore per ottenere il valore poiché sappiamo che ora esiste a causa dell'essere nel CATCHblocco con quel particolare errore. Altri errori possono essere gestiti nel modo tipico RAISERROR/ RETURNo THROW.

Configurazione del test: sequenza, tabella e indice univoco

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

Test Setup: Stored Procedure

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

Il 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

Domanda dall'OP

Perché è meglio del MERGE? Non otterrò la stessa funzionalità senza TRYutilizzare la WHERE NOT EXISTSclausola?

MERGEpresenta vari "problemi" (diversi riferimenti sono collegati nella risposta di @SqlZim, quindi non è necessario duplicare tali informazioni qui). E non esiste un blocco aggiuntivo in questo approccio (meno contese), quindi dovrebbe essere migliore sulla concorrenza. In questo approccio non otterrai mai una violazione del Vincolo univoco, il tutto senza alcun HOLDLOCK, ecc. È praticamente garantito che funzioni.

Il ragionamento alla base di questo approccio è:

  1. Se hai abbastanza esecuzioni di questa procedura in modo tale che devi preoccuparti delle collisioni, allora non vuoi:
    1. fare più passi del necessario
    2. tenere blocchi su qualsiasi risorsa più a lungo del necessario
  2. Poiché le collisioni possono verificarsi solo su nuove voci (nuove voci inviate nello stesso momento ), la frequenza di cadere nel CATCHblocco in primo luogo sarà piuttosto bassa. Ha più senso ottimizzare il codice che verrà eseguito il 99% delle volte anziché il codice che verrà eseguito l'1% delle volte (a meno che non ci siano costi per l'ottimizzazione di entrambi, ma qui non è il caso).

Commento della risposta di @ SqlZim (enfasi aggiunta)

Personalmente preferisco provare e personalizzare una soluzione per evitare di farlo quando possibile . In questo caso, non credo che usare i lucchetti serializablesia un approccio pesante, e sarei sicuro che gestirà bene l'alta concorrenza.

Concordo con questa prima frase se fosse modificata per indicare "e _quando prudente". Solo perché qualcosa è tecnicamente possibile non significa che la situazione (cioè il caso d'uso previsto) ne trarrebbe beneficio.

Il problema che vedo con questo approccio è che si blocca più di quanto viene suggerito. È importante rileggere la documentazione citata su "serializzabile", in particolare quanto segue (enfasi aggiunta):

  • Altre transazioni non possono inserire nuove righe con valori chiave che rientrerebbero nell'intervallo di chiavi letto da qualsiasi istruzione nella transazione corrente fino al completamento della transazione corrente.

Ora, ecco il commento nel codice di esempio:

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

La parola chiave è "range". Il blocco in corso non è solo sul valore in @vName, ma più precisamente un intervallo a partire dala posizione in cui dovrebbe andare questo nuovo valore (ovvero tra i valori chiave esistenti su entrambi i lati del punto in cui si adatta il nuovo valore), ma non il valore stesso. Ciò significa che ad altri processi verrà impedito l'inserimento di nuovi valori, a seconda dei valori attualmente ricercati. Se la ricerca viene eseguita nella parte superiore dell'intervallo, l'inserimento di qualsiasi cosa che possa occupare quella stessa posizione verrà bloccato. Ad esempio, se esistono i valori "a", "b" e "d", quindi se un processo esegue SELECT su "f", non sarà possibile inserire i valori "g" o anche "e" ( poiché uno di questi verrà subito dopo "d"). Tuttavia, è possibile inserire un valore di "c" poiché non verrà inserito nell'intervallo "riservato".

L'esempio seguente dovrebbe illustrare questo comportamento:

(Nella scheda della query (es. Sessione) # 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;

(Nella scheda della query (es. Sessione) # 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

Allo stesso modo, se esiste il valore "C" e viene selezionato il valore "A" (e quindi bloccato), è possibile inserire un valore di "D", ma non un valore di "B":

(Nella scheda della query (es. Sessione) # 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;

(Nella scheda della query (es. Sessione) # 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

Ad essere onesti, nel mio approccio suggerito, quando c'è un'eccezione, ci saranno 4 voci nel registro delle transazioni che non accadranno in questo approccio di "transazione serializzabile". MA, come ho detto sopra, se l'eccezione si verifica l'1% (o addirittura il 5%) del tempo, ciò ha un impatto molto inferiore rispetto al caso molto più probabile del SELECT iniziale che blocca temporaneamente le operazioni INSERT.

Un altro, sebbene minore, problema con questo approccio "transazione serializzabile + clausola OUTPUT" è che la OUTPUTclausola (nel suo utilizzo attuale) rispedisce i dati come set di risultati. Un set di risultati richiede più overhead (probabilmente su entrambi i lati: in SQL Server per gestire il cursore interno e nel livello app per gestire l'oggetto DataReader) di un semplice OUTPUTparametro. Dato che abbiamo a che fare solo con un singolo valore scalare e che il presupposto è un'alta frequenza di esecuzioni, probabilmente il costo aggiuntivo del set di risultati si sommerà.

Mentre la OUTPUTclausola potrebbe essere utilizzata in modo da restituire un OUTPUTparametro, ciò richiederebbe ulteriori passaggi per creare una tabella temporanea o una variabile di tabella e quindi selezionare il valore da quella variabile tabella / tabella temporanea nel OUTPUTparametro.

Ulteriori chiarimenti: risposta alla risposta di @ SqlZim (risposta aggiornata) alla mia risposta alla risposta di @ SqlZim (nella risposta originale) alla mia dichiarazione relativa alla concorrenza e alle prestazioni ;-)

Scusate se questa parte è un po 'lunga, ma a questo punto siamo solo alle sfumature dei due approcci.

Credo che il modo in cui vengono presentate le informazioni potrebbe portare a false assunzioni sulla quantità di blocco che ci si potrebbe aspettare di incontrare quando si utilizza serializablenello scenario come presentato nella domanda originale.

Sì, ammetterò di essere di parte, sebbene sia giusto:

  1. È impossibile per un essere umano non essere di parte, almeno in minima parte, e cerco di tenerlo al minimo,
  2. L'esempio fornito era semplicistico, ma era a scopo illustrativo per trasmettere il comportamento senza complicarlo eccessivamente. Non intendevo implicare una frequenza eccessiva, anche se capisco che anche io non ho affermato esplicitamente il contrario e si potrebbe leggere come implicare un problema più grande di quanto effettivamente esista. Cercherò di chiarire che di seguito.
  3. Ho anche incluso un esempio di blocco di un intervallo tra due chiavi esistenti (la seconda serie di blocchi "Scheda query 1" e "Scheda query 2").
  4. Ho trovato (e offerto volontariamente) il "costo nascosto" del mio approccio, ovvero le quattro voci extra del Registro di Tran ogni volta che INSERTfalliscono a causa di una violazione del Vincolo univoco. Non ho visto quello menzionato in nessuna delle altre risposte / post.

Per quanto riguarda l'approccio "JFDI" di @ gbn, il post "Ugly Pragmatism For The Win" di Michael J. Swart e il commento di Aaron Bertrand sul post di Michael (per quanto riguarda i suoi test che mostrano quali scenari hanno ridotto le prestazioni), e il tuo commento sul tuo "adattamento di Michael J L'adattamento di Stewart della procedura Try Catch JFDI di @ gbn "afferma:

Se stai inserendo nuovi valori più spesso della selezione di valori esistenti, questo potrebbe essere più performante della versione di @ srutzky. Altrimenti preferirei la versione di @ srutzky rispetto a questa.

Per quanto riguarda la discussione gbn / Michael / Aaron relativa all'approccio "JFDI", sarebbe errato equiparare il mio suggerimento all'approccio "JFDI" di gbn. A causa della natura dell'operazione "Ottieni o Inserisci", è necessario eseguire esplicitamente SELECTil IDvalore per ottenere i record esistenti. Questo SELECT funge da IF EXISTScontrollo, il che rende questo approccio più simile alla variazione "Check TryCatch" dei test di Aaron. Il codice riscritto di Michael (e il tuo adattamento finale dell'adattamento di Michael) include anche prima WHERE NOT EXISTSdi fare lo stesso controllo. Quindi, il mio suggerimento (insieme al codice finale di Michael e al tuo adattamento del suo codice finale) non colpirà il CATCHblocco così spesso. Potrebbero essere solo situazioni in cui due sessioni,ItemNameINSERT...SELECTnello stesso preciso istante in modo tale che entrambe le sessioni ricevano un "vero" per lo WHERE NOT EXISTSstesso preciso momento e quindi entrambi tentano di fare INSERTesattamente nello stesso momento. Questo scenario molto specifico si verifica molto meno spesso rispetto alla selezione di uno esistente ItemNameo all'inserimento di un nuovo ItemNamequando nessun altro processo sta tentando di farlo nello stesso preciso momento .

CON TUTTO QUANTO SOPRA: Perché preferisco il mio approccio?

Innanzitutto, diamo un'occhiata a ciò che il blocco ha luogo nell'approccio "serializzabile". Come accennato in precedenza, l '"intervallo" che viene bloccato dipende dai valori chiave esistenti su entrambi i lati di dove si adatterebbe il nuovo valore chiave. L'inizio o la fine dell'intervallo potrebbero anche essere l'inizio o la fine dell'indice, rispettivamente, se non esiste un valore chiave esistente in quella direzione. Supponiamo di avere il seguente indice e le chiavi ( ^rappresenta l'inizio dell'indice mentre $rappresenta la fine di esso):

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

Se la sessione 55 tenta di inserire un valore chiave di:

  • A, quindi l'intervallo n. 1 (da ^a C) è bloccato: la sessione 56 non può inserire un valore di B, anche se univoco e valido (ancora). Ma sessione di 56 può inserire valori di D, Ge M.
  • D, quindi l'intervallo # 2 (da Ca F) è bloccato: la sessione 56 non può inserire un valore di E(ancora). Ma sessione di 56 può inserire valori di A, Ge M.
  • M, quindi l'intervallo # 4 (da Ja $) è bloccato: la sessione 56 non può inserire un valore di X(ancora). Ma sessione di 56 può inserire valori di A, De G.

Man mano che vengono aggiunti più valori chiave, gli intervalli tra i valori chiave si restringono, riducendo così la probabilità / frequenza di più valori che vengono inseriti contemporaneamente combattendo sullo stesso intervallo. Certo, questo non è un grosso problema, e fortunatamente sembra essere un problema che in realtà diminuisce nel tempo.

Il problema con il mio approccio è stato descritto sopra: si verifica solo quando due sessioni tentano di inserire lo stesso valore chiave contemporaneamente. A questo proposito, si riduce a ciò che ha la maggiore probabilità di accadere: due valori chiave diversi, ma vicini, vengono tentati contemporaneamente o lo stesso valore chiave viene tentato contemporaneamente? Suppongo che la risposta risieda nella struttura dell'app che esegue gli inserimenti, ma in generale suppongo che sia più probabile che vengano inseriti due valori diversi che condividono lo stesso intervallo. Ma l'unico modo per sapere davvero sarebbe testare entrambi sul sistema operativo.

Quindi, consideriamo due scenari e come ogni approccio li gestisce:

  1. Tutte le richieste riguardano valori chiave univoci:

    In questo caso, il CATCHblocco nel mio suggerimento non viene mai inserito, quindi nessun "problema" (vale a dire 4 voci del log trans e il tempo necessario per farlo). Tuttavia, nell'approccio "serializzabile", anche se tutti gli inserti sono unici, ci sarà sempre qualche possibilità di bloccare altri inserti nella stessa gamma (anche se non per molto tempo).

  2. Alta frequenza di richieste per lo stesso valore chiave contemporaneamente:

    In questo caso - un livello molto basso di unicità in termini di richieste in entrata per valori chiave inesistenti - il CATCHblocco nel mio suggerimento verrà inserito regolarmente. L'effetto di ciò sarà che ogni inserimento non riuscito dovrà eseguire il rollback automatico e scrivere le 4 voci nel registro delle transazioni, il che rappresenta un leggero calo delle prestazioni ogni volta. Ma l'operazione complessiva non dovrebbe mai fallire (almeno non per questo).

    (Si è verificato un problema con la versione precedente dell'approccio "aggiornato" che gli ha permesso di soffrire di deadlock. Un updlocksuggerimento è stato aggiunto per risolvere questo problema e non ottiene più deadlock.)MA, nell'approccio "serializzabile" (anche la versione aggiornata e ottimizzata), l'operazione si bloccherà. Perché? Perché il serializablecomportamento impedisce solo INSERToperazioni nell'intervallo che è stato letto e quindi bloccato; non impedisce SELECToperazioni su tale intervallo.

    L' serializableapproccio, in questo caso, sembrerebbe non avere spese generali aggiuntive e potrebbe comportare prestazioni leggermente migliori rispetto a quanto sto suggerendo.

Come con molte / la maggior parte delle discussioni relative alle prestazioni, a causa della presenza di così tanti fattori che possono influenzare il risultato, l'unico modo per avere davvero un'idea di come si comporterà qualcosa è provarlo nell'ambiente di destinazione in cui verrà eseguito. A quel punto non sarà una questione di opinione :).


7

Risposta aggiornata


Risposta a @srutzky

Un altro, sebbene minore, problema con questo approccio "transazione serializzabile + clausola OUTPUT" è che la clausola OUTPUT (nel suo utilizzo attuale) rispedisce i dati come set di risultati. Un set di risultati richiede più overhead (probabilmente su entrambi i lati: in SQL Server per gestire il cursore interno e nel livello app per gestire l'oggetto DataReader) rispetto a un semplice parametro OUTPUT. Dato che abbiamo a che fare solo con un singolo valore scalare e che il presupposto è un'alta frequenza di esecuzioni, probabilmente il costo aggiuntivo del set di risultati si sommerà.

Sono d'accordo, e per gli stessi motivi utilizzo prudentemente i parametri di output . È stato un mio errore non usare un parametro di output nella mia risposta iniziale, ero pigro.

Ecco una procedura rivista che utilizza un parametro di output, ulteriori ottimizzazioni, e next value forche @srutzky spiega nella sua risposta :

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;

nota di aggiornamento : includendo updlockcon la selezione verranno acquisiti i blocchi corretti in questo scenario. Grazie a @srutzky, che ha sottolineato che ciò potrebbe causare deadlock quando si utilizza solo serializablesu select.

Nota: questo potrebbe non essere il caso, ma se è possibile la procedura verrà chiamata con un valore per @vValueId, include set @vValueId = null;after set xact_abort on;, altrimenti può essere rimossa.


Per quanto riguarda gli esempi di @ srutzky sul comportamento del blocco della gamma di tasti:

@srutzky usa solo un valore nella sua tabella e blocca la chiave "next" / "infinity" per i suoi test per illustrare il blocco della gamma di chiavi. Mentre i suoi test illustrano cosa succede in quelle situazioni, credo che il modo in cui le informazioni sono presentate potrebbe portare a false assunzioni sulla quantità di blocco che ci si potrebbe aspettare di incontrare quando si utilizza serializablenello scenario come presentato nella domanda originale.

Anche se percepisco un pregiudizio (forse falsamente) nel modo in cui presenta la sua spiegazione ed esempi di blocco della gamma di tasti, sono ancora corretti.


Dopo ulteriori ricerche, ho trovato un articolo di blog particolarmente pertinente del 2011 di Michael J. Swart: Mythbusting: Concurrent Update / Insert Solutions . In esso, verifica diversi metodi per l'accuratezza e la concorrenza. Metodo 4: aumento dell'isolamento + fine tuning Locks si basa sul post Inserisci o Update Pattern di Sam Saffron per SQL Server , e l'unico metodo nel test originale per soddisfare le sue aspettative (più avanti merge with (holdlock)).

Nel febbraio del 2016, Michael J. Swart ha pubblicato Brutto pragmatismo per la vittoria . In quel post, copre alcune ulteriori modifiche apportate alle sue procedure di zafferano Saffron per ridurre il bloccaggio (che ho incluso nella procedura sopra).

Dopo aver apportato tali modifiche, Michael non era felice che la sua procedura stesse iniziando a sembrare più complicata e consultata con un collega di nome Chris. Chris ha letto tutti i post di Mythbusters originali, ha letto tutti i commenti e ha chiesto informazioni sul modello TRY CATCH JFDI di @ gbn . Questo modello è simile alla risposta di @srutzky ed è la soluzione che Michael ha finito per usare in quell'istanza.

Michael J Swart:

Ieri ho cambiato idea sul modo migliore di fare concorrenza. Descrivo diversi metodi in Mythbusting: soluzioni simultanee di aggiornamento / inserimento. Il mio metodo preferito è aumentare il livello di isolamento e ottimizzare i blocchi.

Almeno questa era la mia preferenza. Di recente ho cambiato il mio approccio per usare un metodo suggerito da gbn nei commenti. Descrive il suo metodo come "modello JFDI TRY CATCH". Normalmente evito soluzioni del genere. C'è una regola empirica che dice che gli sviluppatori non dovrebbero fare affidamento sulla rilevazione di errori o eccezioni per il flusso di controllo. Ma ieri ho infranto quella regola empirica.

A proposito, adoro la descrizione di gbn per il modello "JFDI". Mi ricorda il video motivazionale di Shia Labeouf.


A mio avviso, entrambe le soluzioni sono praticabili. Mentre preferisco ancora aumentare il livello di isolamento e ottimizzare i blocchi, la risposta di @ srutzky è anche valida e potrebbe o meno essere più performante nella tua situazione specifica.

Forse in futuro arriverò anch'io alla stessa conclusione di Michael J. Swart, ma non ci sono ancora.


Non è la mia preferenza, ma ecco come sarebbe il mio adattamento dell'adattamento di Michael J. Stewart alla procedura Try Catch JFDI di @ 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;

Se stai inserendo nuovi valori più spesso della selezione di valori esistenti, questo potrebbe essere più performante della versione di @ srutzky . Altrimenti preferirei la versione di @ srutzky rispetto a questa.

I commenti di Aaron Bertrand sul post di Michael J Swart si collegano ai test pertinenti che ha condotto e condotto a questo scambio. Estratto dalla sezione commenti su Brutto pragmatismo per la vittoria :

A volte, tuttavia, JFDI porta a prestazioni complessivamente peggiori, a seconda della percentuale di chiamate non riuscite. Sollevare eccezioni ha un notevole sovraccarico. L'ho mostrato in un paio di post:

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/

Commento di Aaron Bertrand - 11 febbraio 2016 @ 11:49

e la risposta di:

Hai ragione Aaron e l'abbiamo provato.

Si scopre che nel nostro caso, la percentuale di chiamate non riuscite era 0 (quando arrotondata alla percentuale più vicina).

Penso che tu illustri il punto che, per quanto possibile, valuta le cose caso per caso rispetto alle seguenti regole pratiche.

È anche il motivo per cui abbiamo aggiunto la clausola WHERE NOT EXISTS non strettamente necessaria.

Commento di Michael J. Swart - 11 febbraio 2016 @ 11:57


Nuovi collegamenti:


Risposta originale


Preferisco ancora l' approccio upsert di Sam Saffron rispetto all'uso merge, soprattutto quando si tratta di una singola riga.

Adatterò questo metodo di rialzo a questa situazione in questo modo:

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;

Sarei coerente con il tuo nome e, come serializableè lo stesso holdlock, sceglierne uno e essere coerente nel suo utilizzo. Tendo a usare serializableperché è lo stesso nome usato per specificare set transaction isolation level serializable.

Usando serializableo holdlockviene preso un blocco dell'intervallo in base al valore @vNameche fa attendere qualsiasi altra operazione se selezionano o inseriscono valori dbo.NameLookupche includono il valore nella whereclausola.

Affinché il blocco dell'intervallo funzioni correttamente, è necessario che sia presente un indice sulla ItemNamecolonna che si applica anche durante l'utilizzo merge.


Ecco come dovrebbe apparire la procedura principalmente seguendo i white paper di Erland Sommarskog per la gestione degli errori , utilizzando throw. Se thrownon è il modo in cui stai generando i tuoi errori, modificalo per renderlo coerente con il resto delle tue procedure:

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

Riassumendo cosa sta succedendo nella procedura sopra: set nocount on; set xact_abort on;come fai sempre , quindi se la nostra variabile di input is nullo vuota, select id = cast(null as int)come risultato. Se non è nullo o vuoto, ottieni la Idnostra variabile mentre tieni quel punto nel caso in cui non ci fosse. Se Idc'è, invialo. Se non è presente, inseriscilo e invialo nuovo Id.

Nel frattempo, altre chiamate a questa procedura che tentano di trovare l'ID per lo stesso valore attenderanno fino al completamento della prima transazione, quindi selezioneranno e restituiranno. Altre chiamate a questa procedura o altre istruzioni alla ricerca di altri valori continueranno perché questa non è di ostacolo.

Mentre sono d'accordo con @srutzky che puoi gestire le collisioni e ingoiare le eccezioni per questo tipo di problema, personalmente preferisco provare e personalizzare una soluzione per evitare di farlo quando possibile. In questo caso, non credo che usare i lucchetti serializablesia un approccio pesante, e sarei sicuro che gestirà bene l'alta concorrenza.

Citazione dalla documentazione del server sql sulla tabella suggerimenti serializable/holdlock :

SERIALIZABLE

È equivalente a HOLDLOCK. Rende i blocchi condivisi più restrittivi tenendoli fino al completamento di una transazione, invece di rilasciare il blocco condiviso non appena la tabella o la pagina di dati richiesta non è più necessaria, indipendentemente dal fatto che la transazione sia stata completata o meno. La scansione viene eseguita con la stessa semantica di una transazione in esecuzione al livello di isolamento SERIALIZZABILE. Per ulteriori informazioni sui livelli di isolamento, vedere IMPOSTARE LIVELLO DI ISOLAMENTO DELLE TRANSAZIONI (Transact-SQL).

Citazione dalla documentazione del server sql a livello di isolamento delle transazioniserializable

SERIALIZZABILE Specifica quanto segue:

  • Le dichiarazioni non possono leggere i dati che sono stati modificati ma non ancora impegnati da altre transazioni.

  • Nessun'altra transazione può modificare i dati letti dalla transazione corrente fino al completamento della transazione corrente.

  • Altre transazioni non possono inserire nuove righe con valori chiave che rientrerebbero nell'intervallo di chiavi letto da qualsiasi istruzione nella transazione corrente fino al completamento della transazione corrente.


Link relativi alla soluzione sopra:

MERGEha una storia macchiata e sembra che ci sia bisogno di più frugare in giro per assicurarsi che il codice si comporti come lo desideri in tutta quella sintassi. mergeArticoli rilevanti :

Un ultimo collegamento, Kendra Little ha fatto un confronto approssimativo di mergevsinsert with left join , con l'avvertenza in cui dice "Non ho fatto test di carico approfonditi su questo", ma è ancora una buona lettura.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.