仅在尚未插入的行插入


72

我一直使用类似于以下内容的方法来实现它:

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语句是原子性的。

有人有什么想法吗?


3
您是否尝试过使用不带WHEN MATCHED子句的合并?
2010年

3
您正在使用哪个版本的SQL Server?
马丁·史密斯

它取决于客户端。2000 R2和2008 R2之间的任何值。尽管我们最初写声明时可能已经是7岁了!
亚当

我必须看看这个新的MERGE声明(对我来说)。在这种情况下,效果是否更好?
亚当

1
我看不出重点!只需插入您的数据,如果PK已经存在,则插入将失败,这会很好。还是我错过了什么?
帕特里克·奥诺涅兹

Answers:


65

“ JFDI”模式呢?

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

认真地说,这是最快且最并发的无锁操作,尤其是在大容量情况下。如果UPDLOCK升级并且整个表被锁定怎么办?

阅读第4课

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


谢谢-好的,我同意这可能是我最终将要使用的,并且是对实际问题的答案。
亚当

1
我知道依靠这样的错误是不好的,但是我想知道是否仅使用直线INSERT(不带EXISTS)会更好地执行此操作(即无论如何尝试插入并忽略错误2627)。
亚当

1
这取决于你是否主要是插入不存在的值或者大部分值存在。在后一种情况下,我认为由于会引发和忽略大量异常,因此性能会较差。
Gserg

@Gserg:正确。但是,OP可能会发布一个INSERT / UPDATE问题,而不是测试惰性。我们用它来每天过滤掉成千上万个新行中的几千个重复
gbn

4
@学生35k tps =“每秒35000个事务”。TRY CATCH通过捕获唯一约束冲突错误(错误号2627)并忽略它来防止插入重复项。如果不是2627,那么CATCH只会抛出该错误。此代码段存在问题,因为唯一索引冲突是错误2601。因此,您必须检查这两个代码。该解决方案也仅适用于单行INSERT。如果尝试从一个表插入到另一个表,则需要不同的策略。
吉姆(Jim)

24

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

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

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

同样,如果您实际上想更新一行(如果存在),如果行不存在则进行插入,则可能会发现此问题很有用。


2
当该行不存在时,您要锁定什么?
马丁·史密斯

3
索引中的相关范围(在这种情况下为主键)。
Gserg

@GSerg同意。select语句的悲观/乐观锁定需要一个指令。
DaveWilliamson

2
测试显示select 'Done.' where exists(select 0 from foo_testing with(updlock) where id = 4);提供的id=4两个不存在,并且彼此不冲突,这意味着我的原始答案实际上是错误的。解决方案是添加HOLDLOCK提示。查看编辑后的答案。感谢您让我
烦恼

3
在Daniel对我的问题(非常相似)的回答中,有一个很好的解释为什么需要此锁定:stackoverflow.com/questions/3789287/…–
Iain

17

您可以使用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)

在这种情况下,您可以删除“匹配时”,因为亚当只需要在丢失时插入,而无需重新插入。
Iain

4
抱歉,但是没有在您的merge语句中添加保持锁定提示,您将遇到OP所关心的确切问题。
EBarr 2011年

7
有关@EBarr的更多内容,请参阅本文-Martin
Smith

1
@MartinSmith-这是我遇到此问题时所读的确切文章!感谢您的参考。
EBarr 2011年

MSDN文档(性能TIP)是其中不存在的,而不是合并,你应该使用刀片,除非需要复杂... msdn.microsoft.com/en-us/library/...
姆拉登·米哈伊洛维奇


1

首先,我们的男人@gbn对社区做出了巨大的贡献。甚至无法开始解释我听他的建议的频率。

无论如何,足够的粉丝。

要稍微补充一下他的答案,也许可以“增强”它。对于像我这样的人,他们对<> 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给你们俩!
T0t3sMcG0t3s

-5

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

     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

为什么不这样做:如果不存在(从表中选择* param1field = @ param1和param2field = @ param2)开始插入表(param1Field,param2Field)中的值(param1,param2)END
Vidar Nordnes

是的,但是这似乎对并发问题开放(即,如果您的选择和插入之间的另一个连接发生了什么情况该怎么办?)
Adam

2
上面的@Adam Marc的代码对于避免锁定问题没有任何好处。处理并发问题的唯一两种方法是使用WITH(UPDLOCK,HOLDLOCK)进行锁定或处理插入错误并将其转换为更新。
ErikE
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.