it-roy-ru.com

Решения для INSERT OR ОБНОВЛЕНИЕ на SQL Server

Предположим, что структура таблицы MyTable(KEY, datafield1, datafield2...).

Часто я хочу либо обновить существующую запись, либо вставить новую запись, если она не существует.

По существу:

IF (key exists)
  run update command
ELSE
  run insert command

Какой лучший способ написать это?

540
Chris Cudmore

не забывай о сделках. Производительность хорошая, но простой (ЕСЛИ СУЩЕСТВУЕТ) подход очень опасен.
Когда несколько потоков попытаются выполнить вставку или обновление, вы можете легко получить нарушение первичного ключа.

Решения, предоставленные @Beau Crawford & @Esteban, показывают общую идею, но подвержены ошибкам.

Чтобы избежать взаимоблокировок и нарушений PK, вы можете использовать что-то вроде этого:

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert into table (key, ...)
   values (@key, ...)
end
commit tran

или же

begin tran
   update table with (serializable) set ...
   where key = @key

   if @@rowcount = 0
   begin
      insert into table (key, ...) values (@key,..)
   end
commit tran
341
aku

Смотрите мой подробный ответ на очень похожий предыдущий вопрос

@ Beau Crawford's - это хороший способ в SQL 2005 и ниже, хотя, если вы предоставляете rep, он должен перейти к первый парень, который SO _ это , Единственная проблема заключается в том, что для вставок это все еще две операции IO.

MS Sql2008 представляет merge из стандарта SQL: 2003:

merge tablename with(HOLDLOCK) as target
using (values ('new value', 'different value'))
    as source (field1, field2)
    on target.idfield = 7
when matched then
    update
    set field1 = source.field1,
        field2 = source.field2,
        ...
when not matched then
    insert ( idfield, field1, field2, ... )
    values ( 7,  source.field1, source.field2, ... )

Теперь это действительно всего одна операция IO, но ужасный код :-(

368
Keith

Сделать UPSERT:

 ОБНОВЛЕНИЕ MyTable SET FieldA = @ FieldA WHERE Key = @ Key 
 
 IF @@ ROWCOUNT = 0 
 ВСТАВИТЬ В MyTable (FieldA) ЗНАЧЕНИЯ (@FieldA) 

http://en.wikipedia.org/wiki/Upsert

156
Beau Crawford

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

http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/

Даже с этим "более простым" синтаксисом, я все же предпочитаю такой подход (для краткости обработка ошибок опущена):

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END
COMMIT TRANSACTION;

Многие люди предложат этот способ:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
  INSERT ...
END
COMMIT TRANSACTION;

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

Другие предложат этот способ:

BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

Однако это проблематично, если ни по какой другой причине, кроме как позволить SQL Server перехватывать исключения, которые вы могли бы предотвратить в первую очередь, намного дороже, за исключением редкого сценария, когда происходит почти каждая вставка. Я докажу здесь столько же:

81
Aaron Bertrand
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

Edit:

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

51
Esteban Araya

Если вы хотите использовать UPSERT для более чем одной записи одновременно, вы можете использовать оператор ANSI SQL: 2003 DML MERGE.

MERGE INTO table_name WITH (HOLDLOCK) USING table_name ON (condition)
WHEN MATCHED THEN UPDATE SET column1 = value1 [, column2 = value2 ...]
WHEN NOT MATCHED THEN INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...])

Проверьте имитация оператора MERGE в SQL Server 2005 .

36
Eric Weilnau

Хотя уже довольно поздно комментировать это, я хочу добавить более полный пример, используя MERGE.

Такие операторы Insert + Update обычно называются операторами "Upsert" и могут быть реализованы с использованием MERGE в SQL Server.

Очень хороший пример приводится здесь: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

Выше также объясняются сценарии блокировки и параллелизма.

Я буду цитировать то же самое для справки:

ALTER PROCEDURE dbo.Merge_Foo2
      @ID int
AS

SET NOCOUNT, XACT_ABORT ON;

MERGE dbo.Foo2 WITH (HOLDLOCK) AS f
USING (SELECT @ID AS ID) AS new_foo
      ON f.ID = new_foo.ID
WHEN MATCHED THEN
    UPDATE
            SET f.UpdateSpid = @@SPID,
            UpdateTime = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT
      (
            ID,
            InsertSpid,
            InsertTime
      )
    VALUES
      (
            new_foo.ID,
            @@SPID,
            SYSDATETIME()
      );

RETURN @@ERROR;
10
user243131
/*
CREATE TABLE ApplicationsDesSocietes (
   id                   INT IDENTITY(0,1)    NOT NULL,
   applicationId        INT                  NOT NULL,
   societeId            INT                  NOT NULL,
   suppression          BIT                  NULL,
   CONSTRAINT PK_APPLICATIONSDESSOCIETES PRIMARY KEY (id)
)
GO
--*/

DECLARE @applicationId INT = 81, @societeId INT = 43, @suppression BIT = 0

MERGE dbo.ApplicationsDesSocietes WITH (HOLDLOCK) AS target
--set the SOURCE table one row
USING (VALUES (@applicationId, @societeId, @suppression))
    AS source (applicationId, societeId, suppression)
    --here goes the ON join condition
    ON target.applicationId = source.applicationId and target.societeId = source.societeId
WHEN MATCHED THEN
    UPDATE
    --place your list of SET here
    SET target.suppression = source.suppression
WHEN NOT MATCHED THEN
    --insert a new line with the SOURCE table one row
    INSERT (applicationId, societeId, suppression)
    VALUES (source.applicationId, source.societeId, source.suppression);
GO

Замените имена таблиц и полей на все, что вам нужно. Позаботьтесь о условии используя ON. Затем установите соответствующее значение (и тип) для переменных в строке DECLARE.

Приветствия.

8
Denver

Вы можете использовать оператор MERGE. Этот оператор используется для вставки данных, если они не существуют, или обновления, если они существуют.

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
7
Daniel Acosta

Если вы выполняете команду UPDATE if-no-row-updated, а затем INSERT, попробуйте сначала выполнить INSERT, чтобы предотвратить состояние гонки (при условии отсутствия промежуточного удаления).

INSERT INTO MyTable (Key, FieldA)
   SELECT @Key, @FieldA
   WHERE NOT EXISTS
   (
       SELECT *
       FROM  MyTable
       WHERE Key = @Key
   )
IF @@ROWCOUNT = 0
BEGIN
   UPDATE MyTable
   SET [email protected]
   WHERE [email protected]
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END

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

Использование MERGE, вероятно, предпочтительнее для SQL2008 и выше.

4
Kristen

Это зависит от модели использования. Нужно смотреть на общую картину использования, не теряясь в деталях. Например, если шаблон использования обновлений составляет 99% после создания записи, то наилучшим решением будет "UPSERT".

После первой вставки (попадания) это будут все обновления одного оператора, без ifs или buts. Условие "где" на вставке необходимо, иначе оно вставит дубликаты, и вы не хотите иметь дело с блокировкой.

UPDATE <tableName> SET <field>[email protected] WHERE [email protected];

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (field)
   SELECT @field
   WHERE NOT EXISTS (select * from tableName where key = @key);
END
3
Saleh Najar

В SQL Server 2008 вы можете использовать инструкцию MERGE

3
Bart

В MS SQL Server 2008 представлен оператор MERGE, который, как я считаю, является частью стандарта SQL: 2003. Как показали многие, не так уж сложно обрабатывать случаи с одной строкой, но при работе с большими наборами данных нужен курсор со всеми возникающими проблемами производительности. Заявление MERGE будет очень полезным дополнением при работе с большими наборами данных.

2
bjorsig

Прежде чем все перейдут на HOLDLOCK-ы из-за страха от этих злобных пользователей, запускающих ваши sprocs напрямую :-), позвольте мне отметить, что вы должны гарантировать уникальность новых PK-ов по дизайну (идентификационные ключи, последовательность генераторы в Oracle, уникальные индексы для внешних идентификаторов, запросы, покрытые индексами). Это альфа и омега вопроса. Если у вас этого нет, то никакие HOLDLOCK-ы вселенной не спасут вас, и если у вас это есть, то вам не нужно ничего, кроме UPDLOCK, при первом выборе (или сначала использовать обновление).

Sprocs обычно работают в очень контролируемых условиях и в предположении доверенного абонента (средний уровень). Это означает, что если простой паттерн upsert (update + insert или merge) когда-либо увидит дублирующую PK, это означает ошибку в вашем среднем уровне или дизайне таблицы, и хорошо, что SQL в таком случае выкочит ошибку и отклонит запись. Размещение HOLDLOCK в этом случае равнозначно исключениям при приеме пищи и приему потенциально ошибочных данных, помимо снижения производительности.

Сказав, что, используя MERGE или UPDATE, INSERT проще на вашем сервере и менее подвержен ошибкам, так как вам не нужно добавлять (UPDLOCK) для первого выбора. Кроме того, если вы делаете вставки/обновления небольшими партиями, вам нужно знать свои данные, чтобы решить, подходит ли транзакция или нет. Это просто набор несвязанных записей, тогда дополнительная "обволакивающая" транзакция будет пагубной.

1
ZXX

Действительно ли условия гонки имеют значение, если вы сначала попробуете обновление, а затем вставку? Допустим, у вас есть два потока, которые хотят установить значение для ключа ключ:

Поток 1: значение = 1
Тема 2: значение = 2

Пример сценария состояния гонки

  1. ключ не определен
  2. Тема 1 завершается с обновлением
  3. Поток 2 завершается с обновлением
  4. Точно один из потока 1 или потока 2 успешно вставляется. Например. нить 1
  5. Другой поток завершается с ошибкой вставки (с ошибкой дубликата ключа) - поток 2.

    • Результат: "Первый" из двух протекторов, который нужно вставить, определяет значение.
    • Требуемый результат: последний из 2 потоков для записи данных (обновление или вставка) должен определить значение

Но; в многопоточной среде планировщик ОС определяет порядок выполнения потока - в приведенном выше сценарии, где мы имеем это условие гонки, именно ОС определила последовательность выполнения. Т.е. неправильно говорить, что "поток 1" или "поток 2" был "первым" с точки зрения системы.

Когда время выполнения так близко для потока 1 и потока 2, результат условия гонки не имеет значения. Единственное требование должно состоять в том, чтобы один из потоков определял результирующее значение.

Для реализации: Если обновление, сопровождаемое вставкой, приводит к ошибке "дубликат ключа", это следует рассматривать как успех.

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

1
runec

Я пробовал приведенное ниже решение, и оно работает для меня, когда происходит параллельный запрос на оператор вставки.

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert table (key, ...)
   values (@key, ...)
end
commit tran
0
Dev