sql - 为什么按主键分组的插入会抛出违反主键约束的错误?

标签 sql sql-server database

我有一个插入语句抛出主键错误,但我不明白我怎么可能插入重复的键值。

首先,我创建了一个带有主键的临时表。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED //Note: I've tried committed and uncommited, neither materially affects the behavior. See screenshots below for proof.

IF (OBJECT_ID('TEMPDB..#P')) IS NOT NULL DROP TABLE #P;

CREATE TABLE #P(idIsbn INT NOT NULL PRIMARY KEY, price SMALLMONEY, priceChangedDate DATETIME);

然后我从价格表中提取价格,按 idIsbn 分组,这是临时表中的主键。

INSERT  INTO #P(idIsbn, price, priceChangedDate)
SELECT  idIsbn ,
        MIN(lowestPrice) ,
        MIN(priceChangedDate)
FROM Price p
WHERE p.idMarketplace = 3100
GROUP BY p.idIsbn

我知道根据定义按 idIsbn 分组使其独一无二。价格表中的 idIsbn 是:[idIsbn] [int] NOT NULL

但每隔一段时间,当我运行这个查询时,我会得到这个错误:

Violation of PRIMARY KEY constraint 'PK__#P________AED35F8119E85FC5'. Cannot insert duplicate key in object 'dbo.#P'. The duplicate key value is (1447858).

注意:我有很多关于时间的问题。我会选择这条语句,按F5,不会出现错误。然后我会再做一次,它会失败,然后我会一次又一次地运行它,它会在再次失败之前成功几次。我想我想说的是,我找不到关于何时成功和何时不成功的模式。

如果 (A) 我在插入之前刚刚创建了全新的表,并且 (B) 我按设计为主键的列进行分组,我该如何插入重复行?

目前,我正在使用 IGNORE_DUP_KEY = ON 解决问题,但我真的很想知道问题的根本原因。

这是我在 SSMS 窗口中实际看到的内容。不多也不少:

enter image description here

@@版本是:

Microsoft SQL Server 2008 (SP3) - 10.0.5538.0 (X64) 
    Apr  3 2015 14:50:02 
    Copyright (c) 1988-2008 Microsoft Corporation
    Standard Edition (64-bit) on Windows NT 6.1 <X64> (Build 7601: Service Pack 1)

执行计划: enter image description here

这是一个正常运行时的示例。在这里,我使用的是 READ COMMITTED,但这并不重要 b/c 无论我读取它是已提交还是未提交,我都会收到错误。 enter image description here

这是它失败的另一个例子,这次是 READ COMMITTED。

enter image description here

还有:

  • 无论是填充临时表还是 持久表。
  • 当我将 option (maxdop 1) 添加到插入的末尾时,它似乎每次都失败了,尽管我不能完全确定 b/c 我无法运行它为无穷大。但似乎是这样。

这里是价格表的定义。表有 25M 行。过去一小时内有 108,529 次更新。

CREATE TABLE [dbo].[Price](
    [idPrice] [int] IDENTITY(1,1) NOT NULL,
    [idIsbn] [int] NOT NULL,
    [idMarketplace] [int] NOT NULL,
    [lowestPrice] [smallmoney] NULL,
    [offers] [smallint] NULL,
    [priceDate] [smalldatetime] NOT NULL,
    [priceChangedDate] [smalldatetime] NULL,
 CONSTRAINT [pk_Price] PRIMARY KEY CLUSTERED 
(
    [idPrice] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
 CONSTRAINT [uc_idIsbn_idMarketplace] UNIQUE NONCLUSTERED 
(
    [idIsbn] ASC,
    [idMarketplace] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

还有两个非聚集索引:

CREATE NONCLUSTERED INDEX [IX_Price_idMarketplace_INC_idIsbn_lowestPrice_priceDate] ON [dbo].[Price]
(
    [idMarketplace] ASC
)
INCLUDE (   [idIsbn],
    [lowestPrice],
    [priceDate]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [IX_Price_idMarketplace_priceChangedDate_INC_idIsbn_lowestPrice] ON [dbo].[Price]
(
    [idMarketplace] ASC,
    [priceChangedDate] ASC
)
INCLUDE (   [idIsbn],
    [lowestPrice]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

最佳答案

您没有提供表结构。

这是一个具有一些假设细节的重现,这些细节导致读取提交时出现问题(注意:现在您已经提供了我可以在您的案例中看到的定义更新到 priceChangedDate 列将移动行IX_Price_idMarketplace_priceChangedDate_INC_idIsbn_lowestPrice 索引(如果正在寻找)

连接1(设置表)

USE tempdb;

CREATE TABLE Price
  (
     SomeKey          INT PRIMARY KEY CLUSTERED,
     idIsbn           INT IDENTITY UNIQUE,
     idMarketplace    INT DEFAULT 3100,
     lowestPrice      SMALLMONEY DEFAULT $1.23,
     priceChangedDate DATETIME DEFAULT GETDATE()
  );

CREATE NONCLUSTERED INDEX ix
  ON Price(idMarketplace)
  INCLUDE (idIsbn, lowestPrice, priceChangedDate);

INSERT INTO Price
            (SomeKey)
SELECT number
FROM   master..spt_values
WHERE  number BETWEEN 1 AND 2000
       AND type = 'P'; 

连接2

并发 DataModifications,将行从搜索范围 (3100,1) 的开头移动到结尾 (3100,2001) 并重复返回。

USE tempdb;

WHILE 1=1
BEGIN
UPDATE Price SET SomeKey = 2001 WHERE SomeKey = 1
UPDATE Price SET SomeKey = 1 WHERE SomeKey = 2001
END

连接 3(使用唯一约束插入临时表)

USE tempdb;

CREATE TABLE #P
  (
     idIsbn           INT NOT NULL PRIMARY KEY,
     price            SMALLMONEY,
     priceChangedDate DATETIME
  );

WHILE 1 = 1
  BEGIN
      TRUNCATE TABLE #P

      INSERT INTO #P
                  (idIsbn,
                   price,
                   priceChangedDate)
      SELECT idIsbn,
             MIN(lowestPrice),
             MIN(priceChangedDate)
      FROM   Price p
      WHERE  p.idMarketplace = 3100
      GROUP  BY p.idIsbn
  END 

enter image description here

该计划没有聚合,因为对 idIsbn 有唯一约束(对 idIsbn、idMarketplace 的唯一约束也可以),因此可以优化分组依据,因为没有重复值。

但是在读取已提交隔离级别,共享行锁会在读取行后立即释放。因此一行有可能移动位置并被同一次查找或扫描第二次读取。

索引 ix 没有明确包含 SomeKey 作为辅助键列,但是因为它没有被声明为唯一的,SQL Server 默默地包含了幕后的集群键,因此更新该列值可以移动其中的行。

关于sql - 为什么按主键分组的插入会抛出违反主键约束的错误?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36318449/

相关文章:

python - 将数据库名称插入Sqlite3命令中

c# - LINQ 查询 AND & OR

mysql - 两张 table 合二为一?

mysql - 是否真的需要在所有使用它的表中将列定义为外键

sql - PostgreSQL 错误 : Subquery has too many columns

node.js - 无法使用 Node.js、mssql 和express 连接到 Microsoft SQL Server

sql - 删除表中记录数给定的记录数

sql-server - 我可以安排一个作业在每个时区的午夜运行,以及如何让进程知道要处理哪个时区?

c# - 如何从数据库读取特定文本框为文本框背景颜色着色

PHP警告: PHP Startup: Unable to load dynamic library '/usr/lib/php/20151012/mysql.so'