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


104

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

update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

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

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

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

Ответы:


61

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

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

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

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

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


1
Думаю, он ответил по крайней мере на один вопрос. И я не добавил код, потому что код в вопросе уже казался мне подходящим. Хотя я бы поместил это в транзакцию, я не учел уровень изоляции при обновлении. Спасибо, что указали на это в своем ответе!
binOr

54

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

Чтобы быстро ответить, попробуйте следующий шаблон. Он будет нормально работать с 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

1
В своем сообщении в блоге вы завершаете использование подсказки WITH (updlock, serializable) в проверке существования. Однако при чтении MSDN указывается: «UPDLOCK - указывает, что блокировки обновления должны быть приняты и удерживаться до завершения транзакции». Означает ли это, что сериализуемая подсказка излишняя, так как блокировка обновления все равно будет удерживаться до конца транзакции, или я что-то неправильно понял?
Dan Def

10

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

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

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

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



6

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

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
COMMIT TRANSACTION Upsert

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


@Munish Goyal Потому что в базе данных несколько команд и процедур выполняются параллельно. Затем другой поток может вставить строку сразу после выполнения обновления и до выполнения вставки.
Tomas Tintera 03

5

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

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

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


3

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


2

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

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, и, если возможно, исправляем их внутри.


1

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.