Решения для вставки или обновления на SQL Server

599

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

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

По существу:

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

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

Крис Кадмор
источник
28
Для тех, кто впервые сталкивается с этим вопросом - пожалуйста, обязательно прочитайте все ответы и их комментарии. Иногда возраст может привести к вводящей в заблуждение информации ...
Аарон Бертран,
1
Рассмотрите возможность использования оператора EXCEPT, который был представлен в SQL Server 2005.
Tarzan

Ответы:

370

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

Решения, предоставленные @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
Ака
источник
1
Вопрос задан для наиболее эффективного решения, а не самого безопасного. Хотя транзакция повышает безопасность процесса, она также добавляет накладные расходы.
Люк Беннетт
31
Оба эти метода все еще могут потерпеть неудачу. Если два параллельных потока делают одно и то же в одной и той же строке, первый будет успешным, но вторая вставка завершится неудачно из-за нарушения первичного ключа. Транзакция не гарантирует, что вставка будет успешной, даже если обновление не удалось, поскольку запись существовала. Чтобы гарантировать успешное выполнение любого количества параллельных транзакций, вы ДОЛЖНЫ использовать блокировку.
Жан Винсент
7
@aku, по какой причине вы использовали табличные подсказки («with (xxxx)»), а не «УРОВЕНЬ УРОВНЯ ИЗОЛЯЦИИ SET TRANSACTION SERIALIZABLE» непосредственно перед началом BEGIN TRAN?
EBarr
4
@CashCow, последняя победа, это то, что INSERT или UPDATE должны делать: первая вставляет, вторая обновляет запись. Добавление блокировки позволяет сделать это за очень короткий промежуток времени, предотвращая ошибку.
Жан Винсент
1
Я всегда думал, что использование подсказок блокировки - это плохо, и мы должны позволить внутреннему движку Microsoft определять блокировки. Это явное исключение из правила?
382

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

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

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, ... )

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

Кит
источник
10
@ Ян Бойд - да, это синтаксис стандарта SQL: 2003, а не тот, upsertкоторый все другие поставщики БД решили поддержать вместо этого. upsertСинтаксис гораздо лучше способ сделать это, так что по крайней мере MS должны были поддерживать его - это не так, как это только нестандартное ключевое слово в T-SQL
Кит
1
любой комментарий на подсказку блокировки в других ответах? (скоро узнаю, но если это рекомендуемый способ, я рекомендую добавить его в ответ)
eglasius
25
См. Здесь weblogs.sqlteam.com/dang/archive/2009/01/31/… для ответа на вопрос о том, как не допустить возникновения условий гонки из-за ошибок, которые могут возникнуть даже при использовании MERGEсинтаксиса.
Seph
5
@ Да, это настоящий сюрприз - что-то вроде провала со стороны Microsoft: -Я думаю, это означает, что вам нужно HOLDLOCKдля операций слияния в ситуациях с высоким параллелизмом.
Кит
11
Этот ответ действительно нуждается в обновлении, чтобы учесть комментарий Seph о том, что он не является поточно-ориентированным без HOLDLOCK. Согласно сообщению, MERGE неявно снимает блокировку обновления, но снимает ее перед вставкой строк, что может привести к состоянию гонки и нарушениям первичного ключа при вставке. Используя HOLDLOCK, блокировки сохраняются до тех пор, пока не произойдет вставка.
Трийнко
169

Сделать UPSERT:

ОБНОВЛЕНИЕ MyTable SET FieldA = @ FieldA WHERE Key = @ Key

IF @@ ROWCOUNT = 0
   ВСТАВИТЬ В MyTable (FieldA) ЗНАЧЕНИЯ (@FieldA)

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

Бо Кроуфорд
источник
7
Нарушения первичного ключа не должны возникать, если вы применяете надлежащие ограничения уникального индекса. Весь смысл ограничения состоит в том, чтобы предотвратить повторяющиеся строки в каждом случае. Неважно, сколько потоков пытаются вставить, база данных будет сериализована по мере необходимости, чтобы усилить ограничение ... и если это не так, то движок бесполезен. Конечно, включение этого в сериализованную транзакцию сделало бы это более правильным и менее восприимчивым к взаимоблокировкам или неудачным вставкам.
Трийнко
19
@Triynko, я думаю , Saffron @ Сэм имел в виду , что если два + нити чередуют в правильной последовательности , то SQL Server будет бросать ошибки , указывающий на первичный ключ нарушение бы произошло. Заключение в сериализуемую транзакцию является правильным способом предотвращения ошибок в приведенном выше наборе операторов.
EBarr
1
Даже если у вас есть первичный ключ, который представляет собой автоинкремент, вас будут интересовать любые уникальные ограничения, которые могут быть на столе.
Seph
1
база данных должна заботиться о первичных ключевых проблемах. Вы говорите, что если обновление завершится неудачно, а другой процесс первым получит вставку, ваша вставка завершится неудачно. В этом случае у вас все равно есть состояние гонки. Блокировка не изменит того факта, что постусловием будет то, что один из процессов, который пытается написать, получит значение.
CashCow
93

Многие люди предложат вам использовать 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 перехватывать исключения, которые вы могли бы предотвратить в первую очередь, намного дороже, за исключением редкого сценария, когда происходит почти каждая вставка. Я докажу здесь столько же:

Аарон Бертран
источник
3
Как насчет вставки / обновления из таблицы тем, которые вставляют / обновляют много записей?
user960567
@ user960567 Ну,UPDATE target SET col = tmp.col FROM target INNER JOIN #tmp ON <key clause>; INSERT target(...) SELECT ... FROM #tmp AS t WHERE NOT EXISTS (SELECT 1 FROM target WHERE key = t.key);
Аарон Бертран
4
приятно ответил более чем через 2 года :)
user960567
12
@ user960567 К сожалению, я не всегда ловлю уведомления о комментариях в режиме реального времени.
Аарон Бертран,
60
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

Редактировать:

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

Эстебан Арайя
источник
6
Мне все еще нравится этот лучше. Наступление больше похоже на программирование с использованием побочных эффектов, и я никогда не видел, чтобы этот начальный выбор был довольно слабым из-за поиска в кластеризованном индексе, чтобы вызвать проблемы с производительностью в реальной базе данных.
Eric Z Beard
38

Если вы хотите использовать 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 ...])

Ознакомьтесь с Mimicking MERGE Statement в SQL Server 2005 .

Эрик Вейльну
источник
1
В Oracle, выдавая оператор MERGE, я думаю, что блокирует таблицу. То же самое происходит в SQL * Server?
Майк Макаллистер
13
MERGE чувствителен к условиям гонки (см. Weblogs.sqlteam.com/dang/archive/2009/01/31/… ), если только вы не сделаете так, чтобы он удерживал определенные блокировки. Кроме того, взгляните на производительность MERGE в SQL Profiler ... я обнаружил, что он обычно медленнее и генерирует больше операций чтения, чем альтернативные решения.
EBarr
@EBarr - Спасибо за ссылку на замки. Я обновил свой ответ, чтобы включить подсказку блокировки подсказок.
Эрик Вейльну
Также проверьте mssqltips.com/sqlservertip/3074/…
Аарон Бертран
10

Хотя уже довольно поздно комментировать это, я хочу добавить более полный пример, используя 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;
user243131
источник
1
Есть и другие вещи, о которых нужно беспокоиться с MERGE
Аарон Бертран,
8
/*
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.

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

Денвер
источник
7

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

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
Даниэль Акоста
источник
@RamenChef Я не понимаю. Где предложения WHEN MATCHED?
Likejudo
@likejudo Я не писал это; Я только пересмотрел это. Спросите пользователя, который написал пост.
RamenChef
5

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

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

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

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

Кристен
источник
Интересная идея, но неверный синтаксис. Для SELECT требуется FROM <table_source> и TOP 1 (если только выбранный table_source не содержит только 1 строку).
jk7
Спасибо. Я изменил это на НЕ СУЩЕСТВУЕТ. Будет только одна подходящая строка из-за теста на «ключ» согласно O / P (хотя, возможно, это должен быть ключ из нескольких частей :))
Кристен
4

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

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

UPDATE <tableName> SET <field>=@field WHERE key=@key;

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (field)
   SELECT @field
   WHERE NOT EXISTS (select * from tableName where key = @key);
END
Салех Наджар
источник
2

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

bjorsig
источник
1
Мне никогда не приходилось использовать курсор, чтобы делать это с большими наборами данных. Вам просто нужно обновление, которое обновляет совпадающие записи, и вставка с предложением select вместо значения values, которое оставляет присоединения к таблице.
HLGEM
1

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

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

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

ZXX
источник
1
Если вы просто делаете обновление, а затем вставляете без какой-либо блокировки или повышенной изоляции, тогда два пользователя могут попытаться передать одни и те же данные обратно (я бы не посчитал это ошибкой на среднем уровне, если два пользователя пытались представить одинаковую информацию в в то же время - многое зависит от контекста, не так ли?). Они оба входят в обновление, которое возвращает 0 строк для обоих, затем они оба пытаются вставить. Один выигрывает, другой получает исключение. Это то, что люди обычно пытаются избежать.
Аарон Бертран,
1

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

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

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

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

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

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

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

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

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

runec
источник
1

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

Барт
источник
11
это комментарий. в отсутствие какого-либо примера кода это так же, как и многие другие комментарии на сайте.
swasheck
Очень старый, но пример был бы хорош.
Мэтт МакКейб
0

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

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
Dev
источник
0

Вы можете использовать этот запрос. Работа во всех выпусках SQL Server. Это просто и понятно. Но вам нужно использовать 2 запроса. Вы можете использовать, если вы не можете использовать MERGE

    BEGIN TRAN

    UPDATE table
    SET Id = @ID, Description = @Description
    WHERE Id = @Id

    INSERT INTO table(Id, Description)
    SELECT @Id, @Description
    WHERE NOT EXISTS (SELECT NULL FROM table WHERE Id = @Id)

    COMMIT TRAN

ПРИМЕЧАНИЕ: Пожалуйста, объясните отрицательные ответы

Виктор Санчес
источник
Я предполагаю отсутствие блокировки?
Zeek2
Нет недостатка блокировки ... Я использую "TRAN". Транзакции sql-сервера по умолчанию имеют блокировку.
Виктор Санчес
-2

Если вы используете ADO.NET, DataAdapter справится с этим.

Если вы хотите справиться с этим самостоятельно, это способ:

Убедитесь, что в столбце ключей есть ограничение первичного ключа.

Затем вы:

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

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

nruessmann
источник
... и выполнение первой вставки (зная, что иногда это не удастся) дорого обходится SQL Server. sqlperformance.com/2012/08/t-sql-queries/error-handling
Аарон Бертран
-3

Выполнение if существует ... else ... подразумевает выполнение как минимум двух запросов (один для проверки, один для выполнения действия). Следующий подход требует только одного, где запись существует, два, если требуется вставка:

DECLARE @RowExists bit
SET @RowExists = 0
UPDATE MyTable SET DataField1 = 'xxx', @RowExists = 1 WHERE Key = 123
IF @RowExists = 0
  INSERT INTO MyTable (Key, DataField1) VALUES (123, 'xxx')
Люк Беннетт
источник
-3

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

FirstSP:
Если существует
   Call SecondSP (UpdateProc)
еще
   Вызов ThirdSP (InsertProc)

Так вот, я не очень часто следую своему собственному совету, поэтому принимайте его с небольшим количеством соли.

Микки Маккуэйд
источник
Это могло быть актуально в древних версиях SQL Server, но современные версии имеют компиляцию на уровне операторов. Форкс и т. Д. Не являются проблемой, и использование отдельных процедур для этих вещей не решает ни одной из проблем, присущих выбору между обновлением и вставкой в ​​любом случае ...
Аарон Бертран,
-10

Сделайте выбор, если вы получите результат, обновите его, если нет, создайте его.

Клинт Эккер
источник
3
Это два звонка в базу данных.
Крис Кадмор
3
Я не вижу проблемы с этим.
Клинт Экер
10
Проблема в двух вызовах БД: вы заканчиваете тем, что удваиваете количество обращений к БД. Если приложение попадет в базу данных с большим количеством вставок / обновлений, это снизит производительность. UPSERT - лучшая стратегия.
Кев
5
это также создает условия гонки нет?
niico