插入记录的最佳做法是什么?如果它还不存在?

时间:2016-07-21 07:06:49

标签: sql-server tsql

我知道至少有三种方法可以插入记录,如果它不存在于表格中:

  1. 第一个是使用,如果不存在:

    IF NOT EXISTS(select 1 from table where <condition>)
        INSERT...VALUES
    
  2. 第二个是使用合并:

    MERGE table AS target  
    USING (SELECT values) AS source 
    ON (condition)  
    WHEN NOT MATCHED THEN  
    INSERT ... VALUES ...
    
  3. 第三个是使用insert ... select:

    INSERT INTO table (<values list>)
    SELECT <values list>
    WHERE NOT EXISTS(select 1 from table where <condition>)
    
  4. 但哪一个最好?

    第一个选项似乎不是线程安全的,因为如果两个或多个用户尝试插入相同的记录,则可能会在if和下面的insert语句中的select语句之间插入记录。

    至于第二个选项,合并似乎对此有些过分,因为documentation表示:

      

    性能提示:当两个表具有复杂的匹配特征混合时,MERGE语句描述的条件行为最有效。例如,如果行不存在则插入行,或者如果行匹配则更新行。当简单地基于另一个表的行更新一个表时,可以使用基本的INSERT,UPDATE和DELETE语句实现改进的性能和可伸缩性。

    所以我认为第三种选择对于这种情况是最好的(如果它不存在则只插入记录,如果没有则需要更新),但我想知道SQL Server专家的想法

    请注意,在插入之后,我不想知道唱片是否已存在或是否是一张全新的唱片,我只需要它就在那里以便我可以继续其余的存储过程。

2 个答案:

答案 0 :(得分:0)

当您需要保证在无法用UNIQUE或PRIMARY KEY约束表达的条件下记录的唯一性时,您确实需要确保在一个事务中完成对存在和插入的检查。您可以通过以下任一方式实现此目的:

  1. 使用一个执行检查和插入的SQL语句(第三个选项)
  2. 使用具有适当隔离级别的事务
  3. 第四种方法可以帮助您更好地构建代码,并使其在您需要一次处理一批记录的情况下工作。您可以创建TABLE变量或临时表,插入需要插入的所有记录,然后根据此变量编写INSERT,UPDATE和DELETE语句。

    下面是演示此方法的(伪)代码:

    -- Logic to create the data to be inserted if necessary
    
    DECLARE @toInsert TABLE (idCol INT PRIMARY KEY,dataCol VARCHAR(MAX))
    INSERT INTO @toInsert (idCol,dataCol) VALUES (1,'row 1'),(2,'row 2'),(3,'row 3')
    
    -- Logic to insert the data
    
    INSERT INTO realTable (idCol,dataCol)
    SELECT TI.*
    FROM @toInsert TI
    WHERE NOT EXISTS (SELECT 1 FROM realTable RT WHERE RT.dataCol=TI.dataCol)
    

    在许多情况下,我使用这种方法,因为它使TSQL代码更容易阅读,可以重构和应用单元测试。

答案 1 :(得分:0)

在弗拉基米尔·巴拉诺夫的comment之后,阅读Dan Guzman关于Conditional INSERT/UPDATE Race Condition“UPSERT” Race Condition With MERGE的博客文章,似乎这三个选项在多用户环境中都有同样的缺点。

取消合并选项作为矫枉过正,我们留下选项1和3。

Dan提出的解决方案是使用显式事务并向select添加锁定提示以避免竞争条件。

这样,选项1变为:

BEGIN TRANSACTION
IF NOT EXISTS(select 1 from table WITH (UPDLOCK, HOLDLOCK) where <condition>)
BEGIN
    INSERT...VALUES
END
COMMIT TRANSACTION

和选项2变为:

BEGIN TRANSACTION
INSERT INTO table (<values list>)
SELECT <values list>
WHERE NOT EXISTS(select 1 from table WITH (UPDLOCK, HOLDLOCK)where <condition>)
COMMIT TRANSACTION

当然,在这两个选项中都需要进行一些错误处理 - 每个事务都应该使用try ... catch,这样我们就可以在发生错误的情况下回滚事务。

话虽如此,我认为第三种选择可能是我个人的最爱,但我不认为应该有所不同。

更新

在我对Aaron Bertrand的conversation之后的其他问题的评论中 - 我并不完全相信使用ISOLATION LEVEL比单个查询提示更好的解决方案,但至少这是另一个需要考虑的选择:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

BEGIN TRANSACTION;
INSERT INTO table (<values list>)
SELECT <values list>
WHERE NOT EXISTS(select 1 from table where <condition>);
COMMIT TRANSACTION;