Controlla se esiste una riga, altrimenti inserisci


237

Devo scrivere una procedura memorizzata T-SQL che aggiorna una riga in una tabella. Se la riga non esiste, inserirla. Tutti questi passaggi sono racchiusi in una transazione.

Questo è per un sistema di prenotazione, quindi deve essere atomico e affidabile . Deve restituire true se la transazione è stata impegnata e il volo prenotato.

Sono nuovo di T-SQL e non sono sicuro di come utilizzarlo @@rowcount. Questo è quello che ho scritto fino ad ora. Sono sulla strada giusta? Sono sicuro che sia un problema facile per te.

-- BEGIN TRANSACTION (HOW TO DO?)

UPDATE Bookings
 SET TicketsBooked = TicketsBooked + @TicketsToBook
 WHERE FlightId = @Id AND TicketsMax < (TicketsBooked + @TicketsToBook)

-- Here I need to insert only if the row doesn't exists.
-- If the row exists but the condition TicketsMax is violated, I must not insert 
-- the row and return FALSE

IF @@ROWCOUNT = 0 
BEGIN

 INSERT INTO Bookings ... (omitted)

END

-- END TRANSACTION (HOW TO DO?)

-- Return TRUE (How to do?)


Risposte:


158

Dai un'occhiata al comando MERGE . Puoi farlo UPDATE, INSERTe DELETEin una frase.

Ecco un'implementazione funzionante sull'uso MERGE
- Controlla se il volo è pieno prima di fare un aggiornamento, altrimenti fa un inserimento.

if exists(select 1 from INFORMATION_SCHEMA.TABLES T 
              where T.TABLE_NAME = 'Bookings') 
begin
    drop table Bookings
end
GO

create table Bookings(
  FlightID    int identity(1, 1) primary key,
  TicketsMax    int not null,
  TicketsBooked int not null
)
GO

insert  Bookings(TicketsMax, TicketsBooked) select 1, 0
insert  Bookings(TicketsMax, TicketsBooked) select 2, 2
insert  Bookings(TicketsMax, TicketsBooked) select 3, 1
GO

select * from Bookings

E poi ...

declare @FlightID int = 1
declare @TicketsToBook int = 2

--; This should add a new record
merge Bookings as T
using (select @FlightID as FlightID, @TicketsToBook as TicketsToBook) as S
    on  T.FlightID = S.FlightID
      and T.TicketsMax > (T.TicketsBooked + S.TicketsToBook)
  when matched then
    update set T.TicketsBooked = T.TicketsBooked + S.TicketsToBook
  when not matched then
    insert (TicketsMax, TicketsBooked) 
    values(S.TicketsToBook, S.TicketsToBook);

select * from Bookings

6
Inoltre, vedi perché ti potrebbe piacere WITH (HOLDLOCK) per quel MERGE.
Eugene Ryabtsev il

4
Penso che MERGE sia supportato dopo il 2005 (quindi 2008+).
samis,

3
MERGE senza WITH (UPDLOCK) può avere violazioni della chiave primaria, che in questo caso sarebbe male. Vedere [è MERGE un'istruzione atomica in SQL2008?] ( Stackoverflow.com/questions/9871644/... )
James

156

Presumo una singola riga per ogni volo? Se è così:

IF EXISTS (SELECT * FROM Bookings WHERE FLightID = @Id)
BEGIN
    --UPDATE HERE
END
ELSE
BEGIN
   -- INSERT HERE
END

Presumo che cosa ho detto, poiché il tuo modo di fare le cose può prenotare un volo in eccesso, poiché inserirà una nuova riga quando ci saranno 10 biglietti al massimo e ne prenoterai 20.


Sì. C'è 1 fila per volo. Ma il tuo codice esegue SELECT ma non controlla se il volo è pieno prima dell'aggiornamento. Come fare questo?

2
A causa delle condizioni di gara è corretto solo se l'attuale livello di isolamento della transazione è serializzabile.
Jarek Przygódzki,

1
@Martin: la risposta era focalizzata sulla domanda in corso. Dalla stessa dichiarazione del PO "Tutti questi passaggi racchiusi in una transazione". Se la transazione è implementata correttamente, il problema di thread thread non dovrebbe essere un problema.
Gregory A Beamer,

14
@GregoryABeamer - Il semplice inserimento in un BEGIN TRAN ... COMMITlivello di isolamento predefinito non risolverà il problema. L'OP ha specificato che atomica e affidabilità erano requisiti. La tua risposta non riesce a risolverlo in qualsiasi forma o forma.
Martin Smith,

2
Questo sarebbe thread-safe se (UPDLOCK, HOLDLOCK) è stata aggiunta al SELECT: IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)?
Jim,

67

Passa i suggerimenti di updlock, rowlock e holdlock quando si verifica l'esistenza della riga.

begin tran /* default read committed isolation level is fine */

if not exists (select * from Table with (updlock, rowlock, holdlock) where ...)
    /* insert */
else
    /* update */

commit /* locks are released here */

Il suggerimento di updlock impone alla query di eseguire un blocco di aggiornamento sulla riga se esiste già, impedendo ad altre transazioni di modificarlo fino al momento del commit o del rollback.

Il suggerimento holdlock impone alla query di eseguire un blocco dell'intervallo, impedendo ad altre transazioni di aggiungere una riga corrispondente ai criteri del filtro fino al momento del commit o del rollback.

Il suggerimento del rowlock impone il blocco della granularità al livello di riga anziché al livello di pagina predefinito, quindi la tua transazione non bloccherà altre transazioni tentando di aggiornare le righe non correlate nella stessa pagina (ma fai attenzione al compromesso tra contesa ridotta e aumento di overhead di blocco: dovresti evitare di eseguire un numero elevato di blocchi a livello di riga in una singola transazione).

Vedi http://msdn.microsoft.com/en-us/library/ms187373.aspx per ulteriori informazioni.

Nota che i blocchi vengono presi mentre vengono eseguite le istruzioni che li prendono - invocare begin tran non ti dà l'immunità contro un'altra transazione che blocca i blocchi su qualcosa prima di arrivare ad esso. Dovresti provare a fare in modo che il tuo SQL mantenga i blocchi il più breve tempo possibile eseguendo il commit della transazione il più presto possibile (acquisisci in ritardo, rilascialo in anticipo).

Nota che i blocchi a livello di riga potrebbero essere meno efficaci se il tuo PK è un bigint, poiché l'hash interno su SQL Server è degenerato per valori a 64 bit (valori di chiave diversi possono essere associati allo stesso ID di blocco).


4
Il blocco è MOLTO importante per evitare l'overbooking. È corretto supporre che un blocco dichiarato nell'istruzione IF venga mantenuto fino alla fine dell'istruzione IF, ovvero per un'istruzione di aggiornamento? Quindi potrebbe essere saggio mostrare il codice sopra usando gli indicatori di inizio blocco per impedire ai neofiti di copiare e incollare il codice e ancora sbagliare.
Simon B.

C'è un problema se il mio PK è un varchar (NON massimo però) o una combinazione di tre colonne VARCHAR?
Steam,

Ho fatto una domanda relativa a questa risposta all'indirizzo - stackoverflow.com/questions/21945850/…. La domanda è : è possibile utilizzare questo codice per inserire milioni di righe.
Steam,

Questa soluzione imporrebbe un sovraccarico eccessivo di blocco nei casi in cui molti thread testano spesso righe già esistenti. Immagino che questo possa essere risolto con una sorta di doppio controllo controllato tramite controllo preventivo preventivo existssenza suggerimenti di blocco.
Vadzim,

38

sto scrivendo la mia soluzione. il mio metodo non regge 'if' o 'merge'. il mio metodo è semplice.

INSERT INTO TableName (col1,col2)
SELECT @par1, @par2
   WHERE NOT EXISTS (SELECT col1,col2 FROM TableName
                     WHERE col1=@par1 AND col2=@par2)

Per esempio:

INSERT INTO Members (username)
SELECT 'Cem'
   WHERE NOT EXISTS (SELECT username FROM Members
                     WHERE username='Cem')

Spiegazione:

(1) SELEZIONA col1, col2 DA TableName DOVE col1 = @ par1 AND col2 = @ par2 Seleziona dai valori cercati da TableName

(2) SELEZIONA @ par1, @ par2 DOVE NON ESISTE Prende se non esiste dalla (1) sottoquery

(3) Inserisce i valori del passo TableName (2)


1
è solo per l'inserimento, non per l'aggiornamento.
Cem,

In realtà è ancora possibile che questo metodo fallisca perché il controllo dell'esistenza viene eseguito prima dell'inserimento - vedi stackoverflow.com/a/3790757/1744834
Roman Pekar

3

Finalmente sono stato in grado di inserire una riga, a condizione che non esistesse già, utilizzando il seguente modello:

INSERT INTO table ( column1, column2, column3 )
(
    SELECT $column1, $column2, $column3
      WHERE NOT EXISTS (
        SELECT 1
          FROM table 
          WHERE column1 = $column1
          AND column2 = $column2
          AND column3 = $column3 
    )
)

che ho trovato su:

http://www.postgresql.org/message-id/87hdow4ld1.fsf@stark.xeocode.com


1
Questo è un link copia-incolla solo risposta ... più adatto come commento.
Ian,

2

Questo è qualcosa che ho dovuto fare di recente:

set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[cjso_UpdateCustomerLogin]
    (
      @CustomerID AS INT,
      @UserName AS VARCHAR(25),
      @Password AS BINARY(16)
    )
AS 
    BEGIN
        IF ISNULL((SELECT CustomerID FROM tblOnline_CustomerAccount WHERE CustomerID = @CustomerID), 0) = 0
        BEGIN
            INSERT INTO [tblOnline_CustomerAccount] (
                [CustomerID],
                [UserName],
                [Password],
                [LastLogin]
            ) VALUES ( 
                /* CustomerID - int */ @CustomerID,
                /* UserName - varchar(25) */ @UserName,
                /* Password - binary(16) */ @Password,
                /* LastLogin - datetime */ NULL ) 
        END
        ELSE
        BEGIN
            UPDATE  [tblOnline_CustomerAccount]
            SET     UserName = @UserName,
                    Password = @Password
            WHERE   CustomerID = @CustomerID    
        END

    END

1

È possibile utilizzare la funzionalità Unisci per raggiungere. Altrimenti puoi fare:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....

0

La soluzione completa è sotto (inclusa la struttura del cursore). Mille grazie a Cassius Porcus per il begin trans ... commitcodice pubblicato sopra.

declare @mystat6 bigint
declare @mystat6p varchar(50)
declare @mystat6b bigint

DECLARE mycur1 CURSOR for

 select result1,picture,bittot from  all_Tempnogos2results11

 OPEN mycur1

 FETCH NEXT FROM mycur1 INTO @mystat6, @mystat6p , @mystat6b

 WHILE @@Fetch_Status = 0
 BEGIN

 begin tran /* default read committed isolation level is fine */

 if not exists (select * from all_Tempnogos2results11_uniq with (updlock, rowlock, holdlock)
                     where all_Tempnogos2results11_uniq.result1 = @mystat6 
                        and all_Tempnogos2results11_uniq.bittot = @mystat6b )
     insert all_Tempnogos2results11_uniq values (@mystat6 , @mystat6p , @mystat6b)

 --else
 --  /* update */

 commit /* locks are released here */

 FETCH NEXT FROM mycur1 INTO @mystat6 , @mystat6p , @mystat6b

 END

 CLOSE mycur1

 DEALLOCATE mycur1
 go

0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])

-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table

INSERISCI tabella (colonna1, colonna2, colonna3) SELEZIONA $ colonna1, $ colonna2, $ colonna3 SALVA SELEZIONA colonna1, colonna2, colonna3 dalla tabella
Aaron

1
Ci sono molte risposte molto votate a questa domanda. Potresti elaborare per spiegare ciò che questa risposta aggiunge alle risposte esistenti?
francis,

-2

L'approccio migliore a questo problema è innanzitutto rendere la colonna del database UNICA

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name , il valore non verrà inserito se risulta in una chiave duplicata / esiste già nella tabella.

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.