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>
- в първия INSERT има условие за състезание. Ключът може да не съществува по време на вътрешната заявка SELECT, но съществува по време на INSERT, което води до нарушение на ключа.
- има условие за състезание между INSERT и UPDATE. Ключът може да съществува, когато е отметнат във вътрешната заявка на INSERT, но е изчезнал до момента, в който UPDATE се изпълнява.
За условието за второ състезание може да се твърди, че ключът така или иначе щеше да бъде изтрит от едновременната нишка, така че всъщност не е загубена актуализация.
Оптималното решение обикновено е да опитате най-вероятния случай и да се справите с грешката, ако тя не успее (в рамките на транзакция, разбира се):
- ако ключът вероятно липсва, винаги поставяйте първи. Обработете нарушението на уникалното ограничение, резервно за актуализиране.
- ако ключът вероятно е налице, винаги първо актуализирайте. Вмъкнете, ако не е намерен ред. Обработете възможно нарушение на уникалното ограничение, резервно за актуализиране.
Освен коректност, този модел е оптимален и за скорост:по-ефективно е да се опитвате да вмъкнете и обработвате изключението, отколкото да правите фалшиви блокирания. Заключването означава четене на логически страници (което може да означава четене на физическа страница), а IO (дори логическо) е по-скъпо от SEH.
Актуализиране @Питър
Защо едно изявление не е „атомно“? Да кажем, че имаме тривиална таблица:
create table Test (id int primary key);
Сега, ако изпълня това единично изявление от две нишки, в цикъл, то би било „атомно“, както казвате, може да съществува условие без състезание:
insert into Test (id)
select top (1) id
from Numbers n
where not exists (select id from Test where id = n.id);
И все пак само за няколко секунди възниква нарушение на първичния ключ:
Съобщение 2627, ниво 14, състояние 1, ред 4
Нарушение на ограничението ПЪРВЕН КЛЮЧ „PK__Test__24927208“. Не може да се вмъкне дублиран ключ в обект 'dbo.Test'.
Защо така? Прав сте, че планът за SQL заявка ще направи "правилното" при DELETE ... FROM ... JOIN
, на WITH cte AS (SELECT...FROM ) DELETE FROM cte
и в много други случаи. Но има съществена разлика в тези случаи:„подзаявката“ се отнася до целта наактуализация или изтриване операция. За такива случаи планът на заявката наистина ще използва подходящо заключване, всъщност аз това поведение е от решаващо значение в определени случаи, като например при внедряване на опашки с използване на таблици като опашки.
Но в оригиналния въпрос, както и в моя пример, подзаявката се вижда от оптимизатора на заявки просто като подзаявка в заявка, а не като някаква специална заявка от типа „сканиране за актуализация“, която се нуждае от специална защита за заключване. Резултатът е, че изпълнението на търсенето на подзаявка може да се наблюдава като отделна операция от едновременен наблюдател , като по този начин се нарушава „атомното“ поведение на изявлението. Освен ако не се вземат специални предпазни мерки, множество нишки могат да се опитат да вмъкнат една и съща стойност, и двете са убедени, че са проверили, и стойността вече не съществува. Само единият може да успее, другият ще удари нарушението на PK. QED.