2008-09-20 9 views
471

Предположим, что структура таблицы составляет MyTable(KEY, datafield1, datafield2...).Решения для INSERT или UPDATE на SQL Server

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

По существу:

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

Какой самый лучший способ выполнения, чтобы написать это?

+20

Для всех, кто впервые встречает этот вопрос, обязательно прочитайте все ответы и их комментарии. Возраст может иногда приводить к вводящей в заблуждение информации ... – 2014-01-18 20:42:07

+0

* Похожие вопросы: * * [Вставить обновление хранимой процедуры на SQL Server] (http://stackoverflow.com/questions/13540/insert-update-stored-proc-on-sql -сервер) * [SQL Server 2005 реализация MySQL REPLACE INTO?] (http://stackoverflow.com/questions/234/sql-server-2005-implementation-of-mysql-replace-into) – 2008-09-20 15:15:49

ответ

295

не забывайте о сделках. Производительность хорошая, но простой (IF EXISTS ..) подход очень опасен.
Когда несколько потоков попытаются выполнить Вставку или обновление, вы можете легко получить нарушение первичного ключа.

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

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

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 
+0

Вопрос задан для наиболее эффективного решения, а не для самого безопасного. Хотя транзакция добавляет безопасность процессу, она также добавляет дополнительные накладные расходы. – 2008-09-20 15:17:34

+0

Несомненно, но если мы начнем говорить о стабильности приложений, есть и другие вещи, о которых можно подумать. – 2008-09-20 15:26:44

+23

Оба эти метода все еще могут потерпеть неудачу. Если два параллельных потока выполняются одинаково в одной строке, первая из них будет успешной, но вторая вставка не будет выполнена из-за нарушения первичного ключа. Сделка не гарантирует, что вставка будет успешной, даже если обновление завершилось неудачно, поскольку запись существовала. Чтобы гарантировать, что любое количество одновременных транзакций будет успешным, вы ДОЛЖНЫ использовать блокировку. – 2010-07-28 09:17:37

-5

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

+3

Это два вызова базы данных. – 2008-09-20 15:04:23

+2

Я не вижу проблемы с этим. – 2008-09-20 15:14:43

+6

Это два вызова в БД, это проблема, вы заканчиваете удвоение числа обращений к БД. Если приложение попадает в db с большим количеством вставок/обновлений, это повредит производительности. UPSERT - лучшая стратегия. – Kev 2008-09-20 19:05:22

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

Edit:

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

+2

Мне все еще нравится это лучше. Воспроизведение кажется скорее похожей на программирование побочным эффектом, и я * никогда * не видел, как малочисленный кластеризованный индекс запрашивает этот первоначальный выбор, чтобы вызвать проблемы с производительностью в реальной базе данных. – 2008-09-21 01:04:57

135

Делают UPSERT:

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

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

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

+6

Нарушения первичного ключа не должны возникать, если у вас есть соответствующие уникальные ограничения индекса. Вся суть ограничения заключается в том, чтобы предотвратить дублирование строк из всех происходящих событий. Неважно, сколько потоков пытается вставить, база данных будет сериализована по мере необходимости для принудительного ограничения ограничений ... и если это не так, то механизм бесполезен. Конечно, обертывание этого в сериализованной транзакции сделает это более правильным и менее восприимчивым к взаимоблокировкам или неудачным вставкам. – Triynko 2010-05-12 06:12:55

+16

@Triynko, я думаю, что @Sam Saffron означало, что если две + нитки чередуются в правильной последовательности, то sql-сервер будет * бросать * ошибку, указывающую на нарушение первичного ключа *. Обертка его в сериализуемой транзакции - это правильный способ предотвратить ошибки в приведенном выше наборе операторов. – EBarr 2010-12-01 23:12:06

+1

Даже если у вас есть первичный ключ, который является автоматическим приращением, ваша забота будет представлять собой любые уникальные ограничения, которые могут быть на столе. – Seph 2012-01-19 13:38:24

-3

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

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: 
If Exists 
    Call SecondSP (UpdateProc) 
Else 
    Call ThirdSP (InsertProc) 

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

-2

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

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

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

Тогда вы:

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

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

4

В SQL Server 2008 вы можете использовать 'Слить Заявление о

34

Если вы хотите 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 in SQL Server 2005.

2

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

345

Смотрите мой detailed answer to a very similar previous question

@Beau Crawford's является хорошим способом в SQL 2005 и ниже, хотя, если вы предоставления респ он должен идти к first guy to SO it. Единственная проблема заключается в том, что для вставок все еще две операции ввода-вывода.

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

Хотя его довольно поздно комментировать это я хочу для добавления более полного примера с использованием 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; 
1

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

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

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

1

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

Тема 1: значение = 1
темы 2: значение = 2

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

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

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

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

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

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

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

4

Если происходишь обновление, если-нет-строки обновляемых затем ВСТАВИТЬ маршрут, рассмотреть возможность сделать Вкладыш первым, чтобы предотвратить состояние гонки (при условии не вмешиваясь 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 [email protected] 
    WHERE [email protected] 
    IF @@ROWCOUNT = 0 
    ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ... 
END 

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

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

-2

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

Примечание: Пожалуйста, объясните ответ негативов

69

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

6
/* 
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.

Cheers.

0

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

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 
1

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

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

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