Poiché si utilizza una sequenza, è possibile utilizzare la stessa funzione VALORE SUCCESSIVO - già presente in un vincolo predefinito nel Id
campo Chiave primaria - per generare in Id
anticipo 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 OUTPUT
clausola né di fare un ulteriore SELECT
per 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...CATCH
costrutto, puoi effettivamente intercettare un errore specifico (in questo caso: "violazione del vincolo univoco", messaggio 2601) e rieseguire SELECT
il Id
valore per ottenere il valore poiché sappiamo che ora esiste a causa dell'essere nel CATCH
blocco con quel particolare errore. Altri errori possono essere gestiti nel modo tipico RAISERROR
/ RETURN
o 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 TRY
utilizzare la WHERE NOT EXISTS
clausola?
MERGE
presenta 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 è:
- Se hai abbastanza esecuzioni di questa procedura in modo tale che devi preoccuparti delle collisioni, allora non vuoi:
- fare più passi del necessario
- tenere blocchi su qualsiasi risorsa più a lungo del necessario
- Poiché le collisioni possono verificarsi solo su nuove voci (nuove voci inviate nello stesso momento ), la frequenza di cadere nel
CATCH
blocco 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 serializable
sia 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 OUTPUT
clausola (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 OUTPUT
parametro. 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 OUTPUT
clausola potrebbe essere utilizzata in modo da restituire un OUTPUT
parametro, 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 OUTPUT
parametro.
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 serializable
nello scenario come presentato nella domanda originale.
Sì, ammetterò di essere di parte, sebbene sia giusto:
- È impossibile per un essere umano non essere di parte, almeno in minima parte, e cerco di tenerlo al minimo,
- 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.
- 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").
- Ho trovato (e offerto volontariamente) il "costo nascosto" del mio approccio, ovvero le quattro voci extra del Registro di Tran ogni volta che
INSERT
falliscono 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 SELECT
il ID
valore per ottenere i record esistenti. Questo SELECT funge da IF EXISTS
controllo, 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 EXISTS
di 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 CATCH
blocco così spesso. Potrebbero essere solo situazioni in cui due sessioni,ItemName
INSERT...SELECT
nello stesso preciso istante in modo tale che entrambe le sessioni ricevano un "vero" per lo WHERE NOT EXISTS
stesso preciso momento e quindi entrambi tentano di fare INSERT
esattamente nello stesso momento. Questo scenario molto specifico si verifica molto meno spesso rispetto alla selezione di uno esistente ItemName
o all'inserimento di un nuovo ItemName
quando 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
, G
e M
.
D
, quindi l'intervallo # 2 (da C
a F
) è bloccato: la sessione 56 non può inserire un valore di E
(ancora). Ma sessione di 56 può inserire valori di A
, G
e M
.
M
, quindi l'intervallo # 4 (da J
a $
) è bloccato: la sessione 56 non può inserire un valore di X
(ancora). Ma sessione di 56 può inserire valori di A
, D
e 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:
Tutte le richieste riguardano valori chiave univoci:
In questo caso, il CATCH
blocco 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).
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 CATCH
blocco 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 updlock
suggerimento è 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 serializable
comportamento impedisce solo INSERT
operazioni nell'intervallo che è stato letto e quindi bloccato; non impedisce SELECT
operazioni su tale intervallo.
L' serializable
approccio, 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 :).