Вставить обновление хранимой процедуры на 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 используют одинаковое количество чтений из таблицы. Следовательно, проверка существования должна выполняться только при наличии очень веской причины для оправдания дополнительных операций ввода-вывода. Оптимизированный способ сделать что-то - убедиться, что у вас есть как можно меньше чтения из БД.

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

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

binOr
источник
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, если он доступен.

Дима Маленко
источник
8

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

Джон Гэллоуэй
источник
и вам обязательно нужно использовать его, а не эту трудную для чтения доморощенную чушь. Хороший пример здесь - mssqltips.com/sqlservertip/1704/…
Рич Брайант
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, и, если возможно, исправляем их внутри.

головорезы78013
источник
1

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

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

Кевин Фэирчайлд
источник