Insira Atualizar procedimento armazenado no SQL Server

104

Eu escrevi um procedimento armazenado que fará uma atualização se houver um registro, caso contrário, fará uma inserção. É mais ou menos assim:

update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

Minha lógica por trás de escrever desta forma é que a atualização executará uma seleção implícita usando a cláusula where e se retornar 0, então a inserção ocorrerá.

A alternativa para fazer dessa maneira seria fazer uma seleção e, a seguir, com base no número de linhas retornadas, fazer uma atualização ou uma inserção. Isso eu considerei ineficiente porque se você for fazer uma atualização causará 2 selects (a primeira chamada select explícita e a segunda implícita no where da atualização). Se o proc fizesse uma inserção, não haveria diferença na eficiência.

Minha lógica está soando aqui? É assim que você combinaria uma inserção e uma atualização em um procedimento armazenado?

Cara
fonte

Respostas:

61

Sua suposição está certa, esta é a maneira ideal de fazer isso e é chamada de upsert / merge .

Importância do UPSERT - de sqlservercentral.com :

Para cada atualização no caso mencionado acima, estamos removendo uma leitura adicional da tabela se usarmos o UPSERT em vez de EXISTS. Infelizmente para um Insert, os métodos UPSERT e IF EXISTS usam o mesmo número de leituras na tabela. Portanto, a verificação de existência só deve ser feita quando houver um motivo muito válido para justificar a E / S adicional. A maneira otimizada de fazer as coisas é garantir que você tenha o mínimo de leituras possíveis no banco de dados.

A melhor estratégia é tentar a atualização. Se nenhuma linha for afetada pela atualização, insira. Na maioria das circunstâncias, a linha já existirá e apenas uma E / S será necessária.

Editar : verifique esta resposta e a postagem do blog vinculada para saber mais sobre os problemas com esse padrão e como fazê-lo funcionar de forma segura.

binOr
fonte
1
Bem, pelo menos respondeu a uma pergunta, eu acho. E eu não adicionei o código porque o código na pergunta já parecia certo para mim. Embora eu o incluísse em uma transação, não levei em consideração o nível de isolamento para a atualização. Obrigado por apontar isso na sua resposta!
binOr
54

Leia a postagem em meu blog para ver um padrão bom e seguro que você pode usar. Há muitas considerações e a resposta aceita para essa pergunta está longe de ser segura.

Para uma resposta rápida, tente o seguinte padrão. Funcionará bem no SQL 2000 e superior. O SQL 2005 fornece tratamento de erros que abre outras opções e o SQL 2008 fornece um comando MERGE.

begin tran
   update t with (serializable)
   set hitCount = hitCount + 1
   where pk = @id
   if @@rowcount = 0
   begin
      insert t (pk, hitCount)
      values (@id,1)
   end
commit tran
Sam Saffron
fonte
1
Em sua postagem de blog, você conclui usando a dica WITH (updlock, serializable) na verificação de existência. No entanto, a leitura do MSDN indica: "UPDLOCK - Especifica que os bloqueios de atualização devem ser executados e mantidos até que a transação seja concluída." Isso significa que a dica serializável é supérflua, pois o bloqueio de atualização será mantido pelo restante da transação de qualquer maneira, ou eu entendi algo errado?
Dan Def
10

Se for usado com o SQL Server 2000/2005, o código original precisa ser incluído na transação para garantir que os dados permaneçam consistentes no cenário simultâneo.

BEGIN TRANSACTION Upsert
update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)
COMMIT TRANSACTION Upsert

Isso incorrerá em custos de desempenho adicionais, mas garantirá a integridade dos dados.

Adicione, como já sugerido, MERGE deve ser usado quando disponível.

Dima Malenko
fonte
8

MERGE é um dos novos recursos do SQL Server 2008, aliás.

Jon Galloway
fonte
e você deve absolutamente usá-lo, em vez desse absurdo homebrew difícil de ler. Um bom exemplo está aqui - mssqltips.com/sqlservertip/1704/…
Rich Bryant
6

Você não precisa apenas executá-lo na transação, mas também precisa de um alto nível de isolamento. Na verdade, o nível de isolamento padrão é Read Commited e esse código precisa Serializable.

SET transaction isolation level SERIALIZABLE
BEGIN TRANSACTION Upsert
UPDATE myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
  begin
    INSERT into myTable (ID, Col1, Col2) values (@ID @col1, @col2)
  end
COMMIT TRANSACTION Upsert

Talvez adicionar também a verificação de erro @@ e reversão possa ser uma boa ideia.

Tomas Tintera
fonte
@Munish Goyal Porque no banco de dados vários comandos e precedentes são executados em paralelo. Então, outro thread pode inserir uma linha logo após a atualização ser executada e antes da inserção ser executada.
Tomas Tintera
5

Se você não estiver fazendo uma mesclagem no SQL 2008, deve alterá-lo para:

se @@ rowcount = 0 e @@ error = 0

caso contrário, se a atualização falhar por algum motivo, ele tentará inserir depois porque o número de linhas em uma instrução com falha é 0

Simon Munro
fonte
3

Grande fã do UPSERT, realmente reduz o código para gerenciar. Aqui está outra maneira de fazer isso: Um dos parâmetros de entrada é o ID, se o ID for NULL ou 0, você sabe que é um INSERT, caso contrário, é uma atualização. Presume que o aplicativo sabe se há um ID, então não funcionará em todas as situações, mas reduzirá a execução pela metade se você o fizer.

Natron
fonte
2

Postagem modificada de Dima Malenko:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

BEGIN TRANSACTION UPSERT 

UPDATE MYTABLE 
SET    COL1 = @col1, 
       COL2 = @col2 
WHERE  ID = @ID 

IF @@rowcount = 0 
  BEGIN 
      INSERT INTO MYTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

IF @@Error > 0 
  BEGIN 
      INSERT INTO MYERRORTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

COMMIT TRANSACTION UPSERT 

Você pode interceptar o erro e enviar o registro para uma tabela de inserção com falha.
Eu precisava fazer isso porque estamos pegando todos os dados enviados via WSDL e, se possível, corrigindo-os internamente.

thughes78013
fonte
1

Sua lógica parece correta, mas você pode querer considerar a adição de algum código para evitar a inserção se você tiver passado uma chave primária específica.

Caso contrário, se você está sempre fazendo uma inserção se a atualização não afetou nenhum registro, o que acontece quando alguém exclui o registro antes de executar o "UPSERT"? Agora, o registro que você estava tentando atualizar não existe, então, em vez disso, será criado um registro. Esse provavelmente não é o comportamento que você estava procurando.

Kevin Fairchild
fonte