Решения для вставки или обновления на SQL Server
предположим, что структура таблицы MyTable(KEY, datafield1, datafield2...)
.
часто я хочу либо обновить существующую запись или вставить новую запись, если она не существует.
по сути:
IF (key exists)
run update command
ELSE
run insert command
каков наилучший способ написать это?
21 ответов
Не забывайте о сделках. Производительность хорошая, но простая (если существует.. подход очень опасен.
Когда несколько потоков попытаются выполнить 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
посмотреть мои подробный ответ на очень похожий предыдущий вопрос
@Бо Кроуфорда является хорошим способом в SQL 2005 и ниже, хотя если вы предоставляете rep, он должен перейти к первый парень, чтобы так оно. Единственная проблема заключается в том, что для вставки еще две операции ввода-вывода.
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, ... )
теперь это действительно только одна операция ввода-вывода, но ужасный код : - (
сделайте UPSERT:
UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key IF @@ROWCOUNT = 0 INSERT INTO MyTable (FieldA) VALUES (@FieldA)
многие люди будут предлагать вам использовать 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 перехватывать исключения, которые вы могли бы предотвратить в первую очередь, намного дороже, за исключением редких случаев, когда почти каждая вставка терпит неудачу. Я доказываю это здесь:
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)
Edit:
увы, даже в ущерб себе, я должен признать, что решения, которые делают это без выбора, кажутся лучше, поскольку они выполняют задачу с одним шагом меньше.
Если вы хотите одновременно вставить более одной записи, вы можете использовать слияние операторов ANSI SQL:2003 DML.
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.
хотя довольно поздно комментировать это, я хочу добавить более полный пример с помощью MERGE.
такие инструкции Insert+Update обычно называются инструкциями Upsert и могут быть реализованы с помощью MERGE в SQL Server.
здесь приведен очень хороший пример: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
выше описаны сценарии блокировки и параллелизма.
I будет цитировать то же самое для справки:
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;
/*
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 состояние. Затем задайте соответствующее значение (и тип) для переменных в строке объявления.
Ура.
можно использовать MERGE
оператор, этот оператор используется для вставки данных, если не существует или обновления, если существует.
MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
если обновление if-no-rows-updated затем вставить маршрут, подумайте о том, чтобы сначала вставить, чтобы предотвратить условие гонки (при условии отсутствия промежуточного удаления)
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
помимо избежания условия гонки, если в большинстве случаев запись уже будет существовать, это приведет к сбою вставки, тратя CPU.
использование слияния, вероятно, предпочтительнее для SQL2008 и далее.
Это зависит от модели использования. Нужно смотреть на большую картину использования, не теряясь в деталях. Например, если шаблон использования 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
MS SQL Server 2008 представляет инструкцию MERGE, которая, как я считаю, является частью стандарта SQL:2003. Как многие показали, не так уж важно обрабатывать случаи одной строки, но при работе с большими наборами данных нужен Курсор со всеми возникающими проблемами производительности. Оператор MERGE будет приветствоваться при работе с большими наборами данных.
прежде чем все прыгают в HOLDLOCK - S из страха от этих nafarious пользователей, запускающих ваши sprocs напрямую : -) позвольте мне указать, что вы должны гарантировать уникальность новых PK-s по дизайну (ключи идентификации, генераторы последовательности в Oracle, уникальные индексы для внешних ID-s, запросы, покрытые индексами). Это альфа и омега проблемы. Если у вас нет этого, никакие замки Вселенной не спасут вас, и если у вас есть это, вам не нужно ничего за пределами UPDLOCK при первом выборе (или использовать обновление первым).
Sprocs обычно работают в очень контролируемых условиях и с предположением доверенного вызывающего абонента (средний уровень). Это означает, что если простой шаблон upsert (update+insert или merge) когда-либо видит дубликат PK, это означает ошибку в вашем дизайне среднего уровня или таблицы, и хорошо, что SQL будет кричать об ошибке в таком случае и отклонять запись. Размещение HOLDLOCK в этом случае равно едят исключения и принимая в потенциально неисправных данных, кроме того уменьшение вашего perf.
сказав это, использование слияния или обновления, а затем вставка проще на вашем сервере и менее подвержены ошибкам, так как вам не нужно помнить, чтобы добавить (UPDLOCK) для первого выбора. Кроме того, если вы делаете вставки/обновления небольшими партиями, вам нужно знать свои данные, чтобы решить, подходит ли транзакция или нет. Это просто коллекция несвязанных записей, тогда дополнительная" обволакивающая " транзакция будет пагубной.
действительно ли условия гонки имеют значение, если вы сначала попробуете обновление с последующей вставкой? Допустим, у вас есть два потока, которые хотят установить значение для key ключ:
поток 1: значение = 1
Поток 2: значение = 2
пример сценария состояния гонки
- ключ не определен
- поток 1 завершается с обновлением
- поток 2 не работает с обновлением
- ровно-нить 1 или резьба 2 успешно вставить. Е. Г. нить 1
-
другой поток терпит неудачу с вставкой (с ключом дубликата ошибки) - поток 2.
- результат: "первый" из двух протекторов для вставки решает значение.
- желаемый результат: последний из 2 потоков для записи данных (обновление или вставка) должен решить значение
But; в многопоточной среде планировщик ОС принимает решение о порядке выполнения потока - в приведенном выше сценарии, где у нас есть это условие гонки, именно ОС приняла решение о последовательности выполнения. Ie: неправильно говорить, что "поток 1" или "поток 2" был "первым" с точки зрения системы.
когда время выполнения настолько близко для потока 1 и потока 2, результат условия гонки не имеет значения. Единственное требование должно заключаться в том, что один из потоков должен определить результирующее значение.
для реализации: если обновление с последующей вставкой приводит к ошибке "дубликат ключа", это следует рассматривать как успех.
кроме того, никогда не следует предполагать, что значение в базе данных совпадает со значением, которое вы написали последним.
Я пробовал ниже решения, и он работает для меня, когда происходит параллельный запрос инструкции 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
вы можете использовать этот запрос. Работа во всех выпусках 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
Примечание: пожалуйста, объясните отрицательные ответы
Если вы используете ADO.NET, DataAdapter обрабатывает это.
Если вы хотите справиться с этим самостоятельно, вот как:
убедитесь, что в столбце ключа есть ограничение первичного ключа.
затем:
- сделать обновление
- если обновление не удается, потому что запись с таким ключом уже существует, делать вставку. Если обновление не завершится неудачно, вы закончите.
вы также можете сделать это наоборот, то есть сделать сначала вставьте и выполните обновление, если вставка не удалась. Обычно первый способ лучше, потому что обновления выполняются чаще, чем вставки.
выполнение if существует ... еще... включает в себя выполнение минимум двух запросов (один для проверки, один для принятия мер). Следующий подход требует только одного, где запись существует, если требуется вставить:
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')
Я обычно делаю то, что сказали несколько других плакатов в отношении проверки его существования, а затем делаю правильный путь. При этом следует помнить, что план выполнения, кэшируемый sql, может быть неоптимальным для того или иного пути. Я считаю, что лучший способ сделать это-вызвать две разные хранимые процедуры.
FirstSP: If Exists Call SecondSP (UpdateProc) Else Call ThirdSP (InsertProc)
теперь я не очень часто следую собственным советам, поэтому примите их с солью.