Soluções para INSERIR OU ATUALIZAR no SQL Server

598

Suponha uma estrutura de tabela de MyTable(KEY, datafield1, datafield2...).

Frequentemente, eu quero atualizar um registro existente ou inserir um novo registro, se ele não existir.

Essencialmente:

IF (key exists)
  run update command
ELSE
  run insert command

Qual é a melhor maneira de escrever isso?

Chris Cudmore
fonte
27
Para quem se deparar com essa pergunta pela primeira vez, leia todas as respostas e seus comentários. Idade pode levar a informações enganosas ...
Aaron Bertrand
1
Considere o uso do operador EXCEPT, que foi introduzido no SQL Server 2005.
Tarzan

Respostas:

370

não se esqueça de transações. O desempenho é bom, mas a abordagem simples (SE EXISTE ..) é muito perigosa.
Quando vários threads tentam executar a inserção ou atualização, você pode facilmente obter uma violação da chave primária.

As soluções fornecidas pelo @Beau Crawford e pelo @Esteban mostram idéias gerais, mas sujeitas a erros.

Para evitar conflitos e violações de PK, você pode usar algo como isto:

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert into table (key, ...)
   values (@key, ...)
end
commit tran

ou

begin tran
   update table with (serializable) set ...
   where key = @key

   if @@rowcount = 0
   begin
      insert into table (key, ...) values (@key,..)
   end
commit tran
aku
fonte
1
A pergunta pedia a solução com maior desempenho e não a mais segura. Enquanto uma transação adiciona segurança ao processo, também adiciona uma sobrecarga.
Luke Bennett
31
Ambos os métodos ainda podem falhar. Se dois encadeamentos simultâneos fizerem o mesmo na mesma linha, o primeiro terá êxito, mas a segunda inserção falhará devido a uma violação da chave primária. Uma transação não garante que a inserção seja bem-sucedida, mesmo que a atualização falhe porque o registro existe. Para garantir que qualquer número de transações simultâneas seja bem-sucedido, DEVE usar um bloqueio.
31410 Jean Jean
7
@aku por qualquer motivo que você tenha usado dicas de tabela ("com (xxxx)") em vez de "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE" pouco antes do seu BEGIN TRAN?
EBarr
4
@CashCow, as últimas vitórias, é isso que INSERT ou UPDATE deve fazer: o primeiro insere, o segundo atualiza o registro. A adição de um bloqueio permite que isso ocorra em um período muito curto, evitando erros.
Jean Vincent
1
Eu sempre pensei que o uso de dicas de bloqueio é ruim e devemos permitir que o mecanismo Microsoft Internal dite bloqueios. Essa é a aparente exceção à regra?
382

Veja minha resposta detalhada a uma pergunta anterior muito semelhante

O @Beau Crawford's é uma boa maneira no SQL 2005 e abaixo, embora se você estiver concedendo um representante, ele deve ir para o primeiro funcionário a fazê-lo . O único problema é que, para insertos, ainda são duas operações de E / S.

O MS Sql2008 apresenta a mergepartir do padrão SQL: 2003:

merge tablename with(HOLDLOCK) as target
using (values ('new value', 'different value'))
    as source (field1, field2)
    on target.idfield = 7
when matched then
    update
    set field1 = source.field1,
        field2 = source.field2,
        ...
when not matched then
    insert ( idfield, field1, field2, ... )
    values ( 7,  source.field1, source.field2, ... )

Agora é realmente apenas uma operação de E / S, mas um código terrível :-(

Keith
fonte
10
@ Ian Boyd - sim, essa é a sintaxe do padrão SQL: 2003, e não a upsertque quase todos os outros provedores de banco de dados decidiram dar suporte. A upsertsintaxe é uma maneira muito mais agradável para fazer isso, então no mínimo MS deveria ter suportado também - não é como se fosse a única palavra-chave não padrão em T-SQL
Keith
1
algum comentário sobre a dica de bloqueio em outras respostas? (vai descobrir em breve, mas se é a maneira recomendada, eu recomendável adicioná-lo à resposta)
eglasius
25
Consulte aqui weblogs.sqlteam.com/dang/archive/2009/01/31/… para obter respostas sobre como impedir que as condições de corrida causem erros que podem ocorrer mesmo ao usar a MERGEsintaxe.
Seph
5
@ Seph, isso é uma verdadeira surpresa - uma falha da Microsoft: - Acho que isso significa que você precisa HOLDLOCKde operações de mesclagem em situações de alta simultaneidade.
21412 Keith
11
Essa resposta realmente precisa ser atualizada para explicar o comentário de Seph sobre o fato de não ser seguro para threads sem um HOLDLOCK. De acordo com a postagem vinculada, o MERGE remove implicitamente um bloqueio de atualização, mas o libera antes de inserir linhas, o que pode causar uma condição de corrida e violações da chave primária na inserção. Usando HOLDLOCK, os bloqueios são mantidos até após a inserção.
Triynko
169

Faça um UPSERT:

UPDATE MyTable SET CampoA = @ CampoA WHERE Key = @ Key

SE @@ ROWCOUNT = 0
   INSERIR EM VALORES MyTable (FieldA) (@FieldA)

http://en.wikipedia.org/wiki/Upsert

Beau Crawford
fonte
7
As violações da chave primária não devem ocorrer se você tiver as restrições de índice exclusivas adequadas aplicadas. O ponto principal da restrição é impedir que linhas duplicadas ocorram. Não importa quantos threads estão tentando inserir, o banco de dados será serializado conforme necessário para impor a restrição ... e se não o fizer, o mecanismo será inútil. Obviamente, agrupar isso em uma transação serializada tornaria isso mais correto e menos suscetível a conflitos ou inserções com falha.
Triynko
19
@Triynko, acho que @Sam Saffron significava que se dois + threads se entrelaçam na sequência correta, o servidor sql lançará um erro indicando que uma violação de chave primária teria ocorrido. O agrupamento em uma transação serializável é a maneira correta de evitar erros no conjunto de instruções acima.
EBarr
1
Mesmo se você tiver uma chave primária que seja um incremento automático, sua preocupação será qualquer restrição exclusiva que possa estar na mesa.
Seph
1
o banco de dados deve cuidar de questões de chave primária. O que você está dizendo é que, se a atualização falhar e outro processo chegar primeiro com uma inserção, sua inserção falhará. Nesse caso, você tem uma condição de corrida de qualquer maneira. O bloqueio não muda o fato de que a pós-condição será que um dos processos que tenta escrever receberá o valor.
CashCow
93

Muitas pessoas sugerem que você use MERGE, mas eu o aviso contra isso. Por padrão, ele não protege você das condições de concorrência e corrida mais do que várias declarações e apresenta outros perigos:

http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/

Mesmo com essa sintaxe "mais simples" disponível, ainda prefiro essa abordagem (tratamento de erros omitido por questões de brevidade):

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END
COMMIT TRANSACTION;

Muitas pessoas sugerem o seguinte:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
  INSERT ...
END
COMMIT TRANSACTION;

Mas tudo isso é garantir que você precise ler a tabela duas vezes para localizar as linhas a serem atualizadas. No primeiro exemplo, você só precisará localizar as linhas uma vez. (Nos dois casos, se nenhuma linha for encontrada na leitura inicial, ocorrerá uma inserção.)

Outros irão sugerir desta maneira:

BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

No entanto, isso é problemático se, por nenhum outro motivo, deixar o SQL Server capturar exceções que você poderia ter evitado em primeiro lugar é muito mais caro, exceto no cenário raro em que quase todas as inserções falham. Eu provo isso aqui:

Aaron Bertrand
fonte
3
E a inserção / atualização de uma tabela temporária que insere / atualiza muitos registros?
User960567
@ user960567 Bem,UPDATE target SET col = tmp.col FROM target INNER JOIN #tmp ON <key clause>; INSERT target(...) SELECT ... FROM #tmp AS t WHERE NOT EXISTS (SELECT 1 FROM target WHERE key = t.key);
Aaron Bertrand
4
agradável respondeu depois de mais de 2 anos :)
user960567
12
@ user960567 Desculpe, eu nem sempre recebo notificações de comentários em tempo real.
Aaron Bertrand
60
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

Editar:

Infelizmente, mesmo para meu próprio prejuízo, devo admitir que as soluções que fazem isso sem um seleto parecem ser melhores, pois realizam a tarefa com um passo a menos.

Esteban Araya
fonte
6
Eu ainda gosto mais deste. O upsert parece mais com programação por efeito colateral, e eu nunca vi o pequeno índice clusterizado buscar aquela seleção inicial para causar problemas de desempenho em um banco de dados real.
Eric Z Beard
38

Se você deseja UPSERT mais de um registro por vez, pode usar a instrução DML ANSI SQL: 2003 MERGE.

MERGE INTO table_name WITH (HOLDLOCK) USING table_name ON (condition)
WHEN MATCHED THEN UPDATE SET column1 = value1 [, column2 = value2 ...]
WHEN NOT MATCHED THEN INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...])

Confira Imitando a instrução MERGE no SQL Server 2005 .

Eric Weilnau
fonte
1
No Oracle, emitindo uma instrução MERGE, acho que bloqueia a tabela. O mesmo acontece no SQL * Server?
Mike McAllister
13
O MERGE é suscetível a condições de corrida (consulte weblogs.sqlteam.com/dang/archive/2009/01/31/… ), a menos que você o faça reter certian bloqueios. Além disso, dê uma olhada no desempenho do MERGE no SQL Profiler ... acho que é tipicamente mais lento e gera mais leituras do que soluções alternativas.
EBarr
@ EBarr - Obrigado pelo link nos bloqueios. Atualizei minha resposta para incluir a sugestão de bloqueio.
Eric Weilnau
Também verifique mssqltips.com/sqlservertip/3074/…
Aaron Bertrand
10

Embora seja muito tarde para comentar sobre isso, quero adicionar um exemplo mais completo usando MERGE.

Essas instruções Insert + Update são geralmente chamadas de instruções "Upsert" e podem ser implementadas usando MERGE no SQL Server.

Um exemplo muito bom é dado aqui: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

O exposto acima também explica os cenários de bloqueio e simultaneidade.

Vou citar o mesmo para referência:

ALTER PROCEDURE dbo.Merge_Foo2
      @ID int
AS

SET NOCOUNT, XACT_ABORT ON;

MERGE dbo.Foo2 WITH (HOLDLOCK) AS f
USING (SELECT @ID AS ID) AS new_foo
      ON f.ID = new_foo.ID
WHEN MATCHED THEN
    UPDATE
            SET f.UpdateSpid = @@SPID,
            UpdateTime = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT
      (
            ID,
            InsertSpid,
            InsertTime
      )
    VALUES
      (
            new_foo.ID,
            @@SPID,
            SYSDATETIME()
      );

RETURN @@ERROR;
user243131
fonte
1
Há outras coisas com as quais se preocupar com o MERGE: mssqltips.com/sqlservertip/3074/…
Aaron Bertrand
8
/*
CREATE TABLE ApplicationsDesSocietes (
   id                   INT IDENTITY(0,1)    NOT NULL,
   applicationId        INT                  NOT NULL,
   societeId            INT                  NOT NULL,
   suppression          BIT                  NULL,
   CONSTRAINT PK_APPLICATIONSDESSOCIETES PRIMARY KEY (id)
)
GO
--*/

DECLARE @applicationId INT = 81, @societeId INT = 43, @suppression BIT = 0

MERGE dbo.ApplicationsDesSocietes WITH (HOLDLOCK) AS target
--set the SOURCE table one row
USING (VALUES (@applicationId, @societeId, @suppression))
    AS source (applicationId, societeId, suppression)
    --here goes the ON join condition
    ON target.applicationId = source.applicationId and target.societeId = source.societeId
WHEN MATCHED THEN
    UPDATE
    --place your list of SET here
    SET target.suppression = source.suppression
WHEN NOT MATCHED THEN
    --insert a new line with the SOURCE table one row
    INSERT (applicationId, societeId, suppression)
    VALUES (source.applicationId, source.societeId, source.suppression);
GO

Substitua os nomes de tabela e campo pelo que você precisar. Tome cuidado com a condição ON . Em seguida, defina o valor (e o tipo) apropriado para as variáveis ​​na linha DECLARE.

Felicidades.

Denver
fonte
7

Você pode usar MERGEDeclaração, esta declaração é usada para inserir dados, se não existir, ou atualizar, se existir.

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
Daniel Acosta
fonte
@RamenChef Eu não entendo. Onde estão as cláusulas WHEN MATCHED?
likejudo 30/03
@likejudo Eu não escrevi isso; Eu apenas revi. Pergunte ao usuário que escreveu a postagem.
RamenChef 31/03
5

Se você seguir a rota UPDATE se não houver linhas atualizadas e depois INSERIR, considere fazer o INSERT primeiro para evitar uma condição de corrida (supondo que não haja DELETE)

INSERT INTO MyTable (Key, FieldA)
   SELECT @Key, @FieldA
   WHERE NOT EXISTS
   (
       SELECT *
       FROM  MyTable
       WHERE Key = @Key
   )
IF @@ROWCOUNT = 0
BEGIN
   UPDATE MyTable
   SET FieldA=@FieldA
   WHERE Key=@Key
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END

Além de evitar uma condição de corrida, se na maioria dos casos o registro já existir, isso fará com que o INSERT falhe, desperdiçando a CPU.

O uso do MERGE provavelmente é preferível para o SQL2008 em diante.

Kristen
fonte
Ideia interessante, mas sintaxe incorreta. O SELECT precisa de um FROM <table_source> e um TOP 1 (a menos que o table_source escolhido tenha apenas 1 linha).
Jk7 12/04
Obrigado. Eu mudei para NÃO EXISTE. Não só irá sempre ser uma linha correspondente devido ao teste de "chave", como por O / P (apesar de que pode precisar de ser uma chave multi-parte :))
Kristen
4

Isso depende do padrão de uso. É preciso olhar para o panorama geral do uso sem se perder nos detalhes. Por exemplo, se o padrão de uso for 99% de atualizações após a criação do registro, o 'UPSERT' é a melhor solução.

Após a primeira inserção (hit), serão todas as atualizações de instrução únicas, sem ifs ou buts. A condição 'where' na inserção é necessária, caso contrário, ela inserirá duplicatas e você não deseja lidar com o bloqueio.

UPDATE <tableName> SET <field>=@field WHERE key=@key;

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (field)
   SELECT @field
   WHERE NOT EXISTS (select * from tableName where key = @key);
END
Saleh Najar
fonte
2

O MS SQL Server 2008 apresenta a instrução MERGE, que acredito ser parte do padrão SQL: 2003. Como muitos demonstraram, não é grande coisa lidar com casos de uma linha, mas ao lidar com grandes conjuntos de dados, é necessário um cursor, com todos os problemas de desempenho que surgem. A declaração MERGE será uma adição muito bem-vinda ao lidar com grandes conjuntos de dados.

bjorsig
fonte
1
Eu nunca precisei usar um cursor para fazer isso com grandes conjuntos de dados. Você só precisa de uma atualização que atualize os registros correspondentes e uma inserção com uma seleção em vez de uma cláusula de valores que se junta à tabela.
HLGEM 14/04/2009
1

Antes que todo mundo pule para o HOLDLOCK-s por medo desses usuários nefastos executando seus sprocs diretamente :-), deixe-me salientar que você deve garantir a exclusividade dos novos PK-s por design (chaves de identidade, geradores de sequência no Oracle, índices exclusivos para IDs externos, consultas cobertas por índices). Esse é o alfa e o ômega do problema. Se você não tiver isso, nenhum HOLDLOCK-s do universo salvará você e, se você tiver, não precisará de nada além de UPDLOCK na primeira seleção (ou para usar a atualização primeiro).

Os Sprocs normalmente são executados sob condições muito controladas e com a suposição de um chamador confiável (camada intermediária). Isso significa que, se um padrão simples de upsert (atualização + inserção ou mesclagem) vir uma PK duplicada, isso significa um bug no design da camada intermediária ou da tabela e é bom que o SQL grite uma falha nesse caso e rejeite o registro. Colocar um HOLDLOCK nesse caso equivale a comer exceções e receber dados potencialmente defeituosos, além de reduzir seu desempenho.

Dito isto, usar MERGE ou UPDATE e INSERT é mais fácil no servidor e menos propenso a erros, pois você não precisa se lembrar de adicionar (UPDLOCK) à primeira seleção. Além disso, se você estiver inserindo / atualizando em pequenos lotes, precisará conhecer seus dados para decidir se uma transação é apropriada ou não. É apenas uma coleção de registros não relacionados, e as transações adicionais "envolventes" serão prejudiciais.

ZXX
fonte
1
Se você apenas fizer uma atualização e depois inserir sem nenhum bloqueio ou isolamento elevado, dois usuários poderão tentar transmitir os mesmos dados (eu não consideraria um bug na camada intermediária se dois usuários tentassem enviar as mesmas informações exatamente em ao mesmo tempo - depende muito do contexto, não é?). Os dois entram na atualização, que retorna 0 linhas para os dois, depois tentam inserir. Um vence, o outro recebe uma exceção. Isso é o que as pessoas geralmente estão tentando evitar.
Aaron Bertrand
1

As condições da corrida realmente importam se você tentar primeiro uma atualização seguida por uma inserção? Digamos que você tenha dois threads que desejam definir um valor para a chave da chave :

Tópico 1: valor = 1
Tópico 2: valor = 2

Exemplo de cenário de condição de corrida

  1. chave não está definida
  2. O segmento 1 falha com a atualização
  3. O segmento 2 falha com a atualização
  4. Exatamente uma das linhas 1 ou 2 é bem-sucedida com a inserção. Por exemplo, fio 1
  5. O outro encadeamento falha com a inserção (com chave duplicada de erro) - encadeamento 2.

    • Resultado: o "primeiro" dos dois passos a serem inseridos decide o valor.
    • Resultado desejado: O último dos 2 threads a gravar dados (atualizar ou inserir) deve decidir o valor

Mas; em um ambiente multithread, o agendador do SO decide a ordem de execução do encadeamento - no cenário acima, onde temos essa condição de corrida, foi o SO que decidiu a sequência de execução. Ou seja: É errado dizer que "thread 1" ou "thread 2" foi "primeiro" do ponto de vista do sistema.

Quando o tempo de execução é tão próximo para o segmento 1 e o segmento 2, o resultado da condição de corrida não importa. O único requisito deve ser que um dos encadeamentos defina o valor resultante.

Para a implementação: Se a atualização seguida pela inserção resultar no erro "chave duplicada", isso deve ser tratado como êxito.

Além disso, é claro que nunca se deve assumir que o valor no banco de dados é o mesmo que você escreveu por último.

runec
fonte
1

No SQL Server 2008, você pode usar a instrução MERGE

Bart
fonte
11
isso é um comentário na ausência de qualquer código de exemplo real, isso é como muitos outros comentários no site.
swasheck
Muito velho, mas um exemplo seria bom.
Matt McCabe
0

Eu tentei a solução abaixo e funciona para mim, quando ocorre solicitação simultânea de instrução de inserção.

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert table (key, ...)
   values (@key, ...)
end
commit tran
Dev
fonte
0

Você pode usar esta consulta. Trabalhe em todas as edições do SQL Server. É simples e claro. Mas você precisa usar 2 consultas. Você pode usar se não puder usar MERGE

    BEGIN TRAN

    UPDATE table
    SET Id = @ID, Description = @Description
    WHERE Id = @Id

    INSERT INTO table(Id, Description)
    SELECT @Id, @Description
    WHERE NOT EXISTS (SELECT NULL FROM table WHERE Id = @Id)

    COMMIT TRAN

NOTA: Por favor, explique as respostas negativas

Victor Sanchez
fonte
Eu estou supondo falta de bloqueio?
Zeek2 20/05/19
Sem falta de bloqueio ... Eu uso "TRAN". As transações padrão do servidor sql têm bloqueio.
Victor Sanchez
-2

Se você usa o ADO.NET, o DataAdapter lida com isso.

Se você quiser lidar com isso sozinho, é assim:

Verifique se há uma restrição de chave primária na sua coluna de chave.

Então você:

  1. Faça a atualização
  2. Se a atualização falhar porque já existe um registro com a chave, faça a inserção. Se a atualização não falhar, você terminou.

Você também pode fazer o contrário, ou seja, faça a inserção primeiro e faça a atualização se a inserção falhar. Normalmente, a primeira maneira é melhor, porque as atualizações são feitas com mais frequência do que as inserções.

nruessmann
fonte
... e fazer a inserção primeiro (sabendo que às vezes falhará) é caro para o SQL Server. sqlperformance.com/2012/08/t-sql-queries/error-handling
Aaron Bertrand
-3

Fazer um if existe ... else ... envolve fazer no mínimo duas solicitações (uma para verificar, uma para executar). A abordagem a seguir requer apenas uma onde o registro existe, duas se uma inserção for necessária:

DECLARE @RowExists bit
SET @RowExists = 0
UPDATE MyTable SET DataField1 = 'xxx', @RowExists = 1 WHERE Key = 123
IF @RowExists = 0
  INSERT INTO MyTable (Key, DataField1) VALUES (123, 'xxx')
Luke Bennett
fonte
-3

Eu costumo fazer o que vários dos outros pôsteres disseram com relação a verificar se ele existe primeiro e depois fazer o que for o caminho correto. Uma coisa que você deve se lembrar ao fazer isso é que o plano de execução armazenado em cache pelo sql pode não ser o ideal para um caminho ou outro. Acredito que a melhor maneira de fazer isso é chamar dois procedimentos armazenados diferentes.

FirstSP:
Se existir
   Chamar SecondSP (UpdateProc)
Outro
   Ligar para ThirdSP (InsertProc)

Agora, não sigo meu próprio conselho com muita frequência, então tome-o com um grão de sal.

Micky McQuade
fonte
Isso pode ter sido relevante nas versões antigas do SQL Server, mas as versões modernas têm compilação no nível de instrução. Forks etc. não são um problema, e usando procedimentos separados para estas coisas não resolve nenhum dos problemas inerentes a fazer a escolha entre uma atualização e uma inserção de qualquer maneira ...
Aaron Bertrand
-10

Faça uma seleção, se você obtiver um resultado, atualize-o; caso contrário, crie-o.

Clint Ecker
fonte
3
São duas chamadas para o banco de dados.
22620 Chris Cudmore
3
Não vejo problema com isso.
Clint Ecker
10
O problema é duas chamadas para o banco de dados; você acaba dobrando o número de viagens de ida e volta para o banco de dados. Se o aplicativo atingir o banco de dados com muitas inserções / atualizações, isso prejudicará o desempenho. UPSERT é uma estratégia melhor.
Kev
5
também cria uma condição de corrida, não?
Niico 2/17