Atomic UPSERT в SQL Server 2005
Каков правильный шаблон для атомного "UPSERT" (UPDATE, где существует, INSERT в противном случае) в SQL Server 2005?
Я вижу много кода на SO (например, см. Проверьте, существует ли строка, иначе вставьте) со следующим двухчастным шаблоном:
UPDATE ...
FROM ...
WHERE <condition>
-- race condition risk here
IF @@ROWCOUNT = 0
INSERT ...
или
IF (SELECT COUNT(*) FROM ... WHERE <condition>) = 0
-- race condition risk here
INSERT ...
ELSE
UPDATE ...
где < условие > будет оценкой естественных ключей. Ни один из вышеперечисленных подходов, похоже, не справляется с concurrency. Если у меня не может быть двух строк с одним и тем же естественным ключом, кажется, что все перечисленные выше риски вставляют строки с одинаковыми естественными ключами в сценариях условий гонки.
Я использую следующий подход, но я удивлен, что не вижу его в ответах людей, поэтому мне интересно, что с ним не так:
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>
Обратите внимание, что упомянутое здесь условие гонки отличается от условий предыдущего кода. В более раннем коде проблема заключалась в phantom чтениях (строки вставляются между UPDATE/IF или между SELECT/INSERT другим сеансом). В приведенном выше коде условие гонки связано с DELETE. Возможно ли, чтобы соответствующая строка была удалена другим сеансом ПОСЛЕ того, как выполняется (WHERE NOT EXISTS), но до выполнения INSERT? Неясно, где WHERE NOT EXISTS помещает блокировку на что-либо в связи с UPDATE.
Является ли это атомом? Я не могу найти, где это будет документировано в документации SQL Server.
EDIT: Я понимаю, что это можно сделать с транзакциями, но я думаю, что мне нужно установить уровень транзакции SERIALIZABLE, чтобы избежать проблемы с чтением phantom? Неужели это слишком сложно для такой общей проблемы?
Ответы
Ответ 1
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.
Обновить @Peter
Почему не один оператор "атомный"? Скажем, у нас есть тривиальная таблица:
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);
Однако за пару секунд происходит нарушение первичного ключа:
Msg 2627, уровень 14, состояние 1, строка 4
Нарушение ограничения PRIMARY KEY "PK__Test__24927208". Невозможно вставить дубликат ключа в объект "dbo.Test".
Почему? Вы правы в том, что в плане запросов SQL будет "правильная вещь" на DELETE ... FROM ... JOIN
, на WITH cte AS (SELECT...FROM ) DELETE FROM cte
и во многих других случаях. Но в этих случаях есть решающее различие: "подзапрос" относится к цели операции update или удалить. Для таких случаев в плане запроса действительно будет использоваться соответствующая блокировка, на самом деле это поведение имеет решающее значение в некоторых случаях, например, при реализации очередей Использование таблиц как очередей.
Но в исходном вопросе, как и в моем примере, подзапрос рассматривается оптимизатором запросов как подзапрос в запросе, а не как специальный запрос типа "проверка на обновление", для которого требуется специальная защита от блокировки. В результате выполнение подзапроса может наблюдаться как отдельная операция созерцательного наблюдателя, тем самым нарушая "атомное" поведение оператора. Если не будет предпринята специальная предосторожность, несколько потоков могут попытаться вставить одно и то же значение, оба убеждены, что они проверили, а значение еще не существует. Только один может преуспеть, другой попадет в нарушение ПК. Что и требовалось доказать.
Ответ 2
При проверке существования строки передавайте подсказки, блокировки, блокировки, блокировки. Holdlock гарантирует, что все вставки сериализованы; rowlock разрешает одновременное обновление существующих строк.
Обновления могут по-прежнему блокироваться, если ваш PK является bigint, поскольку внутреннее хеширование вырождено для 64-битных значений.
begin tran -- default read committed isolation level is fine
if not exists (select * from <table> with (updlock, rowlock, holdlock) where <PK = ...>
-- insert
else
-- update
commit
Ответ 3
РЕДАКТИРОВАТЬ: Remus правильный, условная вставка w/where предложение не гарантирует согласованного состояния между коррелированным подзапросом и таблицей.
Возможно, правильные подсказки таблицы могут привести к согласованному состоянию. INSERT <table> WITH (TABLOCKX, HOLDLOCK)
, похоже, работает, но я понятия не имею, является ли это оптимальным уровнем блокировки для условной вставки.
В тривиальном тесте, таком как описанный Ремус, TABLOCKX, HOLDLOCK
показал ~ 5x объем вставки без табличных подсказок и без ошибок PK или курса.
ОРИГИНАЛЬНЫЙ ОТВЕТ, НЕПРАВИЛЬНЫЙ:
Является ли это атомом?
Да, условная вставка w/where является атомарной, а ваша форма INSERT ... WHERE NOT EXISTS() ... UPDATE
- это правильный способ выполнить UPSERT.
Я бы добавил IF @@ROWCOUNT = 0
между INSERT и UPDATE:
INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
WHERE NOT EXISTS
-- no race condition here
( SELECT 1 FROM <table> WHERE <natural keys> )
IF @@ROWCOUNT = 0 BEGIN
UPDATE ...
WHERE <natural keys>
END
Одиночные утверждения всегда выполняются внутри транзакции, либо их собственные (autocommit, и неявно) или вместе с другими операторами (явным).
Ответ 4
Один трюк, который я видел, - это попробовать INSERT, и если он не сработает, выполните UPDATE.
Ответ 5
Вы можете использовать блокировки приложений: (sp_getapplock)
http://msdn.microsoft.com/en-us/library/ms189823.aspx