为什么此查询生成PK违规错误?

时间:2018-08-27 12:48:11

标签: sql-server sql-server-2008

因此,我试图在一个查询中,仅在不存在的行中插入行。

我的查询如下:

INSERT INTO [dbo].[users_roles] ([user_id], [role_id]) 
SELECT 29851, 1 WHERE NOT EXISTS (
  SELECT 1 FROM [dbo].[users_roles] WHERE user_id = 29851 AND role_id = 1)

有时(非常罕见,但仍然),它会产生以下错误:

  

违反了PRIMARY KEY约束'PK_USERS_ROLES'。不能   在对象“ dbo.users_roles”中插入重复键。重复的   键值为(29851,1)。

PK_USERS_ROLES[user_id], [role_id]。这是表模式的完整SQL:

create table users_roles
(
    user_id int not null
        constraint FK_USERS_ROLES_USER
        references user,
    role_id int not null
        constraint FK_USERS_ROLES_USER_ROLE
        references user_role,
    constraint PK_USERS_ROLES
    primary key (user_id, role_id)
)

上下文

这是由Apache服务器上托管的PHP脚本执行的,并且在数百次事件中(很可能是与并发相关的事件)发生一次“随机”。

更多信息:

  • SELECT @@VERSION给出:
  

Microsoft SQL Server 2008 R2(SP2)-10.50.4000.0(X64)2012年6月28日   08:36:30版权所有(c)Microsoft Corporation Enterprise Edition   Windows NT 6.1(内部版本7601:Service Pack)上安装(64位)

  • SQL Server版本:SQL Server 2008 R2

  • 事务隔离级别:ReadCommitted

  • 这是在 explicit 事务中执行的(通过PHP语句,但是我认为最终结果是相同的)

问题

  • 有人可以解释为什么/如何发生吗?

  • 一种安全地一次性插入(换句话说,在单个查询中)的有效方法是什么? 我看到了其他答案,例如this one,但这些解决方案仅适用于存储过程。

谢谢。

2 个答案:

答案 0 :(得分:1)

此表是否已被截断或行已删除?以及多久一次?对我来说有意义的是,由于您正在运行“如果不存在则插入”,因此一时不应该找到行,并且此刻可能有两个或更多查询访问数据库以插入相同的数据……只有一个将...如果在行“不存在”之前插入该行,则另一项不执行任何操作;如果在行之后插入该行,则失败。

我现在只有一个Oracle数据库可以进行一些测试,我可以重现此问题。我的提交模式很明确:

  • 创建一个空表,唯一约束并授予选择权,然后插入另一个用户:

    CREATE TABLE just_a_test (val NUMBER(3,0));
    
    ALTER TABLE just_a_test ADD CONSTRAINT foobar UNIQUE (val);
    
    GRANT SELECT, INSERT ON just_a_test TO user2;
    
  • 用户1上的数据库会话:

    INSERT INTO just_a_test 
    SELECT 10 
    FROM DUAL 
    WHERE NOT EXISTS 
    (
      SELECT 1 
      FROM just_a_test 
      WHERE val = 10
    )
    ;
    
    -- no commit yet...
    
  • 用户2上的数据库会话:

    INSERT INTO user1.just_a_test 
    SELECT 10 
    FROM DUAL 
    WHERE NOT EXISTS 
    (
      SELECT 1 
      FROM user1.just_a_test 
      WHERE val = 10
    )
    ;
    
    
    -- no commit yet, the db just hangs til the other session commit...
    

所以我提交了第一个事务,插入了行,然后在user2会话上收到以下错误:

"unique constraint violated"
*Cause:    An UPDATE or INSERT statement attempted to insert a duplicate key.
           For Trusted Oracle configured in DBMS MAC mode, you may see
           this message if a duplicate entry exists at a different level.

现在,我回滚第二个事务,并在user2上再次运行相同的插入操作,现在我得到以下输出:

0 rows inserted.

您的情况可能与此类似。希望对您有所帮助。

编辑

对不起。您问了两个问题,而我只回答了Could someone explain why/how this is happening?个问题。所以我错过了What would be an efficient way to safely insert in one go (in other words, in a single query)?

“安全”到底对您意味着什么?假设您正在运行很多行的INSERT / SELECT,与存储的行相比,其中只有一行是重复的。为了达到“安全”级别,您应该忽略所有要插入的行还是仅忽略重复的行,并存储其他行?

同样,我现在没有SQL Server可以尝试,但是看起来您可以告诉SQL Server是在出现任何重复情况时是否拒绝插入所有行,还是仅拒绝重复项,而保留其他重复项。对于单行插入同样有效...如果是dup,则抛出错误...或者反之亦然。

语法应如下所示,仅忽略dup行并且不抛出错误:

    ALTER TABLE [TableName] REBUILD WITH (IGNORE_DUP_KEY = ON);

默认情况下,此选项为OFF,这意味着SQL Server会引发错误并丢弃正在插入的非dup行。

通过这种方式,您将保留INSERT / SELECT语法,看起来不错。

希望有帮助。

来源:

https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-table-index-option-transact-sql?view=sql-server-2008

https://stackoverflow.com/a/11207687/1977836

答案 1 :(得分:1)

对此进行明确说明可能会有所帮助。下面在显式事务中运行此操作,显式锁定行。

DECLARE @user_id INT; SET @user_id=29851;
DECLARE @role_id INT; SET @role_id=1;

BEGIN TRY
    BEGIN TRANSACTION;

    DECLARE @exists INT;
    SELECT @exists=1 
    FROM [dbo].[users_roles] WITH(ROWLOCK,HOLDLOCK,XLOCK)
    WHERE user_id=@user_id AND role_id=@role_id;

    IF @exists IS NULL
    BEGIN
        INSERT INTO [dbo].[users_roles] ([user_id], [role_id])
        VALUES(@user_id,@role_id);
    END

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH