假设MyTable的表结构(KEY, datafield1, datafield2…)

通常,我希望更新现有记录,或者插入不存在的新记录。

从本质上讲:

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

最好的写法是什么?


当前回答

Before everyone jumps to HOLDLOCK-s out of fear from these nafarious users running your sprocs directly :-) let me point out that you have to guarantee uniqueness of new PK-s by design (identity keys, sequence generators in Oracle, unique indexes for external ID-s, queries covered by indexes). That's the alpha and omega of the issue. If you don't have that, no HOLDLOCK-s of the universe are going to save you and if you do have that then you don't need anything beyond UPDLOCK on the first select (or to use update first).

scproc通常在非常受控的条件下运行,并假设有可信的调用者(中间层)。这意味着如果一个简单的upsert模式(更新+插入或合并)看到重复的PK,这意味着在中间层或表设计中存在错误,SQL将在这种情况下调用错误并拒绝记录。在这种情况下,放置HOLDLOCK除了降低性能外,还等于吃掉异常并接收潜在的错误数据。

话虽如此,使用MERGE或UPDATE然后INSERT在您的服务器上更容易,而且更容易出错,因为您不必记得在第一次选择时添加(UPDLOCK)。此外,如果您正在小批量地进行插入/更新,则需要了解数据,以便决定某个事务是否合适。如果它只是一个不相关记录的集合,那么额外的“包络”交易将是有害的。

其他回答

IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

编辑:

唉,即使对我自己不利,我也必须承认,没有选择的解决方案似乎更好,因为它们少了一步就完成了任务。

您可以使用MERGE语句,此语句用于插入不存在的数据或更新存在的数据。

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

这取决于使用模式。一个人必须在不迷失在细节的情况下看到使用的大局观。例如,如果使用模式是在创建记录后99%更新,那么'UPSERT'是最佳解决方案。

在第一次插入(hit)之后,它将全部是单个语句更新,没有if或but。插入的'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

如果您首先尝试更新,然后插入,那么竞态条件真的重要吗? 假设你有两个线程想要为key key设置一个值:

线程1:value = 1 线程2:value = 2

竞态条件场景示例

键没有定义 线程1更新失败 线程2更新失败 线程1或线程2中的一个成功插入。例如线程1 另一个线程插入失败(错误重复键)-线程2。 结果:要插入的两个步骤中的“第一个”决定值。 期望结果:最后一个线程写入数据(更新或插入)应该决定值

但;在多线程环境中,操作系统调度器决定线程执行的顺序——在上面的场景中,我们有这个竞争条件,是操作系统决定执行的顺序。从系统的观点来看,说“线程1”或“线程2”是“第一个”是错误的。

当线程1和线程2的执行时间非常接近时,竞态条件的结果并不重要。唯一的要求应该是其中一个线程应该定义结果值。

对于实现:如果更新后插入导致错误的“重复键”,这应该被视为成功。

此外,我们当然不应该假定数据库中的值与您最后写入的值相同。

许多人会建议您使用MERGE,但我提醒您不要使用它。默认情况下,它不会像多条语句那样保护你不受并发性和竞态条件的影响,它还会带来其他危险:

注意SQL Server的MERGE语句 那么,你想使用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;

请停止使用此UPSERT反模式

很多人会这样建议:

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捕获您本来可以在第一个位置防止的异常代价要高得多,那么这就有问题了,除非在几乎所有插入都失败的罕见情况下。我在这里证明:

在进入TRY/CATCH之前检查潜在的约束违反 不同错误处理技术对性能的影响