问题 选择/插入Upsert的版本:是否有高并发的设计模式?


我想做一个UPSERT的SELECT / INSERT版本。下面是现有代码的模板:

// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))

IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE)
BEGIN
   INSERT Table VALUES (@Value)
   SELECT @id = SCOPEIDENTITY()
END
ELSE
   SELECT @id = RowID FROM Table WHERE RowValue = @VALUE)

将从许多并发会话中调用该查询。我的性能测试表明它会在特定负载下持续抛出主键违规。

这个查询是否有高并发方法,可以保持性能,同时仍然避免插入已经存在的数据?


5304
2017-08-29 07:07


起源

这类似于: stackoverflow.com/questions/13540/... - Sam Saffron
我同意它类似,但我会说不同之处在于不需要更新,只需要插入或选择语句。在您的答案中,您使用SERIALIZABLE作为联合提示。这是您对SELECT语句上面的查询的建议吗? - 8kb
Yerp带有可序列化提示的插入,可能在事务中应该可以解决问题。我可以尝试写一个答案,但这个iPad让我听起来很傲慢:( - Sam Saffron
主要是插入,还是主要更新? - gbn
@gbn ...我认为你的意思大多是插入或选择...这是能够优化这个的关键..如果插入很少,那么你可以从选择开始,然后如果你错过了做一个事务声音插入然后是选择,以防插入没有插入。优化方式高度依赖于访问模式。我不确定MERGE是否是你想要的。 - Sam Saffron


答案:


您可以使用LOCK来制作SERIALIZABLE,但这会降低并发性。为什么不首先尝试常见病症(“主要是插入或大部分选择”),然后安全处理“补救”行动?也就是说,“JFDI”模式......

大多数INSERT预期(球场70-80%+):

试着插入。如果失败,则表示已创建该行。无需担心并发性,因为TRY / CATCH会为您处理重复项。

BEGIN TRY
   INSERT Table VALUES (@Value)
   SELECT @id = SCOPEIDENTITY()
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
    ELSE -- only error was a dupe insert so must already have a row to select
      SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
END CATCH

主要选择:

类似,但尝试先获取数据。无数据= INSERT需要。同样,如果2个并发调用尝试INSERT,因为它们都发现该行缺少TRY / CATCH句柄。

BEGIN TRY
   SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
   IF @@ROWCOUNT = 0
   BEGIN
       INSERT Table VALUES (@Value)
       SELECT @id = SCOPEIDENTITY()
   END
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
    ELSE
      SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
END CATCH

第二个似乎重演,但它是高度并发的。锁会实现相同但却以并发为代价......

编辑:

为什么  使用MERGE ...

如果使用OUTPUT子句,它将仅返回更新的内容。因此,您需要一个虚拟UPDATE来为OUTPUT子句生成INSERTED表。如果你必须通过许多调用进行虚拟更新(如OP所暗示的那样),那就是大量的日志写入 只是 能够使用MERGE。


15
2017-08-29 10:14



@gbn - 为什么你会使用你的第二个建议(高并发)而不是MERGE命令(假设@ 8kb使用的是sql 2008+)? - Pure.Krome
@ Pure.Krome:因为它是插入/选择,而不是插入/更新。你最终会得到一个假的UPDATE来使用OUTPUT。大。 - gbn
@gbn - 所以我们无法检查SCOPE_IDENTITY来抓取 持续 / 最近的 插入的身份..假设插入发生?如果没有,那么你可以使用@@ Rowcount来检查更新是否有效...... - Pure.Krome
@ Pure.Krome:是的 不 更新 - gbn
@gbn - becuase merge确实更新然后插入,你说的是,因为发布说只有插入,然后合并不应该使用..因为它永远不会更新,不应该更新? (我想我现在已经有了)...... - Pure.Krome


// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))

- 确保将RowValue和RowID上的非聚集唯一索引作为聚簇索引。

IF EXISTS (SELECT * FROM Table WHERE RowValue = @VALUE)
   SELECT @id = RowID FROM Table WHERE RowValue = @VALUE
ELSE BEGIN
   INSERT Table VALUES (@Value)
   SELECT @id = SCOPEIDENTITY()
END

1
2017-12-30 09:12





一如既往,gbn的答案是正确的,最终将我带到了我需要的地方。但是,我发现了一个特殊的边缘案例,他的方法没有涵盖。那是一个 2601 标识a的错误 Unique Index Violation

为了弥补这一点,我修改了他的代码如下

...
declare @errornumber int = ERROR_NUMBER()
if @errornumber <> 2627 and @errornumber <> 2601
...

希望这有助于某人!


0
2018-06-14 16:57