Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

UPSERT atômico no SQL Server 2005

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
  • há uma condição de corrida no primeiro INSERT. A chave pode não existir durante a consulta interna SELECT, mas existe no momento de INSERT, resultando em violação de chave.
  • há uma condição de corrida entre INSERT e UPDATE. A chave pode existir quando verificada na consulta interna do INSERT, mas desaparece quando UPDATE é executado.

Para a segunda condição de corrida, pode-se argumentar que a chave teria sido excluída de qualquer maneira pelo encadeamento simultâneo, portanto, não é realmente uma atualização perdida.

A solução ideal geralmente é tentar o caso mais provável e lidar com o erro se ele falhar (dentro de uma transação, é claro):
  • se a chave provavelmente estiver faltando, sempre insira primeiro. Lide com a violação de restrição exclusiva, faça fallback para atualizar.
  • se a chave provavelmente estiver presente, sempre atualize primeiro. Insira se nenhuma linha foi encontrada. Lidar com uma possível violação de restrição exclusiva, fallback para atualizar.

Além da correção, esse padrão também é ótimo para velocidade:é mais eficiente tentar inserir e tratar a exceção do que fazer travamentos espúrios. Lockups significam leituras de páginas lógicas (o que pode significar leituras de páginas físicas) e IO (mesmo lógico) é mais caro que SEH.

Atualizar @Peter

Por que uma única declaração não é 'atômica'? Digamos que temos uma tabela trivial:
create table Test (id int primary key);

Agora, se eu executasse essa única instrução de dois threads, em um loop, seria 'atômico', como você diz, uma condição sem corrida pode existir:
  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

No entanto, em apenas alguns segundos, ocorre uma violação de chave primária:

Msg 2627, Level 14, State 1, Line 4
Violação da restrição PRIMARY KEY 'PK__Test__24927208'. Não é possível inserir a chave duplicada no objeto 'dbo.Test'.

Por que é que? Você está correto em que o plano de consulta SQL fará a 'coisa certa' em DELETE ... FROM ... JOIN , em WITH cte AS (SELECT...FROM ) DELETE FROM cte e em muitos outros casos. Mas há uma diferença crucial nesses casos:a 'subconsulta' se refere ao destino de uma atualização ou excluir Operação. Para tais casos, o plano de consulta realmente usará um bloqueio apropriado, na verdade, esse comportamento é crítico em certos casos, como ao implementar filas usando tabelas como filas.

Mas na pergunta original, assim como no meu exemplo, a subconsulta é vista pelo otimizador de consulta apenas como uma subconsulta em uma consulta, não como uma consulta especial do tipo 'scan for update' que precisa de proteção de bloqueio especial. O resultado é que a execução da pesquisa de subconsulta pode ser observada como uma operação distinta por um observador concorrente , quebrando assim o comportamento 'atômico' da declaração. A menos que uma precaução especial seja tomada, vários threads podem tentar inserir o mesmo valor, ambos convencidos de que verificaram e o valor ainda não existe. Apenas um pode ter sucesso, o outro atingirá a violação de PK. QED.