Рішення для ВСТАВЛЕННЯ АБО ОНОВЛЕННЯ на SQL Server


598

Припустимо структуру таблиці MyTable(KEY, datafield1, datafield2...).

Часто я хочу або оновити існуючий запис, або вставити новий запис, якщо його не існує.

По суті:

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

Який найкращий спосіб написати це?



27
Для всіх, хто стикається з цим питанням вперше - будь ласка, прочитайте всі відповіді та їх коментарі. Вік іноді може призвести до оманливої ​​інформації ...
Аарон Бертран

1
Подумайте про використання оператора
Тарзан

Відповіді:


370

не забувайте про транзакції. Продуктивність хороша, але простий (ЯКЩО Є Є ..) підхід дуже небезпечний.
Коли кілька потоків спробують виконати Insert-or-update, ви можете легко отримати порушення первинного ключа.

Рішення, надані @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

Питання задається найбільш ефективним рішенням, а не найбезпечнішим. Хоча транзакція додає безпеку процесу, вона також додає накладні витрати.
Люк Беннетт

31
Обидва ці способи все ще можуть зазнати невдачі. Якщо дві паралельні нитки роблять те саме в одному рядку, перший буде успішним, але другий вставкою не вдасться через порушення первинного ключа. Угода не гарантує, що вставка буде успішною, навіть якщо оновлення не вдалося, оскільки запис існував. Щоб гарантувати, що будь-яка кількість одночасних транзакцій вдасться, ОБОВ'ЯЗКОВО використовувати замок.
Жан Вінсент

7
@aku з будь-якої причини ви використовували підказки таблиці ("with (xxxx)") на відміну від "SET SET TRANSACTION ISOLATION LEVEL SERIALIZABLE" безпосередньо перед початком TRAN?
EBarr

4
@CashCow, остання перемога, це те, що потрібно робити INSERT або UPDATE: перший вставляє, другий оновлює запис. Додавання блокування дозволить це зробити за дуже короткий проміжок часу, запобігаючи помилці.
Жан Вінсент

1
Я завжди вважав, що підказки щодо блокування є поганими, і ми повинні дозволити двигуну Microsoft Internal диктувати замки. Це очевидний виняток із правила?

381

Дивіться мою детальну відповідь на дуже схоже попереднє запитання

@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
@Ian Boyd - так, це синтаксис стандарту SQL: 2003, а не те, upsertщо майже всі інші постачальники БД вирішили замість цього підтримати. upsertСинтаксис набагато краще спосіб зробити це, так що принаймні MS повинні були підтримувати його - це не так, як це тільки нестандартне ключове слово в T-SQL
Кіт

1
будь-який коментар до натяку на блокування в інших відповідях? (дізнаємось незабаром, але якщо це рекомендований спосіб, рекомендую додати його у відповідь)
eglasius

25
Дивіться тут weblogs.sqlteam.com/dang/archive/2009/01/31/…, щоб отримати відповіді про те, як запобігти гоночним умовам, викликаючи помилки, які можуть виникати навіть при використанні MERGEсинтаксису.
Seph

5
@Seph, це справжній сюрприз - дещо невдача Microsoft там: -Я здогадуюся, що означає, що вам потрібні HOLDLOCKоперації з об'єднання у ситуаціях з високою сумісністю .
Кіт

11
Цю відповідь справді потрібно оновити, щоб врахувати коментар Seph про те, що вона не є безпечною для потоків без HOLDLOCK. Згідно з пов’язаним повідомленням, MERGE неявно знімає блокування оновлення, але випускає його перед тим, як вставити рядки, що може спричинити стан гонки та порушення первинного ключа при вставці. Використовуючи HOLDLOCK, фіксатори зберігаються до моменту вставки.
Трайнко

169

Зробіть UPSERT:

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

ЯКЩО @ 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 одночасно більше, ніж один запис, ви можете використовувати оператор DML ANSI SQL: 2003 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 .


1
В Oracle, видаючи заяву MERGE, я думаю, що блокує таблицю. Чи відбувається те ж саме в SQL * Server?
Майк МакАллістер

13
MERGE сприйнятливий до умов перегонів (див. Weblogs.sqlteam.com/dang/archive/2009/01/31/… ), якщо ви не змусите його мати сертифікатні замки. Крім того, погляньте на ефективність MERGE в SQL Profiler ... Я вважаю, що це типово повільніше і генерує більше читання, ніж альтернативні рішення.
EBarr

@EBarr - Дякую за посилання на блокування. Я оновив свою відповідь, щоб включити підказку щодо блокування.
Ерік Вайнау


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
Є інші речі, про які варто турбуватися з MERGE: mssqltips.com/sqlservertip/3074/…
Аарон Бертран

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

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

Ура.


7

Ви можете використовувати MERGEОператор. Цей вислів використовується для вставки даних, якщо таких немає, або оновлення, якщо вони є.

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

@RamenChef я не розумію. Де розміщені пункти КОЛЕННЯ ПІДГОТОВКИ?
likejudo

@likejudo я цього не писав; Я лише його переглянув. Запитайте користувача, який написав публікацію.
RamenChef

5

Якщо ви рухаєтесь ОНОВЛЕНО, якщо оновлено рядки, якщо не-рядки, то 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 буде дуже вітальним доповненням при роботі з великими наборами даних.


1
Мені ніколи не потрібно було використовувати курсор для цього з великими наборами даних. Вам просто потрібне оновлення, яке оновлює записи, які відповідають, та вставку з виділенням, а не пунктом значень, який залишився приєднаним до таблиці.
HLGEM

1

Перш ніж всі стрибають на HOLDLOCK-і з остраху від цих нахабних користувачів, які запускають ваші відростки безпосередньо :-), дозвольте мені зазначити, що ви повинні гарантувати унікальність нових ПК-програм за допомогою дизайну (ідентифікаційні ключі, генератори послідовностей в Oracle, унікальні індекси для зовнішні ідентифікатори, запити, охоплені індексами). Це альфа та омега випуску. Якщо у вас цього немає, жоден HOLDLOCK-и всесвіту не врятують вас, і якщо у вас це є, то вам не потрібно нічого, крім UPDLOCK при першому виборі (або використовувати оновлення спочатку).

Sprocs зазвичай працюють у дуже контрольованих умовах та з допущенням надійного абонента (середнього рівня). Це означає, що якщо простий шаблон вставки (оновлення + вставлення або злиття) коли-небудь побачить дублікат ПК, що означає помилку у вашому дизайні середнього рівня або таблиці, і це добре, що SQL буде кричати помилку в такому випадку і відхиляти запис. Якщо розмістити HOLDLOCK в цьому випадку, це виняток з їжі та отримання потенційно несправних даних, окрім зменшення кількості парфумів.

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


1
Якщо ви просто зробите оновлення, потім вставте без блокування або підвищеної ізоляції, то два користувачі можуть спробувати передати ті самі дані назад (я б не вважав це помилкою в середньому ярусі, якби два користувачі намагалися подати ту саму інформацію на в той же час - багато що залежить від контексту, чи не так?). Вони обидва вводять оновлення, яке повертає 0 рядків для обох, потім вони обидва намагаються вставити. Один виграє, інший отримує виняток. Саме цього люди намагаються уникати.
Аарон Бертран

1

Чи насправді перегони мають значення, якщо спершу спробувати оновлення, а потім вставку? Скажімо, у вас є два потоки, які хочуть встановити значення для ключового ключа :

Нитка 1: значення = 1
Нитка 2: значення = 2

Приклад сценарію умови гонки

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

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

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

Коли час виконання настільки близький для нитки 1 та нитки 2, результат умови перегонів не має значення. Єдина вимога повинна полягати в тому, що одна з ниток повинна визначати отримане значення.

Для реалізації: Якщо оновлення з подальшим введенням призводить до помилки "дублікат ключа", це слід трактувати як успіх.

Крім того, ніколи не слід вважати, що значення в базі даних є таким самим, як значення, яке ви написали останнім часом.


1

У SQL Server 2008 ви можете використовувати оператор MERGE


11
це коментар. за відсутності реального прикладу коду, це так само, як і багато інших коментарів на сайті.
swasheck

Дуже старий, але приклад був би приємний.
Метт МакКейб

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

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. Якщо оновлення не вдається, оскільки запис із ключем вже існує, виконайте вставку. Якщо оновлення не відбувається, ви закінчите.

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


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

-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:
Якщо існує
   Виклик SecondSP (UpdateProc)
Інше
   Виклик ThirdSP (InsertProc)

Зараз я не дуже часто дотримуюся власної поради, тому приймайте її із зерном солі.


Це, можливо, було актуально в стародавніх версіях SQL Server, але сучасні версії мають компіляцію на рівні заяв. Forks і т.д. не є проблемою, і використання окремих процедур для цих речей не вирішує жодної з питань, властивих зробити вибір між оновленням та вставкою все одно ...
Аарон Бертран

-10

Зробіть вибір, якщо отримаєте результат, оновіть його, якщо ні, створіть його.


3
Це два дзвінки до бази даних.
Кріс Кадмор

3
Я не бачу проблем з цим.
Клінт Екер

10
Це проблема в двох дзвінках до БД, ви закінчуєте подвоєння кількості зворотних переходів до БД. Якщо програма потрапить на db з великою кількістю вставок / оновлень, це зашкодить продуктивності. UPSERT - це краща стратегія.
Кев

5
це також створює перегони умови ні?
niico
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.