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

时间:2013-09-30 13:23:51

标签: sql sql-server

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

  

两种变体都不正确。您将插入一对重复的@ value1,@ value2,保证。

虽然我对于检查与INSERT“分离”的语法(并且不存在显式锁定/事务管理)确实同意这一点;我很难理解为什么以及什么时候这对于其他提出的语法来说是正确的

INSERT INTO mytable (x)
SELECT @x WHERE NOT EXISTS (SELECT * FROM mytable WHERE x = @x);

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

我认为发生的事情是INSERT&amp; 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条记录(由于日期时间的3ms'精度'),之后没有按预期发生任何实际插入。

更新:

原来我上面的例子没有按照我的意图去做。我没有尝试同时插入相同记录的多个连接,而是在第一秒之后没有插入已存在的记录。因为复制粘贴可能需要大约一秒钟。在下一个连接上执行查询,从来没有重复的危险。我将在今天余下的时间里戴着我的驴耳......

无论如何,我已经使测试更符合当前的事情(使用相同的表格)

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

并且lo&amp;不料,输出窗口现在存在很多错误

  

Ms 2627,Level 14,State 1,Line 8   违反PRIMARY KEY约束'PK__t_test__3BD019E521C3B7EE'。无法在对象'dbo.t_test'中插入&gt;重复键。重复键值为(57581873)。

仅供参考:正如Andomar所指出的那样,添加一个HOLDLOCK和/或SERIALIZABLE提示确实“解决”了这个问题,但结果却造成了很多死锁......这不是很好但是当我想的时候并不出乎意料它通过。

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

2 个答案:

答案 0 :(得分:4)

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

  

查询将对相关记录(密钥)进行IX锁定,并且在整个查询完成之前不会释放它

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时,他们都会发现值不是existign然后它们都会进入INSERT。一个幸运的赢家将成功,其余所有将导致PK违规。经常。我故意使用@i=MAX(id)来大幅增加冲突的追逐,但这不是必需的。我将留下问题,找出为什么所有违规行为都发生在值%5 + 1上作为练习。

答案 1 :(得分:3)

您正在从单个连接进行测试,因此您根本不测试并发性。从不同的窗口运行脚本两次,您将开始看到冲突。

冲突有多种原因:

  • 默认情况下,在(隐式)事务结束之前不会保持锁定。使用with (holdlock)查询提示更改此行为。
  • 查询的并发问题称为“幻像读取”。默认事务隔离级别为“已提交读”,但不能防止幻像读取。使用with (serializable)查询提示来提高隔离级别。 (尽量避免使用set transaction isolation level命令,因为连接返回到连接池时isolation level is not cleared。)

始终强制执行主键约束。因此,您的查询将尝试插入重复的行,并通过抛出重复的键错误而失败。

一个好的方法是使用您的查询(99%的时间都可以工作)并使客户端以优雅的方式处理偶尔的重复键异常。

维基百科有一个很棒的explanation of isolation levels