在SQL Server 2005上INSERT WHERE COUNT(*)= 0上违反UNIQUE KEY约束

Iai*_*ain 21 sql-server sql-server-2005

我从多个进程插入一个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.
Run Code Online (Sandbox Code Playgroud)

我的查询看起来像:

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
Run Code Online (Sandbox Code Playgroud)

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

我的问题:

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

请注意,我知道还有其他方法可以解决"INSERT if not exists"的原始问题,例如(摘要):

  • 使用TRY CATCH
  • 如果不存在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)
)
Run Code Online (Sandbox Code Playgroud)

编辑2决定:

只是为了更新这个 - 我决定使用链接问题(链接)中建议的"JFDI"实现.虽然我仍然很好奇为什么原来的实现不起作用.

Dan*_*nov 48

为什么这不起作用?

我相信SQL Server的默认行为是在不再需要时立即释放共享锁.您的子查询将导致表上的短暂共享(S)锁定,该子锁定将在子查询完成后立即释放.

此时,没有什么可以防止并发事务插入刚刚验证的行.

我需要进行哪些修改才能因违反约束而无法出现异常?

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

HOLDLOCK单独暗示将足以保持S锁,防止并发事务从插入你防范行.但是,您可能会发现您的唯一密钥违例错误被死锁替换,发生在同一频率上.

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

幸运的是,这个确切的场景还有另一种锁类型,称为Update(U)锁.U锁与S锁相同,但有以下区别:虽然可以在同一资源上同时保持多个S锁,但一次只能保持一个U锁.(换句话说,虽然S锁相互兼容(即可以共存但没有冲突),U锁彼此不兼容,但可以与S锁共存;并且沿着频谱,独占(X)锁不是兼容S或U锁)

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

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

NULL值

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

如果ANSI_NULLS处于打开状态(默认),那么FieldC=NULL即使在FieldC为NULL的情况下,相等性检查也会返回false(启用时必须使用IS NULL运算符检查null ANSI_NULLS).由于FieldC可以为空,因此在插入NULL值时,重复检查将不起作用.

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

SQL Server联机丛书参考


Mar*_*ith 5

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

为什么会起作用?

有什么可以防止两个并发事务按如下方式交错?

Tran A                                Tran B
---------------------------------------------
SELECT COUNT(*)...
                                  SELECT COUNT(*)...
INSERT ....
                                  INSERT... (duplicate key violation).
Run Code Online (Sandbox Code Playgroud)

唯一发生冲突锁的时间是在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)
Run Code Online (Sandbox Code Playgroud)

然后将以下内容粘贴到两个不同的 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
Run Code Online (Sandbox Code Playgroud)

  • @Iain - 要深入了解这一点,您可以使用 SQL Profiler 来跟踪在语句执行期间发生的各种锁定事件。 (2认同)