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.