Solutions for INSERT OR UPDATE on SQL Server
假设表结构为
通常我想更新一个现有记录,或者插入一个不存在的新记录。
基本上:
1 2 3 4 | IF (KEY EXISTS) run UPDATE command ELSE run INSERT command |
写这个最好的方法是什么?
请看我对前面一个非常相似问题的详细回答。
@BeauCrawford在2005及更低版本的SQL中是一个很好的方法,但是如果您要授予rep,它应该是第一个这样做的人。唯一的问题是对于插入,它仍然是两个IO操作。
MS SQL2008从SQL:2003标准引入了
1 2 3 4 5 6 7 8 9 10 11 12 | 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操作,但是糟糕的代码:-(
不要忘记交易。性能很好,但简单(如果存在….)的方法是非常危险的。当多个线程尝试执行插入或更新时,您可以轻松地获取主键冲突。
@beau crawford&;@esteban提供的解决方案显示了一般的想法,但容易出错。
为了避免死锁和pk冲突,可以使用如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 |
或
1 2 3 4 5 6 7 8 9 | BEGIN tran UPDATE TABLE WITH (serializable) SET ... WHERE KEY = @KEY IF @@rowcount = 0 BEGIN INSERT INTO TABLE (KEY, ...) VALUES (@KEY,..) END commit tran |
做一个高潮:
1 2 3 4 | UPDATE MyTable SET FieldA=@FieldA WHERE KEY=@KEY IF @@ROWCOUNT = 0 INSERT INTO MyTable (FieldA) VALUES (@FieldA) |
http://en.wikipedia.org/wiki/upsert网站
很多人会建议你使用
http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/
即使有了这个"简单"的语法,我仍然更喜欢这种方法(为了简洁起见,省略了错误处理):
1 2 3 4 5 6 7 8 | 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; |
很多人会这样建议:
1 2 3 4 5 6 7 8 9 10 | 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; |
但所有这些完成的工作都是确保您可能需要读取表两次,才能找到要更新的行。在第一个示例中,您只需要定位行一次。(在这两种情况下,如果在初始读取中找不到行,则会发生插入。)
其他人会这样建议:
1 2 3 4 5 6 7 | BEGIN TRY INSERT ... END TRY BEGIN CATCH IF ERROR_NUMBER() = 2627 UPDATE ... END CATCH |
但是,如果除了让SQL Server捕获本来可以防止的异常之外,其他任何原因都不会导致这种情况的发生,那么这就有问题了,除非在几乎每个插入都失败的罕见情况下。我在这里也证明了:
- http://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/
- http://www.sqlpperformance.com/2012/08/t-sql-queries/error-handling
1 2 3 4 | IF EXISTS (SELECT * FROM [TABLE] WHERE ID = rowID) UPDATE [TABLE] SET propertyOne = propOne, property2 . . . ELSE INSERT INTO [TABLE] (propOne, propTwo . . .) |
编辑:
唉,即使对我自己不利,我也必须承认,没有选择就这样做的解决方案似乎更好,因为它们用更少的步骤完成了任务。
如果一次要升迁多个记录,可以使用ansisql:2003 dml语句merge。
1 2 3 | 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 ...]) |
签出SQL Server 2005中的模拟合并语句。
虽然现在评论这个还为时已晚,但我想用merge添加一个更完整的示例。
这种insert+update语句通常称为"upsert"语句,可以使用SQL Server中的merge实现。
这里给出了一个很好的例子:http://weblogs.sqlteam.com/dang/archive/2009/01/31/upsert-race-condition-with-merge.aspx
上面也解释了锁定和并发场景。
我将引用同样的话作为参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /* 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行上的变量设置适当的值(和类型)。
干杯。
您可以使用
1 2 3 | MERGE INTO Employee AS e USING EmployeeUpdate AS eu ON e.EmployeeID = eu.EmployeeID` |
如果进行更新,如果没有更新的行,则插入路由,请考虑先执行插入以防止竞争条件(假设没有中间删除)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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以后的版本,使用合并可能更可取。
在SQL Server 2008中,可以使用MERGE语句
这取决于使用模式。我们必须了解使用情况的大局,而不要迷失在细节中。例如,如果在创建记录后使用模式为99%更新,则"upsert"是最佳解决方案。
在第一次插入(hit)之后,它将是所有单个语句更新,没有ifs或but。插入的"Where"条件是必需的,否则它将插入重复项,并且您不想处理锁定。
1 2 3 4 5 6 7 8 | 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标准的一部分。正如许多人所指出的,处理一行的情况并不是什么大问题,但是在处理大型数据集时,需要一个带有所有性能问题的光标。在处理大型数据集时,合并语句将非常受欢迎。
如果先尝试更新,然后插入,竞争条件真的很重要吗?假设您有两个线程想要为密钥设置值:
螺纹1:值=1螺纹2:值=2
竞赛条件场景示例
另一个线程因insert(错误为duplicate key)失败-线程2。
- 结果:要插入的两个踏板中的"第一个"决定了值。
- 期望结果:写入数据(更新或插入)的两个线程中的最后一个应决定值
但是,在多线程环境中,OS调度程序决定线程执行的顺序——在上面的场景中,我们有这个争用条件,是OS决定了执行的顺序。IE:从系统的角度来说,"线程1"或"线程2"是"第一"是错误的。
当线程1和线程2的执行时间如此接近时,争用条件的结果并不重要。唯一的要求应该是其中一个线程应该定义结果值。
对于实现:如果更新后再插入导致错误"duplicate key",则应将其视为成功。
当然,也不应该假定数据库中的值与上次写入的值相同。
在所有人都跳到holdlock-s之前,出于对这些直接运行存储过程的不熟悉的用户的恐惧,让我指出您必须通过设计(标识键、Oracle中的序列生成器、外部ID-S的唯一索引、索引覆盖的查询)来确保新的PK-S的唯一性。这就是问题的重点。如果你没有,宇宙中没有holdlock-s可以拯救你,如果你有,那么你在第一次选择时不需要updlock以外的任何东西(或者先使用update)。
存储过程通常在非常受控的条件下运行,并假定受信任的调用方(中间层)。这意味着,如果一个简单的upsert模式(update+insert或merge)曾经看到重复的pk,这意味着中间层或表设计中有一个bug,那么SQL在这种情况下会大喊一个错误并拒绝该记录,这很好。在这种情况下,放置一个holdlock,除了降低性能外,还等于接受异常和潜在的错误数据。
尽管如此,在您的服务器上使用合并或更新然后插入更容易,并且不太容易出错,因为您不必记住在第一次选择时添加(updlock)。此外,如果您正在小批量进行插入/更新,则需要知道您的数据,以便决定事务是否合适。它只是一个不相关记录的集合,那么额外的"封装"交易将是有害的。
我尝试过下面的解决方案,当并发的insert语句请求发生时,它对我有效。
1 2 3 4 5 6 7 8 9 10 11 12 | 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个查询。如果不能使用合并,则可以使用
1 2 3 4 5 6 7 8 9 10 11 | 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将处理此问题。
如果你想自己处理,可以这样做:
确保您的键列上有主键约束。
然后你:
你也可以反过来做,也就是说,先插入,如果插入失败就更新。通常第一种方法更好,因为更新比插入更频繁。
我通常按照其他几张海报上所说的,先检查它是否存在,然后再做正确的事情。在执行此操作时,您应该记住的一点是,由SQL缓存的执行计划对于一个或另一个路径可能是非最佳的。我认为最好的方法是调用两个不同的存储过程。
1 2 3 4 5 | FirstSP: IF EXISTS CALL SecondSP (UpdateProc) ELSE CALL ThirdSP (InsertProc) |
现在,我不经常听从自己的建议,所以吃点盐吧。
正在执行if exists…否则…至少要做两个请求(一个要检查,一个要采取行动)。以下方法只需要一个记录所在的位置,如果需要插入,则需要两个:
1 2 3 4 5 | 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') |
做一个选择,如果你得到一个结果,更新它,如果没有,创建它。