Вставить обновление хранимой процедуры на SQL Server

Я написал хранимую процедуру, которая будет выполнять обновление, если запись существует, иначе она выполнит вставку. Это выглядит примерно так:

update myTable set [email protected], [email protected] where [email protected]
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

Моя логика написания этого способа заключается в том, что обновление будет выполнять неявный выбор с использованием предложения where, и если оно вернет 0, то произойдет вставка.

Альтернативой этому может быть выбор, а затем, в зависимости от количества возвращенных строк, либо обновление, либо вставка. Это я считал неэффективным, потому что если вы собираетесь выполнить обновление, это вызовет 2 выбора (первый явный вызов select и второй неявный в месте обновления). Если бы процесс делал вставку, то разницы в эффективности не было бы.

Здесь моя логика звучит? Это как бы вы могли объединить вставку и обновление в сохраненную процедуру?

Ответов (9)

Решение

Ваше предположение верно, это оптимальный способ сделать это, и он называется upsert / merge .

Важность UPSERT - от sqlservercentral.com :

Для каждого обновления в упомянутом выше случае мы удаляем одно дополнительное чтение из таблицы, если мы используем UPSERT вместо EXISTS. К сожалению для Insert, методы UPSERT и IF EXISTS используют одинаковое количество чтений из таблицы. Следовательно, проверка существования должна выполняться только при наличии очень веской причины для оправдания дополнительных операций ввода-вывода. Оптимизированный способ сделать что-то - убедиться, что у вас есть как можно меньше операций чтения из БД.

Лучшая стратегия - попытаться обновить. Если обновление не коснулось ни одной строки, вставьте. В большинстве случаев строка уже существует, и потребуется только один ввод-вывод.

Изменить : ознакомьтесь с этим ответом и связанным сообщением в блоге, чтобы узнать о проблемах с этим шаблоном и о том, как сделать его безопасным.

Большой поклонник UPSERT, действительно сокращает код для управления. Вот еще один способ, которым я это делаю: один из входных параметров - это ID, если ID равен NULL или 0, вы знаете, что это INSERT, в противном случае это обновление. Предполагается, что приложение знает, есть ли идентификатор, поэтому не будет работать во всех ситуациях, но сократит выполнение вдвое, если вы это сделаете.

Если вы не выполняете слияние в SQL 2008, вы должны изменить его на:

если @@ rowcount = 0 и @@ error = 0

в противном случае, если обновление по какой-либо причине не удается, оно будет пытаться выполнить вставку позже, потому что количество строк в неудачном операторе равно 0

Доработанный пост Димы Маленко:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

BEGIN TRANSACTION UPSERT 

UPDATE MYTABLE 
SET    COL1 = @col1, 
       COL2 = @col2 
WHERE  ID = @ID 

IF @@rowcount = 0 
  BEGIN 
      INSERT INTO MYTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

IF @@Error > 0 
  BEGIN 
      INSERT INTO MYERRORTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

COMMIT TRANSACTION UPSERT 

Вы можете перехватить ошибку и отправить запись в таблицу с ошибкой вставки.
Мне нужно было это сделать, потому что мы берем все данные, отправляемые через WSDL, и, если возможно, исправляем их внутри.

Между прочим, MERGE - одна из новых возможностей SQL Server 2008.

При использовании с SQL Server 2000/2005 исходный код должен быть заключен в транзакцию, чтобы гарантировать, что данные остаются согласованными в параллельном сценарии.

BEGIN TRANSACTION Upsert
update myTable set [email protected], [email protected] where [email protected]
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)
COMMIT TRANSACTION Upsert

Это повлечет за собой дополнительные затраты на производительность, но обеспечит целостность данных.

Добавьте, как уже было предложено, MERGE, если он доступен.

Ваша логика кажется здравой, но вы можете подумать о добавлении кода для предотвращения вставки, если вы передали определенный первичный ключ.

В противном случае, если вы всегда выполняете вставку, если обновление не повлияло на какие-либо записи, что произойдет, если кто-то удалит запись до запуска «UPSERT»? Теперь запись, которую вы пытались обновить, не существует, поэтому вместо нее будет создана запись. Вероятно, это не то поведение, которого вы искали.

Вам не только нужно запускать его в транзакции, он также требует высокого уровня изоляции. На самом деле уровень изоляции по умолчанию - Read Commited, и этот код требует Serializable.

SET transaction isolation level SERIALIZABLE
BEGIN TRANSACTION Upsert
UPDATE myTable set [email protected], [email protected] where [email protected]
if @@rowcount = 0
  begin
    INSERT into myTable (ID, Col1, Col2) values (@ID @col1, @col2)
  end
COMMIT TRANSACTION Upsert

Возможно, добавление проверки ошибок @@ и отката может быть хорошей идеей.

Прочтите сообщение в моем блоге , чтобы узнать о хорошем и безопасном шаблоне, который вы можете использовать. Есть много соображений, и принятый ответ на этот вопрос далеко не безопасен.

Для быстрого ответа попробуйте следующий шаблон. Он будет нормально работать с SQL 2000 и выше. SQL 2005 предоставляет вам обработку ошибок, которая открывает другие параметры, а SQL 2008 дает вам команду MERGE.

begin tran
   update t with (serializable)
   set hitCount = hitCount + 1
   where pk = @id
   if @@rowcount = 0
   begin
      insert t (pk, hitCount)
      values (@id,1)
   end
commit tran