Атомный UPSERT в SQL Server 2005 - PullRequest
48 голосов
/ 26 марта 2010

Каков правильный шаблон для выполнения атомарного «UPSERT» (ОБНОВЛЕНИЕ, где существует, ВСТАВИТЬ в противном случае) в 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 ...

где <условие> будет оценкой натуральных ключей. Ни один из вышеперечисленных подходов, похоже, не справляется с параллелизмом. Если у меня не может быть двух строк с одним и тем же естественным ключом, кажется, что все вышеперечисленное рискует вставить строки с одинаковыми естественными ключами в сценарии состояния гонки.

Я использовал следующий подход, но я удивлен, что нигде не вижу его в ответах людей, поэтому мне интересно, что с ним не так:

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>

Обратите внимание, что упомянутое здесь состояние гонки отличается от условий в предыдущем коде. В предыдущем коде проблема заключалась в фантомном чтении (строки, вставленные между UPDATE / IF или между SELECT / INSERT другим сеансом). В приведенном выше коде условие гонки связано с DELETE. Возможно ли удаление соответствующей строки другим сеансом ПОСЛЕ выполнения (ГДЕ НЕ СУЩЕСТВУЕТ), но до выполнения INSERT? Непонятно, где WHERE NOT EXISTS блокирует что-либо вместе с UPDATE.

Это атомно? Я не могу найти, где это будет описано в документации по SQL Server.

РЕДАКТИРОВАТЬ: Я понимаю, что это может быть сделано с транзакциями, но я думаю, что мне нужно было бы установить уровень транзакции SERIALIZABLE, чтобы избежать проблемы фантомного чтения? Конечно, это излишне для такой распространенной проблемы?

Ответы [ 5 ]

29 голосов
/ 26 марта 2010
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.

6 голосов
/ 15 апреля 2010

Пропускать подсказки при обновлении, блокировке строки и блокировке при проверке наличия строки. 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 голосов
/ 27 марта 2010

РЕДАКТИРОВАТЬ : Remus является правильным, условная вставка с условием вставки / где не гарантирует согласованного состояния между коррелированным подзапросом и вставкой таблицы.

Возможно, правильные табличные подсказки могут привести к согласованному состоянию. INSERT <table> WITH (TABLOCKX, HOLDLOCK), кажется, работает, но я понятия не имею, является ли это оптимальным уровнем блокировки для условной вставки.

В тривиальном тесте, подобном описанному Ремусом, TABLOCKX, HOLDLOCK показал ~ 5x объем вставки без табличных подсказок и без ошибок PK или курса.

ОРИГИНАЛЬНЫЙ ОТВЕТ, НЕПРАВИЛЬНЫЙ:

Это атом?

Да, условная вставка с условным обозначением атома, и ваша форма 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

Отдельные операторы всегда выполняются внутри транзакции, либо их собственные ( автокоммит и неявные ), либо вместе с другими операторами ( явные ).

2 голосов
/ 26 марта 2010

Вы можете использовать блокировки приложений: (sp_getapplock) http://msdn.microsoft.com/en-us/library/ms189823.aspx

2 голосов
/ 26 марта 2010

Одна хитрость, которую я видел, - это попробовать ВСТАВКУ и, если она не удалась, выполнить ОБНОВЛЕНИЕ.

...