【问题标题】:Only inserting a row if it's not already there仅在行不存在时才插入行
【发布时间】:2011-03-25 09:14:39
【问题描述】:

我一直使用类似于以下的东西来实现它:

INSERT INTO TheTable
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WHERE
        PrimaryKey = @primaryKey)

...但是一旦在负载下,就会发生主键冲突。这是唯一插入该表的语句。那么这是否意味着上面的语句不是原子的呢?

问题是这几乎不可能随意重新创建。

也许我可以将其更改为以下内容:

INSERT INTO TheTable
WITH
    (HOLDLOCK,
    UPDLOCK,
    ROWLOCK)
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WITH
        (HOLDLOCK,
        UPDLOCK,
        ROWLOCK)
    WHERE
        PrimaryKey = @primaryKey)

不过,也许我使用了错误的锁或使用了太多的锁等。

我在 stackoverflow.com 上看到了其他问题,其中的答案建议使用“IF (SELECT COUNT(*) ... INSERT”等,但我一直处于(可能不正确)假设下,即单个 SQL 语句会是原子的。

有人有什么想法吗?

【问题讨论】:

  • 您是否尝试过使用不带WHEN MATCHED 子句的合并?
  • 您使用的是什么版本的 SQL Server?
  • 因客户而异。介于 2000 和 2008 R2 之间的任何内容。虽然我们可能在最初写声明时已经在 7 岁!
  • 我必须看看这个新的(对我而言)MERGE 声明。在这种情况下它的表现会更好吗?
  • 我不明白这一点!只需插入您的数据,如果 PK 已经存在,则插入将失败,这很好。还是我错过了什么?

标签: sql sql-server tsql concurrency locking


【解决方案1】:

"JFDI" 模式呢?

BEGIN TRY
   INSERT etc
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
END CATCH

说真的,这是最快的,并且在没有锁的情况下并发最多,尤其是在大容量时。 如果UPDLOCK升级,整个表都被锁定了怎么办?

Read lesson 4:

第 4 课:在调整索引之前开发 upsert proc 时,我首先相信 If Exists(Select…) 行会针对任何项目触发并禁止重复。纳达。在很短的时间内,有数千个重复项,因为同一个项目会在同一毫秒命中 upsert,两个事务都会看到不存在并执行插入。经过大量测试后,解决方案是使用唯一索引,捕获错误,然后重试允许事务查看行并执行更新而不是插入。

【讨论】:

  • 谢谢 - 好的,我同意这可能是我最终会使用的,并且是实际问题的答案。
  • 我知道依赖这样的错误是不好的,但我想知道是否只使用直接的INSERT(没有EXISTS)会表现得更好(即无论如何都尝试插入并且只需忽略错误 2627)。
  • 这取决于您是主要插入不存在的值还是主要确实存在的值。在后一种情况下,我认为性能会更差,因为会引发和忽略大量异常。
  • @Gserg:正确。但是随后 OP 会发布一个 INSERT/UPDATE 问题,而不是测试惰性。我们每天使用它来过滤掉数千个新行中的数千个重复项
  • @student 35k tps = "每秒 35000 笔交易"。 TRY CATCH 通过捕获 unique constraint 违规错误(错误号 2627)并忽略它来防止插入重复条目。如果它是not 2627,CATCH 只会重新抛出错误。这个 sn-p 存在问题,因为 unique index 违规是错误 2601。所以你必须检查对于这两个代码。此解决方案也仅适用于单行 INSERT。如果您尝试从一个表 INSERT 到另一个表,则需要不同的策略。
【解决方案2】:

我添加了最初不存在的 HOLDLOCK。请忽略没有此提示的版本。

就我而言,这应该足够了:

INSERT INTO TheTable 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 0
     FROM TheTable WITH (UPDLOCK, HOLDLOCK)
     WHERE PrimaryKey = @primaryKey) 

此外,如果您确实想要更新一行(如果存在)并插入(如果不存在),您可能会发现 this question 很有用。

【讨论】:

  • 当行不存在时你在锁定什么?
  • 索引中的相关范围(本例中为主键)。
  • @GSerg 同意。 select语句的悲观/乐观锁定需要一个指令。
  • 测试显示两个select 'Done.' where exists(select 0 from foo_testing with(updlock) where id = 4);provided id=4不存在,不相互冲突,也就是说我原来的答案其实是错误的。解决方案是添加HOLDLOCK 提示。请参阅编辑后的答案。感谢您一直困扰我:)
  • 丹尼尔对我的(非常相似的)问题的回答中很好地解释了为什么需要这种锁定:stackoverflow.com/questions/3789287/…
【解决方案3】:

你可以使用 MERGE:

MERGE INTO Target
USING (VALUES (@primaryKey, @value1, @value2)) Source (key, value1, value2)
ON Target.key = Source.key
WHEN MATCHED THEN
    UPDATE SET value1 = Source.value1, value2 = Source.value2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Name, ReasonType) VALUES (@primaryKey, @value1, @value2)

【讨论】:

  • 在这种情况下,您可以删除“WHEN MATCHED THEN”,因为 Adam 只需要在丢失时插入,而不是 upsert。
  • 抱歉,如果不向您的合并语句添加保持锁定提示,您将遇到 OP 所关心的确切问题。
  • 请参阅this article 了解更多关于@EBarr 的观点
  • @MartinSmith - 这是我在遇到问题时阅读的确切文章!感谢您的参考。
  • MSDN 文档声明(在性能提示中)您应该在不存在的地方使用插入而不是合并,除非需要复杂性...msdn.microsoft.com/en-us/library/…
【解决方案4】:

首先,非常感谢我们的人 @gbn 对社区的贡献。甚至无法解释我发现自己遵循他的建议的频率。

无论如何,足够的狂热。

稍微增加他的答案,也许“增强”它。对于像我这样的人来说,在&lt;&gt; 2627 场景中做什么感到不安(并且没有空的CATCH 不是一个选项)。我从technet 找到了这个小金块。

    BEGIN TRY
       INSERT etc
    END TRY
    BEGIN CATCH
        IF ERROR_NUMBER() <> 2627
          BEGIN
                DECLARE @ErrorMessage NVARCHAR(4000);
                DECLARE @ErrorSeverity INT;
                DECLARE @ErrorState INT;

                SELECT @ErrorMessage = ERROR_MESSAGE(),
                @ErrorSeverity = ERROR_SEVERITY(),
                @ErrorState = ERROR_STATE();

                    RAISERROR (
                        @ErrorMessage,
                        @ErrorSeverity,
                        @ErrorState
                    );
          END
    END CATCH

【讨论】:

  • 这正是我在上一个答案中没有方向的地方。为你们俩 +1!
【解决方案5】:

我不知道这是否是“官方”方式,但您可以尝试INSERT,如果失败则回退到UPDATE

【讨论】:

    【解决方案6】:

    我过去曾使用不同的方法进行过类似的操作。首先,我声明一个变量来保存主键。然后我用 select 语句的输出填充该变量,该语句查找具有这些值的记录。然后我做和 IF 语句。如果主键为空,则插入,否则返回错误码。

         DECLARE @existing varchar(10)
        SET @existing = (SELECT primaryKey FROM TABLE WHERE param1field = @param1 AND param2field = @param2)
    
        IF @existing is not null
        BEGIN
        INSERT INTO Table(param1Field, param2Field) VALUES(param1, param2)
        END
        ELSE
        Return 0
    END
    

    【讨论】:

    • 为什么不干脆这样做:如果不存在(SELECT * FROM TABLE WHERE param1field = @param1 AND param2field = @param2)BEGIN INSERT INTO Table(param1Field, param2Field) VALUES(param1, param2) END跨度>
    • 是的,但这看起来很容易出现并发问题(即,如果您的 select 和 insert 之间的另一个连接发生了问题怎么办?)
    • @Adam Marc 的上述代码在避免锁定问题方面并没有任何好处。处理并发问题的唯一两种方法是使用 WITH (UPDLOCK, HOLDLOCK) 锁定或处理插入错误并将其转换为更新。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-30
    • 2016-08-23
    • 1970-01-01
    • 1970-01-01
    • 2017-04-21
    相关资源
    最近更新 更多