假设表结构为MyTable(KEY, datafield1, datafield2...)
。
通常我想更新现有记录,或者如果它不存在则插入一条新记录。
本质上:
IF (key exists)
run update command
ELSE
run insert command
写这个的最佳执行方式是什么?
假设表结构为MyTable(KEY, datafield1, datafield2...)
。
通常我想更新现有记录,或者如果它不存在则插入一条新记录。
本质上:
IF (key exists)
run update command
ELSE
run insert command
写这个的最佳执行方式是什么?
不要忘记交易。性能很好,但简单的(如果存在..)方法非常危险。
当多个线程将尝试执行插入或更新时,您很容易得到主键违规。
@Beau Crawford 和 @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
请参阅我对之前非常相似的问题的详细回答
@Beau Crawford's在 SQL 2005 及更低版本中是一个好方法,但如果你授予代表它应该交给第一个人 SO it。唯一的问题是对于插入它仍然是两个 IO 操作。
MS Sql2008merge
从 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, ... )
现在它实际上只是一个 IO 操作,但是代码很糟糕:-(
做一个UPSERT:
UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key 如果@@ROWCOUNT = 0 插入 MyTable (FieldA) 值 (@FieldA)
很多人会建议你使用MERGE
,但我警告你不要这样做。默认情况下,它不会保护您免受并发和竞争条件的影响,就像多个语句一样,它还引入了其他危险:
即使有这种“更简单”的语法可用,我仍然更喜欢这种方法(为简洁起见,省略了错误处理):
BEGIN TRANSACTION;
UPDATE dbo.table WITH (UPDLOCK, SERIALIZABLE)
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
BEGIN
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 . . .)
编辑:
唉,即使对我自己不利,我也必须承认,在没有选择的情况下执行此操作的解决方案似乎更好,因为它们以更少的步骤完成任务。
如果您想一次 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 添加一个更完整的示例。
此类 Insert+Update 语句通常称为“Upsert”语句,可以在 SQL Server 中使用 MERGE 来实现。
这里给出了一个很好的例子: 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;
/*
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 行上的变量设置适当的值(和类型)。
干杯。
这取决于使用模式。人们必须查看使用大图,而不会迷失在细节中。例如,如果使用模式是在创建记录后 99% 更新,那么“UPSERT”是最佳解决方案。
在第一次插入(命中)之后,将是所有单语句更新,没有 if 或 buts。插入的“where”条件是必要的,否则它将插入重复项,并且您不想处理锁定。
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
您可以使用MERGE
语句,该语句用于如果不存在则插入数据或如果存在则更新。
MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
如果执行 UPDATE if-no-rows-updated 然后 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 失败,从而浪费 CPU。
从 SQL2008 开始,使用 MERGE 可能更可取。
MS SQL Server 2008 引入了 MERGE 语句,我相信它是 SQL:2003 标准的一部分。正如许多人所表明的那样,处理单行情况并不是什么大问题,但是在处理大型数据集时,需要一个游标,随之而来的所有性能问题。在处理大型数据集时,MERGE 语句将非常受欢迎。
如果您首先尝试更新然后插入,那么竞争条件真的很重要吗?假设您有两个线程想要为键key设置值:
线程 1:值 = 1
线程 2:值 = 2
示例竞争条件场景
另一个线程因插入而失败(带有错误重复键) - 线程 2。
但; 在多线程环境中,操作系统调度程序决定线程执行的顺序——在上面的场景中,我们有这种竞争条件,是操作系统决定了执行的顺序。即:从系统的角度说“线程 1”或“线程 2”是“第一”是错误的。
当线程 1 和线程 2 的执行时间如此接近时,竞争条件的结果就无关紧要了。唯一的要求应该是其中一个线程应该定义结果值。
对于实现:如果更新后插入导致错误“重复键”,则应视为成功。
此外,当然永远不要假设数据库中的值与您上次写入的值相同。
在每个人因为害怕这些直接运行您的存储过程的邪恶用户而跳到 HOLDLOCK-s 之前 :-) 让我指出您必须通过设计保证新 PK-s 的唯一性(身份密钥、Oracle 中的序列生成器、用于外部 ID-s,索引覆盖的查询)。这就是问题的阿尔法和欧米茄。如果你没有这个,宇宙中的任何 HOLDLOCK 都不会拯救你,如果你有,那么你在第一次选择时不需要任何东西(或首先使用更新)。
Sprocs 通常在非常受控的条件下运行,并假设一个受信任的调用者(中间层)。这意味着如果一个简单的 upsert 模式(更新+插入或合并)曾经看到重复的 PK,这意味着您的中间层或表设计中存在错误,并且在这种情况下 SQL 会大喊错误并拒绝记录是很好的。在这种情况下放置 HOLDLOCK 等于吃异常和接收潜在的错误数据,除了降低你的性能。
话虽如此,使用 MERGE 或 UPDATE 然后 INSERT 在您的服务器上更容易并且更不容易出错,因为您不必记住添加 (UPDLOCK) 到第一次选择。此外,如果您正在小批量进行插入/更新,则需要了解您的数据才能确定事务是否合适。它只是一组不相关的记录,那么额外的“封装”交易将是有害的。
当并发请求插入语句时,我尝试了以下解决方案,它对我有用。
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
注意:请解释答案否定
假设您要插入/更新单行,最优化的方法是使用 SQL Server 的REPEATABLE READ
事务隔离级别:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION
IF (EXISTS (SELECT * FROM myTable WHERE key=@key)
UPDATE myTable SET ...
WHERE key=@key
ELSE
INSERT INTO myTable (key, ...)
VALUES (@key, ...)
COMMIT TRANSACTION
此隔离级别将防止/阻止后续可重复读取事务WHERE key=@key
在当前运行的事务打开时访问同一行 ( )。另一方面,另一行的操作不会被阻塞(WHERE key=@key2
)。
MySQL(以及随后的 SQLite)也支持 REPLACE INTO 语法:
REPLACE INTO MyTable (KEY, datafield1, datafield2) VALUES (5, '123', 'overwrite');
这会自动识别主键并找到要更新的匹配行,如果没有找到则插入新行。
在 SQL Server 2008 中,您可以使用 MERGE 语句
执行 if exists ... 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')
我通常会按照其他几位发帖人所说的先检查它是否存在,然后再做正确的路径。这样做时您应该记住的一件事是,由 sql 缓存的执行计划对于一个路径或另一个路径可能不是最优的。我相信最好的方法是调用两个不同的存储过程。
第一SP: 如果存在 调用 SecondSP (UpdateProc) 别的 调用 ThirdSP (InsertProc)
现在,我不经常听从自己的建议,所以要持保留态度。
如果您使用 ADO.NET,则 DataAdapter 会处理此问题。
如果你想自己处理,这是这样的:
确保您的键列上有一个主键约束。
那么你:
你也可以反过来做,即先插入,如果插入失败则进行更新。通常第一种方法更好,因为更新比插入更频繁。
做一个选择,如果你得到结果,更新它,如果没有,创建它。