SQL Server na prática

Upserts concorrentes no SQL Server sem disputa de existência

Proteja inserções ou atualizações com chaves únicas, bloqueios de intervalo, semântica de substituição e repetições que respeitam o resultado do commit.

Um upsert parece simples: atualizar quando a chave de negócio existe e inserir caso contrário. Duas sessões, porém, podem observar simultaneamente a mesma ausência e decidir inserir. Com uma restrição única, uma recebe erro. Sem ela, ambas podem concluir e violar o relacionamento esperado.

A primeira decisão é o significado da atualização. Substituir uma preferência de idioma é diferente de incrementar um saldo ou rejeitar uma edição baseada em dados antigos. Upsert representa uma política de gravação. Defina essa política antes de escolher a estratégia de bloqueio.

Proteger a chave de negócio

A tabela de demonstração permite uma preferência por cliente e nome. Crie-a somente em uma base descartável de prática.

-- Create only in a disposable practice database.
CREATE TABLE dbo.PreferenceDemo (
    CustomerId int NOT NULL,
    PreferenceKey nvarchar(50) NOT NULL,
    PreferenceValue nvarchar(200) NOT NULL,
    CONSTRAINT PK_PreferenceDemo PRIMARY KEY (CustomerId, PreferenceKey)
);

A chave única é a última barreira de integridade, mesmo que todas as aplicações devam seguir o procedimento correto. Inclua todas as dimensões necessárias, como TenantId quando identificadores só são únicos dentro de um cliente. Para chaves textuais, combine normalização e sensibilidade a maiúsculas; a collation influencia a igualdade.

Um IF NOT EXISTS separado do INSERT pode sofrer essa disputa no read committed comum. Colocar ambos em uma transação não protege necessariamente a chave ausente. É preciso proteger o intervalo em que ela seria inserida.

Este lote tenta atualizar primeiro e mantém a proteção da busca até o fim da transação.

DECLARE @CustomerId int = 42;
DECLARE @Key nvarchar(50) = N'language';
DECLARE @Value nvarchar(200) = N'en';
SET XACT_ABORT ON;
IF @@TRANCOUNT <> 0
    THROW 50001, 'This batch owns its transaction.', 1;

BEGIN TRY
    BEGIN TRANSACTION;
    UPDATE dbo.PreferenceDemo WITH (UPDLOCK, HOLDLOCK)
    SET PreferenceValue = @Value
    WHERE CustomerId = @CustomerId AND PreferenceKey = @Key;

    IF @@ROWCOUNT = 0
        INSERT dbo.PreferenceDemo(CustomerId, PreferenceKey, PreferenceValue)
        VALUES (@CustomerId, @Key, @Value);

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;

HOLDLOCK aplica comportamento serializable à referência da tabela. UPDLOCK solicita bloqueios voltados à atualização. O índice único permite proteger a chave ou o intervalo pertinente. Isso não garante bloqueio de apenas uma linha física: o caminho de acesso e o restante do trabalho influenciam a abrangência.

Verifique @@ROWCOUNT imediatamente depois do UPDATE, sem inserir uma instrução de log entre eles. Uma linha encontrada conta mesmo quando já contém o valor fornecido; portanto, o código não tenta criar outra nesse caso.

Explicitar conflitos aceitáveis

Duas solicitações atribuindo idiomas diferentes podem ser executadas em sequência. A última atribuição serializada concluída determina o valor. Isso não garante que vença a última requisição recebida pelo servidor web e não identifica alguém sobrescrevendo uma edição de outro usuário.

Se edições antigas devem ser rejeitadas, inclua uma rowversion esperada no predicado UPDATE e investigue a ausência de correspondência como conflito. rowversion é um token de alteração, não uma data. Criação e substituição condicional podem justificar operações distintas na API.

Repetir uma atribuição do mesmo valor costuma ser idempotente no nível do dado. Repetir "somar dez" não é. Gatilhos, auditoria e mensagens externas também podem produzir efeitos adicionais. Utilize um identificador durável de solicitação quando a operação precisa ser aplicada uma vez por pedido.

MERGE não elimina as questões de unicidade, isolamento e concorrência. Avalie a carga e a versão específicas em vez de presumir que uma única instrução torna segura toda a operação de negócio.

Testar com conexões concorrentes

Execute o lote em duas conexões contra a mesma tabela, incluindo uma chave ainda inexistente. Em um teste controlado, pause temporariamente uma sessão após UPDATE, com a transação aberta, e observe a outra aguardando. Retire a pausa do código da aplicação.

Teste também chaves distintas, repetições, violações de restrição e perda de conexão perto de COMMIT. O cliente pode não saber se houve confirmação. Uma repetição cega pode duplicar efeitos; resolva a dúvida pelo identificador da solicitação ou pelo estado autoritativo.

Bloqueios de intervalo podem aumentar a contenção e não eliminam deadlocks. Acesse múltiplas chaves em ordem consistente e mantenha transações curtas. Repita a transação inteira da vítima com espera limitada, e não apenas o INSERT dentro de uma transação comprometida. O upsert correto preserva a chave única e a política declarada de conflitos.

Referências técnicas: Microsoft Learn: Table hints · Microsoft Learn: Transaction locking guide.

Pergunte sobre este artigo

Tem alguma dúvida sobre este tema?

Conte o que você está avaliando ou onde encontrou dificuldades. Responderemos com uma recomendação prática.

Inquiries are not enabled in this preview.

Fazer uma pergunta sobre este artigo