【问题标题】:Violation of UNIQUE KEY constraint on INSERT WHERE COUNT(*) = 0 on SQL Server 2005在 SQL Server 2005 上的 INSERT WHERE COUNT(*) = 0 上违反 UNIQUE KEY 约束
【发布时间】:2011-04-16 21:36:12
【问题描述】:

我正在从多个进程插入 SQL 数据库。进程有时可能会尝试将重复数据插入表中。我尝试以一种可以处理重复项的方式编写查询,但我仍然得到:

System.Data.SqlClient.SqlException: Violation of UNIQUE KEY constraint 'UK1_MyTable'. Cannot insert duplicate key in object 'dbo.MyTable'.
The statement has been terminated.

我的查询类似于:

INSERT INTO MyTable (FieldA, FieldB, FieldC)
SELECT FieldA='AValue', FieldB='BValue', FieldC='CValue'
WHERE (SELECT COUNT(*) FROM MyTable WHERE FieldA='AValue' AND FieldB='BValue' AND FieldC='CValue' ) = 0

约束“UK1_MyConstraint”表示在 MyTable 中,3 个字段的组合应该是唯一的。

我的问题:

  1. 为什么这不起作用?
  2. 我需要进行哪些修改才能避免因违反约束而出现异常?

请注意,我知道还有其他方法可以解决“如果不存在则插入”的原始问题,例如(总而言之):

  • 使用 TRY CATCH
  • IF NOT EXIST INSERT(在具有可序列化隔离的事务内)

我应该使用其中一种方法吗?

编辑 1 用于创建表的 SQL:

CREATE TABLE [dbo].[MyTable](
  [Id] [bigint] IDENTITY(1,1) NOT NULL,
  [FieldA] [bigint] NOT NULL,
  [FieldB] [int] NOT NULL,
  [FieldC] [char](3) NULL,
  [FieldD] [float] NULL,
  CONSTRAINT [PK_MyTable] PRIMARY KEY NONCLUSTERED 
  (
    [Id] ASC
  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON),
  CONSTRAINT [UK1_MyTable] UNIQUE NONCLUSTERED 
  (
    [FieldA] ASC,
    [FieldB] ASC,
    [FieldC] ASC
  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
)

编辑 2 决定:

只是为了更新这一点 - 我决定使用链接问题 (link) 中建议的“JFDI”实现。虽然我仍然对为什么原始实现不起作用感到好奇。

【问题讨论】:

  • 我没有什么有用的补充,但是你现在的声望(1234)和我行李箱的组合一样。
  • 我希望您的行李组合在我的声誉发生变化时保持同步。如果我的声誉达到 5 位数,您可能需要获得新锁 :-)
  • 我认为@Mike Cheel 拥有它,或者至少已经确定了一种可能导致这种情况的情况。如果 BValue 或 CValue 为空,除非 ANSI_NULLS 为 OFF,否则您的 SELECT COUNT(*) 将返回 0。
  • @pgroke - 您当前的方法中没有任何东西可以防止这些错误。在足够高的负载下,您会看到它们。

标签: sql-server sql-server-2005


【解决方案1】:

为什么这不起作用?

我相信 SQL Server 的默认行为是在不再需要共享锁时立即释放它们。您的子查询将在表上产生一个短暂的共享 (S) 锁,该锁将在子查询完成后立即释放。

此时,没有什么可以阻止并发事务插入您刚刚验证不存在的行。

我需要进行哪些修改才能避免因违反约束而引发异常?

HOLDLOCK 提示添加到您的子查询将指示 SQL Server 保持锁定直到事务完成。 (在您的情况下,这是一个隐式事务。)HOLDLOCK 提示等同于 SERIALIZABLE 提示,它本身等同于您在“其他方法”列表中引用的可序列化事务隔离级别。

HOLDLOCK 提示就足以保留 S 锁并防止并发事务插入您要防范的行。但是,您可能会发现您的唯一密钥违规错误被死锁所取代,并且发生频率相同。

如果您只保留表上的 S 锁,请考虑两次插入同一行的并发尝试之间的竞争,以锁步进行 - 都成功获取表上的 S 锁,但都无法成功获取执行插入所需的独占 (X) 锁。

幸运的是,在这种情况下还有另一种锁类型,称为更新 (U) 锁。 U 锁与 S 锁相同,但有以下区别:虽然可以在同一资源上同时持有多个 S 锁,但一次只能持有一个 U 锁。 (换句话说,虽然 S 锁相互兼容(即可以无冲突共存),但 U 锁相互不兼容,但可以与 S 锁共存;更进一步,排他(X)锁不兼容 S 或 U 锁)

您可以使用UPDLOCK 提示将子查询上的隐式 S 锁升级为 U 锁。

在表中插入同一行的两个并发尝试现在将在初始 select 语句中序列化,因为这会获取(并持有)一个 U 锁,该 U 锁与来自并发插入尝试的另一个 U 锁不兼容。

空值

FieldC 允许 NULL 值这一事实可能会产生一个单独的问题。

如果ANSI_NULLS 开启(默认),则相等检查FieldC=NULL 将返回false,即使在FieldC 为NULL 的情况下(当ANSI_NULLS 开启时,您必须使用IS NULL 运算符来检查null )。由于 FieldC 可以为空,因此在插入 NULL 值时,您的重复检查将不起作用。

要正确处理空值,您需要修改 EXISTS 子查询以在插入 NULL 值时使用 IS NULL 运算符而不是 =。 (或者您可以更改表以禁止在所有相关列中使用 NULL。)

SQL Server 联机丛书参考

【讨论】:

  • 有一个类似问题的答案也建议使用 ROWLOCK:“rowlock 允许对现有行进行并发更新。”这是真的? stackoverflow.com/questions/639854/…
  • 是否可以提供一个 sn-p 显示他的查询在 HOLDLOCK 下的样子
  • 我认为这个答案并不完整,它取决于当前的事务隔离级别。例如,这个不适用于已提交的隔离级别。请参阅stackoverflow.com/questions/639854/… 了解更多信息。
【解决方案2】:

RE:“我仍然很好奇为什么最初的实现不起作用。”

为什么会起作用?

有什么办法可以防止两个并发事务如下交错?

Tran A                                Tran B
---------------------------------------------
SELECT COUNT(*)...
                                  SELECT COUNT(*)...
INSERT ....
                                  INSERT... (duplicate key violation).

只有在Insert阶段才会使用冲突锁。

在 SQL Profiler 中查看此内容

创建表格脚本

create table MyTable
(
FieldA int NOT NULL, 
FieldB int NOT NULL, 
FieldC int NOT NULL
)
create unique nonclustered index ix on  MyTable(FieldA, FieldB, FieldC)

然后将以下内容粘贴到两个不同的 SSMS 窗口中。记下连接的 spid(x 和 y)并设置 SQL Profiler Trace 以捕获锁定事件和用户错误消息。应用 spid=x 或 y 和严重性 = 0 的过滤器,然后执行这两个脚本。

插入脚本

DECLARE @FieldA INT, @FieldB INT, @FieldC INT
SET NOCOUNT ON
SET CONTEXT_INFO 0x696E736572742074657374

BEGIN TRY
WHILE 1=1
    BEGIN

        SET @FieldA=( (CAST(GETDATE() AS FLOAT) - FLOOR(CAST(GETDATE() AS FLOAT))) * 24 * 60 * 60 * 300)
        SET @FieldB = @FieldA
        SET @FieldC = @FieldA

        RAISERROR('beginning insert',0,1) WITH NOWAIT
        INSERT INTO MyTable (FieldA, FieldB, FieldC)
        SELECT FieldA=@FieldA, FieldB=@FieldB, FieldC=@FieldC
        WHERE (SELECT COUNT(*) FROM MyTable WHERE FieldA=@FieldA AND FieldB=@FieldB AND FieldC=@FieldC ) = 0
    END
END TRY
BEGIN CATCH
    DECLARE @message VARCHAR(500)
    SELECT @message = 'in catch block ' + ERROR_MESSAGE()
    RAISERROR(@message,0,1) WITH NOWAIT
    DECLARE @killspid VARCHAR(10) 
    SELECT @killspid = 'kill ' +CAST(SPID AS VARCHAR(4)) FROM sys.sysprocesses WHERE SPID!=@@SPID AND CONTEXT_INFO = (SELECT CONTEXT_INFO FROM sys.sysprocesses WHERE SPID=@@SPID)
    EXEC ( @killspid )
END CATCH

【讨论】:

  • 这正是我正在寻找的——更深入地了解实际情况。我在子查询上使用 UPDLOCK 和 HOLDLOCK(基于对链接问题的回答)进行了测试,也解决了问题 - 但我不明白为什么。
  • @Iain - 要深入了解这一点,您可以使用 SQL Profiler 跟踪语句执行期间发生的各种锁定事件。
【解决方案3】:

在我的脑海中,我有一种感觉,其中一个或多个列接受空值。我想查看包含约束的表的创建语句。

【讨论】:

  • 按要求添加的用于创建表的查询。
  • @Adrift - 诸如 OP 之类的查询在负载下失败是完全正常的。不能保证 2 个并发事务不会同时执行 SELECT COUNT 位(两者都将获得不冲突的 S 锁)然后继续执行具有相同值的插入,此时第二个会失败。
  • 我可以在不使用空值的情况下轻松重现这一点。我设置了一个简单的测试控制台应用程序,它使用硬编码值执行查询。如果我同时运行其中的 20 个,至少有 1 个会遇到问题。
  • 对于 FieldC(接受 NULL),当分配值为 NULL 时,您是否将 = 符号更改为 IS?
  • 我不确定分配的值是否会为空。在这种情况下,您应该问“为什么它接受空值?” - 我回答 - “不知道,会调查它”。 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多