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>
- в первой ВСТАВКЕ есть состояние гонки. Ключ может не существовать во время внутреннего запроса 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
Нарушение ограничения PRIMARY KEY 'PK__Test__24927208'. Невозможно вставить повторяющийся ключ в объект 'dbo.Test'.
Почему это? Вы правы в том, что план SQL-запросов будет делать «правильные вещи» на DELETE ... FROM ... JOIN
, на WITH cte AS (SELECT...FROM ) DELETE FROM cte
и во многих других случаях. Но в этих случаях есть существенное различие: подзапрос относится к target операции update или delete . Для таких случаев в плане запроса действительно будет использоваться соответствующая блокировка, на самом деле, в некоторых случаях это поведение критично, например, при реализации очередей Использование таблиц в качестве очередей .
Но в исходном вопросе, как и в моем примере, подзапрос воспринимается оптимизатором запросов как подзапрос в запросе, а не как какой-то особый запрос типа «сканирование для обновления», который требует специальной защиты блокировки. В результате параллельный наблюдатель может наблюдать за выполнением поиска подзапроса как отдельную операцию, что нарушает «атомарное» поведение оператора. Если не приняты особые меры предосторожности, несколько потоков могут пытаться вставить одно и то же значение, и оба убеждены, что проверили, а значение еще не существует. Только один может добиться успеха, другой ударит по PK. QED.