Вопрос по sql, upsert, database, sql-server, insert – Решения для вставки или обновления на SQL Server
Предположим, что структура таблицыMyTable(KEY, datafield1, datafield2...)
.
Часто я хочу либо обновить существующую запись, либо вставить новую запись, если она не существует.
По существу:
IF (key exists)
run update command
ELSE
run insert command
Какой лучший способ написать это?
Сделать UPSERT:
UPDATE MyTable SET [email protected] WHERE [email protected] IF @@ROWCOUNT = 0 INSERT INTO MyTable (FieldA) VALUES (@FieldA)
/*
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.
Приветствия.
Прежде чем все перейдут на 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) для первого выбора. Кроме того, если вы делаете вставки / обновления небольшими партиями, вам нужно знать свои данные, чтобы решить, подходит ли транзакция или нет. Это просто набор несвязанных записей, а затем дополнительный "обволакивающий" сделка будет пагубной.
Многие люди предложат вам использовать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 перехватывать исключения, которые вы могли бы предотвратить в первую очередь, намного дороже, за исключением редкого сценария, когда происходит почти каждая вставка. Я докажу здесь столько же:
Хотя уже довольно поздно комментировать это, я хочу добавить более полный пример, используя 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;
Если вы выполняете команду 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 и выше.
Вы можете использовать этот запрос. Работа во всех выпусках 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
ПРИМЕЧАНИЕ: Пожалуйста, объясните отрицательные ответы
Я пробовал приведенное ниже решение, и оно работает для меня, когда происходит параллельный запрос на оператор вставки.
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
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)
Edit:
Увы, даже к моему собственному ущербу, я должен признать, что решения, которые делают это без выбора, кажутся лучше, так как они выполняют задачу с одним меньшим шагом.
Если вы используете ADO.NET, DataAdapter справится с этим.
Если вы хотите справиться с этим самостоятельно, это способ:
Убедитесь, что в столбце ключей есть ограничение первичного ключа.
Затем вы:
- Do the update
- If the update fails because a record with the key already exists, do the insert. If the update does not fail, you are finished.
Вы также можете сделать это наоборот, т.е. сначала выполнить вставку и выполнить обновление, если вставка не удалась. Обычно первый способ лучше, потому что обновления делаются чаще, чем вставки.
В MS SQL Server 2008 представлен оператор MERGE, который, как я считаю, является частью стандарта SQL: 2003. Как показали многие, не так уж и сложно обрабатывать случаи с одной строкой, но при работе с большими наборами данных нужен курсор со всеми возникающими проблемами производительности. Заявление MERGE будет очень полезным дополнением при работе с большими наборами данных.
Выполнение 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')
См мойподробный ответ на очень похожий предыдущий вопрос
@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, ... )
Теперь это действительно всего лишь одна операция ввода-вывода, но ужасный код :-(
MERGE
синтаксис.
upsert
что почти все другие поставщики БД решили поддержать вместо этого.upsert
синтаксис - гораздо более хороший способ сделать это, поэтому, по крайней мере, MS должен был бы его тоже поддерживать - он не такой, как это единственное нестандартное ключевое слово в T-SQL
не забывайте о транзакциях. Производительность хорошая, но простой (ЕСЛИ СУЩЕСТВУЕТ) подход очень опасен.
Когда несколько потоков будут пытаться выполнить вставку или обновление, вы можете легко
получить нарушение первичного ключа.
Решения, предоставленные @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
Сделайте выбор, если вы получите результат, обновите его, если нет, создайте его.
Действительно ли условия гонки имеют значение, если вы сначала попробуете обновление, а затем вставку? Допустим, у вас есть два потока, которые хотят установить значение для ключаkey:
Поток 1: значение = 1
Поток 2: значение = 2
Пример сценария состояния гонки
- key is not defined
- Thread 1 fails with update
- Thread 2 fails with update
- Exactly one of thread 1 or thread 2 succeeds with insert. E.g. thread 1
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, результат условия гонки не имеет значения. Единственное требование должно состоять в том, чтобы один из потоков определял результирующее значение.
Для реализации: если после обновления с последующей вставкой выдается ошибка «дубликат ключа», это следует рассматривать как успех.
Кроме того, конечно, никогда не следует предполагать, что значение в базе данных совпадает со значением, которое вы написали последним.
Если вы хотите использовать 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.
Я обычно делаю то, что сказали некоторые из других плакатов, в первую очередь проверяя, существует ли он, а затем делаю правильный путь. При этом следует помнить одну вещь: план выполнения, кэшируемый sql, может быть неоптимальным для одного или другого пути. Я считаю, что лучший способ сделать это - вызвать две разные хранимые процедуры.
FirstSP: If Exists Call SecondSP (UpdateProc) Else Call ThirdSP (InsertProc)
Теперь я не очень часто следую своему собственному совету, поэтому примите его с небольшим количеством соли.
Это зависит от модели использования. Нужно смотреть на общую картину использования, не теряясь в деталях. Например, если шаблон использования обновлений составляет 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