Verifique se existe uma linha, caso contrário, insira


237

Eu preciso escrever um procedimento armazenado T-SQL que atualize uma linha em uma tabela. Se a linha não existir, insira-a. Todas essas etapas estão envolvidas por uma transação.

Isso é para um sistema de reservas, portanto deve ser atômico e confiável . Ele deve retornar verdadeiro se a transação foi confirmada e o voo reservado.

Eu sou novo no T-SQL e não tenho certeza de como usá-lo @@rowcount. Isto é o que eu escrevi até agora. Estou no caminho certo? Tenho certeza que é um problema fácil para você.

-- 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?)


Respostas:


158

Dê uma olhada no comando MERGE . Você pode fazer UPDATE, INSERT& DELETEem uma declaração.

Aqui está uma implementação funcional sobre o uso MERGE
- Ele verifica se o vôo está cheio antes de fazer uma atualização, caso contrário, faz uma inserção.

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 depois ...

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
Além disso, veja por que você pode gostar de WITH (HOLDLOCK) para esse MERGE.
Eugene Ryabtsev

4
Eu acho que o MERGE é suportado depois de 2005 (mais de 2008).
samis 4/12/13

3
MERGE sem WITH (UPDLOCK) pode ter violações de chave primária, o que seria ruim nesse caso. Veja [É MERGE uma declaração atômica em SQL2008?] ( Stackoverflow.com/questions/9871644/... )
James

156

Presumo uma única linha para cada voo? Se então:

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

Suponho o que eu disse, pois sua maneira de fazer as coisas pode reservar em excesso um voo, pois inserirá uma nova linha quando houver 10 bilhetes no máximo e você fizer 20 reservas.


Sim. Há 1 linha por voo. Mas seu código faz o SELECT, mas não verifica se o voo está cheio antes de ATUALIZAR. Como fazer isso?

2
Devido às condições de corrida, só é correto se o nível de isolamento da transação atual for Serializable.
Jarek Przygódzki 27/09/11

1
@ Martin: A resposta foi focada na pergunta em questão. Da própria declaração do OP "Todas essas etapas estão envolvidas por uma transação". Se a transação for implementada corretamente, o problema de segurança do encadeamento não deve ser um problema.
Gregory A Beamer

14
@ GregoryABeamer - Simplesmente colocá-lo em um BEGIN TRAN ... COMMITnível de isolamento abaixo do padrão não resolverá o problema. O OP especificou que atômicos e confiáveis eram requisitos. Sua resposta falha ao resolver isso de qualquer forma.
Martin Smith

2
Isso seria seguro para threads se (UPDLOCK, HOLDLOCK) fosse adicionado ao SELECT IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id):?
Jim

67

Passe dicas de updlock, rowlock, holdlock ao testar a existência da linha.

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 */

A dica updlock força a consulta a receber um bloqueio de atualização na linha, se ela já existir, impedindo que outras transações a modifiquem até você confirmar ou retroceder.

A dica holdlock força a consulta a bloquear um intervalo, impedindo que outras transações adicionem uma linha que corresponda aos seus critérios de filtro até você confirmar ou retroceder.

A dica de bloqueio de linha força a granularidade de bloqueio ao nível da linha, em vez do nível da página padrão, para que sua transação não bloqueie outras transações que tentam atualizar linhas não relacionadas na mesma página (mas esteja ciente da troca entre contenção reduzida e aumento de sobrecarga de bloqueio - evite usar um grande número de bloqueios no nível de linha em uma única transação).

Consulte http://msdn.microsoft.com/en-us/library/ms187373.aspx para obter mais informações.

Observe que os bloqueios são tomados como as instruções que os executam são executados - invocar begin tran não dá imunidade a outra transação que bloqueia bloqueios em algo antes de você chegar a ele. Você deve tentar fatorar seu SQL para reter bloqueios pelo menor tempo possível, confirmando a transação o mais rápido possível (adquira tarde, libere antecipadamente).

Observe que os bloqueios no nível da linha podem ser menos eficazes se a sua PK for grande, pois o hash interno no SQL Server é degenerado para valores de 64 bits (valores de chave diferentes podem ser hash para o mesmo ID de bloqueio).


4
O bloqueio é MUITO importante para evitar reservas em excesso. É correto supor que um bloqueio declarado na instrução IF seja mantido até o final da instrução IF, ou seja, para uma instrução de atualização? Pode ser aconselhável mostrar o código acima usando marcadores de início e fim para impedir que os novatos copiem e colem seu código e ainda assim estejam errados.
Simon B.

Existe um problema se meu PK for um varchar (embora não seja máximo) ou uma combinação de três colunas VARCHAR?
Steam

Fiz uma pergunta relacionada a esta resposta em - stackoverflow.com/questions/21945850/… Pergunta: esse código pode ser usado para inserir milhões de linhas.
Steam

Essa solução imporia muita sobrecarga de bloqueio nos casos em que muitos threads frequentemente testam linhas já existentes. Eu acho que isso pode ser resolvido com um tipo de bloqueio verificado duas vezes por meio de verificação extra preventiva existssem dicas de bloqueio.
Vadzim

38

estou escrevendo minha solução. meu método não é 'se' ou 'mesclar'. meu método é fácil.

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

Por exemplo:

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

Explicação:

(1) SELECT col1, col2 FROM TableName WHERE col1 = @ par1 AND col2 = @ par2 Seleciona os valores pesquisados ​​do TableName

(2) SELECT @ par1, @ par2 ONDE NÃO EXISTE Demora se não existir na (1) subconsulta

(3) Insere os valores das etapas TableName (2)


1
é apenas para inserir, não atualizar.
Cem

Na verdade, ainda é possível que esse método falhe, pois a verificação da existência é feita antes da inserção - consulte stackoverflow.com/a/3790757/1744834
Roman Pekar

3

Finalmente, consegui inserir uma linha, com a condição de ela ainda não existir, usando o seguinte modelo:

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 
    )
)

que eu encontrei em:

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


1
Este é um link copiar-colar apenas para resposta ... mais adequado como comentário.
31316 Ian

2

Isso é algo que recentemente tive que fazer:

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

Você pode usar a funcionalidade de mesclagem para alcançar. Caso contrário, você pode fazer:

declare @rowCount int

select @rowCount=@@RowCount

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

0

A solução completa está abaixo (incluindo a estrutura do cursor). Muito obrigado a Cassius Porcus pelo begin trans ... commitcódigo da postagem acima.

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

INSERT INTO table (coluna1, coluna2, coluna3) SELECT $ column1, $ column2, $ column3 EXCEPT SELECT coluna1, coluna2, coluna3 da tabela
Aaron

1
Há muitas respostas altamente votadas para esta pergunta. Você poderia explicar o que essa resposta adiciona às respostas existentes?
francis

-2

A melhor abordagem para esse problema é primeiro tornar a coluna do banco de dados ÚNICA

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name , o valor não será inserido se resultar em uma chave duplicada / já existir na tabela.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.