Вопрос по sql, upsert, database, sql-server, insert – Решения для вставки или обновления на SQL Server

524

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

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

По существу:

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

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

Для тех, кто впервые сталкивается с этим вопросом - пожалуйста, обязательно прочитайте все ответы и их комментарии. Возраст иногда может привести к вводящей в заблуждение информации ... Aaron Bertrand

Ваш Ответ

21   ответ
150

Сделать UPSERT:

UPDATE MyTable SET [email protected] WHERE [email protected]

IF @@ROWCOUNT = 0
   INSERT INTO MyTable (FieldA) VALUES (@FieldA)

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

Даже если у вас есть первичный ключ, который представляет собой автоинкремент, вас будут интересовать любые уникальные ограничения, которые могут быть на столе.
база данных должна заботиться о первичных ключевых проблемах. Вы говорите, что если обновление завершится неудачно, а другой процесс первым получит вставку, ваша вставка завершится неудачно. В этом случае у вас все равно есть состояние гонки. Блокировка не изменит тот факт, что постусловие будет состоять в том, что один из процессов, который пытается написать, получит значение.
@Triynko, я думаю, что @Sam Saffron имел в виду, что если два + потока чередуются в правильной последовательности, то SQL Server будетthrow ошибка, указывающая на нарушение первичного ключаwould have произошло. Заключение в сериализуемую транзакцию является правильным способом предотвращения ошибок в вышеприведенном наборе операторов.
Нарушения первичного ключа не должны возникать, если вы применяете надлежащие ограничения уникального индекса. Весь смысл ограничения состоит в том, чтобы предотвратить повторяющиеся строки в каждом случае. Неважно, сколько потоков пытаются вставить, база данных будет сериализована по мере необходимости, чтобы усилить ограничение ... и, если это не так, то механизм бесполезен. Конечно, включение этого в сериализованную транзакцию сделало бы это более правильным и менее восприимчивым к взаимоблокировкам или неудачным вставкам.
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

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

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

1

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

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

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

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

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

@ 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);
@ user960567 К сожалению, я не всегда получаю уведомления о комментариях в режиме реального времени.
Как насчет вставки / обновления из таблицы тем, которые вставляют / обновляют много записей?
приятно ответил через 2 с лишним года :)
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;
Есть и другие вещи, о которых нужно беспокоиться с MERGE:mssqltips.com/sqlservertip/3074/…
4

Если вы выполняете команду 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 и выше.

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

Вы можете использовать этот запрос. Работа во всех выпусках 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

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

3

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

это комментарий. в отсутствие какого-либо примера кода это так же, как и многие другие комментарии на сайте.
Очень старый, но пример был бы хорош.
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
48
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

Edit:

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

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

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

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

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

Затем вы:

  1. Do the update
  2. If the update fails because a record with the key already exists, do the insert. If the update does not fail, you are finished.

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

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

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

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
2

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

Мне никогда не приходилось использовать курсор, чтобы делать это с большими наборами данных. Вам просто нужно обновление, которое обновляет совпадающие записи, и вставка с предложением select вместо значения values, которое оставляет присоединения к таблице.
-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')
363

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

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

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

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

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

Решения, предоставленные @Beau Crawford & amp; @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
Конечно, но если мы собираемся начать говорить о стабильности приложений, есть еще много других вещей, о которых стоит подумать.
Вопрос задан для наиболее эффективного решения, а не самого безопасного. Хотя транзакция повышает безопасность процесса, она также добавляет накладные расходы.
@CashCow, последние победы, это то, что INSERT или UPDATE должны делать: первый вставляет, второй обновляет запись. Добавление блокировки позволяет сделать это за очень короткий промежуток времени, предотвращая ошибку.
@aku по любой причине, по которой вы использовали табличные подсказки («с (xxxx)») вместо «УСТАНОВИТЬ УРОВЕНЬ ИЗОЛЯЦИИ СДЕЛКИ СДЕЛКИ»
Оба эти метода все еще могут потерпеть неудачу. Если два параллельных потока делают одно и то же в одной и той же строке, первый будет успешен, но вторая вставка завершится неудачно из-за нарушения первичного ключа. Транзакция не гарантирует, что вставка будет успешной, даже если обновление не удалось, поскольку запись существовала. Чтобы гарантировать успешное выполнение любого количества параллельных транзакций, вы ДОЛЖНЫ использовать блокировку.
-6

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

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

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

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

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

  1. key is not defined
  2. Thread 1 fails with update
  3. Thread 2 fails with update
  4. Exactly one of thread 1 or thread 2 succeeds with insert. E.g. thread 1
  5. The other thread fails with insert (with error duplicate key) - thread 2.

    • Result: The "first" of the two treads to insert, decides value.
    • Wanted result: The last of the 2 threads to write data (update or insert) should decide value

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

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

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

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

36

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

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

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

FirstSP:
If Exists
   Call SecondSP (UpdateProc)
Else
   Call ThirdSP (InsertProc)

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

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

Это зависит от модели использования. Нужно смотреть на общую картину использования, не теряясь в деталях. Например, если шаблон использования обновлений составляет 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

Похожие вопросы