在浏览 SO 时,我发现以下关于插入尚不存在的记录的“最佳”方法的问题/讨论。令我印象深刻的陈述之一是 [Remus Rusanu] 说:
两种变体都不正确。您将插入重复的@value1、@value2 对,保证。
尽管我确实同意检查与插入“分离”的语法(并且不存在显式锁定/事务管理);我很难理解为什么以及何时对于看起来像这样的其他建议语法是正确的
INSERT INTO mytable (x)
SELECT @x WHERE NOT EXISTS (SELECT * FROM mytable WHERE x = @x);
我不想开始(另一个)最好/最快的讨论,我也不认为语法可以“替换”唯一索引/约束(或 PK),但我真的需要知道这种结构在什么情况下会导致双打'过去一直在使用这种语法,并且想知道将来继续这样做是否不安全。
我认为发生的情况是 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
瞧,输出窗口现在包含大量错误
消息 2627,级别 14,状态 1,第 8 行违反主键约束“PK__t_test__3BD019E521C3B7EE”。无法在对象“dbo.t_test”中插入 > 重复键。重复键值为 (57581873)。
仅供参考:正如 Andomar 所指出的,添加 HOLDLOCK 和/或 SERIALIZABLE 提示确实“解决”了问题,但结果却导致了很多死锁......当我想通的时候,这不是很好,但也不是出乎意料。
猜猜我有很多代码审查要做......