sql - INSERT <table> (x) VALUES (@x) WHERE NOT EXISTS ( SELECT * FROM <table> WHERE x = @x) 会导致重复吗?

标签 sql sql-server

在浏览 SO 时,我发现了 following关于插入尚不存在的记录的“最佳”方法的问题/讨论。令我印象深刻的声明之一是 [Remus Rusanu] 的声明之一:

Both variants are incorrect. You will insert pairs of duplicate @value1, @value2, guaranteed.



尽管我确实同意检查与 INSERT '分离'的语法(并且不存在显式锁定/事务管理);我很难理解为什么以及何时对于看起来像这样的其他建议语法是正确的
INSERT INTO mytable (x)
SELECT @x WHERE NOT EXISTS (SELECT * FROM mytable WHERE x = @x);

我不想开始(另一个)什么是最好/最快的讨论,我也不认为语法可以“替换”一个唯一的索引/约束(或 PK),但我真的需要知道在什么情况下这种结构会导致 double ,因为我过去一直在使用这种语法,并想知道将来继续这样做是否不安全。

我认为发生的是 INSERT 和 SELECT 都在同一个(隐式)事务中。查询将对相关记录(键)进行 IX 锁定,并且在整个查询完成之前不会释放它,因此只有在插入记录之后。
这个锁会阻止所有其他连接进行相同的 INSERT,因为在我们的插入完成之前,它们自己无法获得锁;只有这样他们才能获得锁并开始自己验证记录是否已经存在。

恕我直言,找出答案的最佳方法是通过测试,我已经在笔记本电脑上运行以下代码一段时间了:

创建表
CREATE TABLE t_test (x int NOT NULL PRIMARY KEY (x))

在许多许多并行连接上运行)
SET NOCOUNT ON

WHILE 1 = 1
    BEGIN
        INSERT t_test (x)
        SELECT x = DatePart(ms, CURRENT_TIMESTAMP)
         WHERE NOT EXISTS ( SELECT *
                              FROM t_test old
                             WHERE old.x = DatePart(ms, CURRENT_TIMESTAMP) )
    END

到目前为止,唯一需要注意的是:
  • 没有遇到错误(还)
  • CPU 运行得很热 =)
  • 表快速保存了 300 条记录(由于日期时间的 3 毫秒“精度”),之后没有再发生实际插入,正如预期的那样。

  • 更新:

    结果我上面的例子没有做我想要做的。而不是多个连接试图同时插入相同的记录,我只是让它在第一秒后不插入已经存在的记录。因为在下一个连接上复制粘贴和执行查询可能需要大约一秒钟的时间,所以从来没有重复的危险。剩下的时间我都会戴上我的驴耳朵......

    无论如何,我已经使测试更符合手头的问题(使用同一张表)
    SET NOCOUNT ON
    
    DECLARE @midnight datetime
    SELECT @midnight = Convert(datetime, Convert(varchar, CURRENT_TIMESTAMP, 106), 106)
    
    WHILE 1 = 1
        BEGIN
            INSERT t_test (x)
            SELECT x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP)
             WHERE NOT EXISTS ( SELECT *
                                  FROM t_test old
                                 WHERE old.x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP))
        END
    

    瞧,输出窗口现在包含大量错误

    Msg 2627, Level 14, State 1, Line 8 Violation of PRIMARY KEY constraint 'PK__t_test__3BD019E521C3B7EE'. Cannot insert >duplicate key in object 'dbo.t_test'. The duplicate key value is (57581873).



    仅供引用:正如 Andomar 所指出的,添加 HOLDLOCK 和/或 SERIALIZABLE 提示确实“解决”了问题,但结果却导致了很多死锁......这不是很好,但当我仔细考虑时也并不意外。

    猜猜我有很多代码审查要做......

    最佳答案

    感谢您发布单独的问题。你有几个误解:

    The query will take an IX lock on the related record (key) and not release it until the entire query has finished



    INSERT 将锁定插入的行,X 锁(像 IX 这样的意图锁只能在锁层次结构上的父实体上请求,而不能在记录上请求)。这个锁必须一直保持到事务提交(严格 two-phase locking 要求 X 锁总是在事务结束时才被释放)。

    请注意, INSERT 获取的锁不会阻止更多的插入,即使是同一个键。防止重复的唯一方法是唯一索引,并且强制唯一性的机制不是基于锁的。是的,在主键上,由于其唯一性,将防止重复,但作用力是不同的,即使锁定确实起作用。

    在您的示例中,由于新插入行上的 X 与 S 锁冲突,操作将序列化,因为 INSERT 上的 SELECT 块。另一个需要考虑的想法是 300 条 INT 类型的记录将适合单个页面,并且会进行大量优化(例如,使用扫描而不是多次搜索)并会改变测试结果。请记住,一个有很多积极因素而没有证据的假设仍然只是一个猜想......

    要测试问题,您需要确保 INSERT 不会阻止并发 SELECT。在 RCSI 或快照隔离下运行是实现这一目标的一种方法(并且可能会不自觉地在生产中“实现”它并破坏做出上述所有假设的应用程序......) WHERE 子句是另一种方式。一个非常大的表和二级索引是另一种方式。

    所以这是我测试它的方法:
    set nocount on;
    go
    
    drop database test;
    go
    
    create database test;
    go
    
    use test;
    go
    
    create table test (id int primary key, filler char(200));
    go
    
    -- seed 10000 values, fill some pages
    declare @i int = 0;
    begin transaction
    while @i < 10000
    begin
        insert into test (id) values (@i);
        set @i += 1;
    end
    commit;
    

    现在从几个并行连接运行它(我使用了 3 个):
    use test;
    go
    
    set nocount on;
    go
    
    declare @i int;
    while (1=1)
    begin
        -- This is not cheating. This ensures that many concurrent SELECT attempt 
        -- to insert the same values, and all of them believe the values are 'free'
        select @i = max(id) from test with (readpast);
        insert into test (id)
        select id
            from (values (@i), (@i+1), (@i+2), (@i+3), (@i+4), (@i+5)) as t(id)
            where t.id not in (select id from test);
    end
    

    以下是一些结果:
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130076).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130096).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130106).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130121).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130141).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130151).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130176).
    The statement has been terminated.
    Msg 2627, Level 14, State 1, Line 6
    

    即使有锁定,没有快照隔离,也没有 RCSI。当每个 SELECT 尝试插入 @i+1...@i+5 时,它们都会发现值不存在,然后它们都会继续进行 INSERT。一名幸运的获胜者将成功,其余的将导致PK违规。频繁地。我用了 @i=MAX(id)故意显着增加冲突的追逐,但这不是必需的。我将找出为什么所有违规发生在值 %5+1 上的问题作为练习。

    关于sql - INSERT <table> (x) VALUES (@x) WHERE NOT EXISTS ( SELECT * FROM <table> WHERE x = @x) 会导致重复吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19095429/

    相关文章:

    mysql - 包含两列的 COUNT,每列都有自己的 AS 值

    Mysql - 在编号 7 的 vnum END 处查询 UPDATE

    c# - Convert.ToString(DateTime) 产生英国格式而不是美国格式

    Sql Server - 在查询中选择逻辑结果

    sql - 存储过程超时..删除,然后创建,然后又恢复了?

    java - Db2 从具有动态值的 jdbc 合并

    mysql - 同一属性 : null entries, EAV 存在多种可能的数据类型,还是存储为 varchar?

    php - 如何找出 MySQL 表中的索引

    javascript - Express js - 数据库连接中的 Promise 待定

    SQL Server 动态排序依据