2010-08-29 14 views
11

Voglio fare la versione SELECT/INSERT di un UPSERT. Di seguito è riportato un modello del codice esistente:Seleziona/Inserisci versione di un Upsert: esiste un modello di progettazione per alta concorrenza?

// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50)) 

IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE) 
BEGIN 
    INSERT Table VALUES (@Value) 
    SELECT @id = SCOPEIDENTITY() 
END 
ELSE 
    SELECT @id = RowID FROM Table WHERE RowValue = @VALUE) 

La query verrà chiamata da molte sessioni simultanee. I miei test delle prestazioni mostrano che genererà costantemente violazioni delle chiavi primarie sotto un carico specifico.

Esiste un metodo ad alta concorrenza per questa query che consentirà di mantenere le prestazioni evitando comunque l'inserimento di dati già esistenti?

+1

Questo è simile a: http://stackoverflow.com/questions/13540/insert-update-stored-proc-on-sql-server/193876#193876 –

+0

Sono d'accordo è simile, ma direi che la differenza è che non è necessario un aggiornamento, solo un inserimento o una selezione.Nella tua risposta, usi SERIALIZABLE come suggerimento congiunto. Potrebbe essere la tua raccomandazione per la query sopra sulla dichiarazione SELECT? – 8kb

+0

Appare un inserto con l'hint serializzabile, potenzialmente in una transazione dovrebbe fare il trucco. Posso provare a scrivere una risposta ma questo iPad mi fa sembrare sfacciato :( –

risposta

15

È possibile utilizzare LOCK per rendere le cose SERIALIZZABILI ma ciò riduce la concorrenza. Perché non provare prima la condizione comune ("principalmente inserire o selezionare per lo più") seguita da una gestione sicura dell'azione "correttiva"? Cioè, il pattern "JFDI" ...

Per lo più INSERT attesi (Ball Park 70-80% +):

Basta provare a inserire. Se fallisce, la riga è già stata creata. Non c'è bisogno di preoccuparsi della concorrenza perché TRY/CATCH si occupa di duplicati per te.

BEGIN TRY 
    INSERT Table VALUES (@Value) 
    SELECT @id = SCOPEIDENTITY() 
END TRY 
BEGIN CATCH 
    IF ERROR_NUMBER() <> 2627 
     RAISERROR etc 
    ELSE -- only error was a dupe insert so must already have a row to select 
     SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
END CATCH 

Per lo più scegliere il:

simili, ma cercare di ottenere i dati prima. Nessun dato = INSERT necessario. Anche in questo caso, se 2 chiamate simultanee tentano di INSERIRE perché entrambi hanno trovato la riga mancante degli handle TRY/CATCH.

BEGIN TRY 
    SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
    IF @@ROWCOUNT = 0 
    BEGIN 
     INSERT Table VALUES (@Value) 
     SELECT @id = SCOPEIDENTITY() 
    END 
END TRY 
BEGIN CATCH 
    IF ERROR_NUMBER() <> 2627 
     RAISERROR etc 
    ELSE 
     SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
END CATCH 

Il 2 ° sembra ripetersi, ma è altamente concomitante. Serrature sarebbero ottenere lo stesso, ma a scapito di concorrenza ...

Edit:

Perché non usare MERGE ...

se si utilizza la clausola OUTPUT sarà solo ritorno cosa viene aggiornato. Quindi è necessario un dummy UPDATE per generare la tabella INSERTED per la clausola OUTPUT. Se si devono eseguire aggiornamenti fittizi con molte chiamate (come implicito dall'OP), molte scritture dei registri devono essere solo per poter utilizzare MERGE.

+0

@gbn - perché dovresti usare il tuo secondo suggerimento (per alta concorrenza) invece del comando MERGE (assumendo che @ 8kb stia usando sql 20 08+)? –

+0

@ Pure.Krome: perché è inserimento/selezione, non inserimento/aggiornamento. Ti ritroverai con un dummy UPDATE per usare OUTPUT. Grande. – gbn

+0

@gbn - quindi non possiamo controllare SCOPE_IDENTITY per afferrare l'identità _last_/_most recent_ che è stata inserita .. supponendo che sia stato inserito un inserto? se così non fosse, allora puoi usare @@ Rowcount per verificare che l'aggiornamento abbia funzionato ... ?? –

1
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50)) 

- assicurarsi di disporre di un indice univoco non raggruppato su RowValue e RowID come indice cluster.

IF EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE) 
    SELECT @id = RowID FROM Table WHERE RowValue = @VALUE 
ELSE BEGIN 
    INSERT Table VALUES (@Value) 
    SELECT @id = SCOPEIDENTITY() 
END 
0

Come sempre, la risposta di GBN è corretto e, infine, mi portano a dove avevo bisogno di essere. Tuttavia, ho trovato un caso limite che non era coperto dal suo approccio. Questo è un errore 2601 che identifica un Unique Index Violation.

Per compensare questo, ho modificato il suo codice come seguire

... 
declare @errornumber int = ERROR_NUMBER() 
if @errornumber <> 2627 and @errornumber <> 2601 
... 

Speriamo che questo aiuta qualcuno!

Problemi correlati