这个 SQL Server PK 违规怎么可能?

Aba*_*cus 6 sql-server primary-key transaction

我收到了一个我认为不可能的错误。我有一个 SQL Server(11.0.6594 版)表,如下所示:

CREATE TABLE [IDMaster](
  [ID] [int] NOT NULL,
 CONSTRAINT [PK_IDMaster] PRIMARY KEY CLUSTERED 
([ID] ASC) ON [PRIMARY]
) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

它基本上应该做 IDENTITY 现在所做的事情(已经有一段时间了)——它用于获取新的唯一整数值​​,这些值在其他几个表中使用。

现在我有一个 SQL 语句,它选择 MAX 值,添加到它,然后将该值插入到同一个表中——所有这些都在一个语句中。所以从理论上讲,这个语句不可能违反 PK(尽管我认为可能会陷入僵局)。但是,不知何故,它越来越之一。

违反 PRIMARY KEY 约束“PK_IDMaster”。无法在对象“IDMaster”中插入重复键。重复的键值为 (25309587)。

我不知道这怎么可能,但显然是这样;因为它刚刚发生。

想象一下,尝试从同一张表中生成新 ID 值的任何其他方法的疯狂、奇怪的混合,它们可能与此系统中存在的内容相去甚远。但是我想不出它们会导致该特定错误的任何方式。这是我的理解是SQL Server所使用的交易锁定强制执行,不具有(NOLOCK)一个SQL语句,它或任何东西内,也不能与行,它是引用的任何干扰。

唯一的另一个因素,我认为可能不相关但谁知道,是该 SQL 包含在 TRY 中,并设置了一些选项。这是出现该错误的 SQL:

SET XACT_ABORT ON;
SET IMPLICIT_TRANSACTIONS ON;
BEGIN TRY

INSERT INTO IDMaster (ID) 
 SELECT COALESCE(
  -- make sure the new value is odd
  CASE WHEN MAX(COALESCE(ID,0)) % 2 = 0
   THEN MAX(COALESCE(ID,0)) + 1
   ELSE MAX(COALESCE(ID,0)) + 2
   END,1)
 FROM IDMaster;

END TRY
BEGIN CATCH
  WHILE (@@TRANCOUNT > 0) ROLLBACK;
  THROW;
END CATCH
WHILE (@@TRANCOUNT > 0) COMMIT;
Run Code Online (Sandbox Code Playgroud)

请解释:

  1. 怎么可能得到那个错误?
  2. 如果可能,我怎样才能防止它发生(不改变表格)?

Sea*_*ser 23

现在我有一个 SQL 语句,它选择 MAX 值,添加到它,然后将该值插入到同一个表中——所有这些都在一个语句中。所以从理论上讲,这个语句不可能违反 PK(尽管我认为可能会陷入僵局)。但是,不知何故,它正在得到一个。

这绝对可能的。这是由于您的隔离级别以及锁定的工作方式造成的。

违反 PRIMARY KEY 约束“PK_IDMaster”。无法在对象“IDMaster”中插入重复键。重复的键值为 (25309587)。

这是由于锁定;最初在读提交隔离级别将在(在这种情况下)键上获取共享锁,同时读取最大值是什么。弄清楚之后,需要一个X锁才能插入表中。这就是它的简单概述。

当您只有一个会话时,这不是问题。当您有多个会话时,这是一个巨大的问题,因为共享锁彼此兼容。这意味着多个读取器可以获得相同的值。

不知道这怎么可能,但显然是这样;因为它刚刚发生。

看上面。

那么你如何解决它?

  1. 不要试图比 SQL Server 更聪明。使用适合需要的可用构造 - 在 SQL Server 2012(主要版本 11)中具有标识和序列。使用它们。

  2. 通过使用可序列化或锁定提示(如 XLOCK)故意终止并发。这会故意阻止其他会话,这意味着您的性能会降低……而您是故意这样做的……所以这看起来很糟糕,但这是一个可能的解决方案。

  3. 预先创建值(再次尝试比 SQL Server 更智能)并使用 XLOCK + READPAST 从不同的表中获得更好的并发性。废话。让我重申 #1,不要试图比 SQL Server 更聪明 [在这种情况下]。

  • @Abacus 不,#2 和 #3 回答那部分。这很糟糕,但这是一个答案,因为您不想解决实际问题,但更多的是不惜一切代价让它工作而不改变任何东西,即使这意味着停止所有工作。 (3认同)

jmo*_*eno 6

你的理论是错误的。从您的“身份”表中读取不会锁定它,插入它会锁定它。虽然它们是单个复合语句的一部分,但它们仍然是单独的操作。在读取和插入之间的时间内,两个(或更多)连接可以从表中读取。插入使用锁,因此实际上只有一个连接能够将它们读取的值写入表中,其他连接将如您发现的那样获得主键冲突错误。

最好的解决方案是使用其中一种内置方法来处理此问题:标识列、序列或 guid。通常应该避免使用自己的解决方案:您根本没有数据库管理系统所做的经验或用户群(并且您正在处理抽象,因此即使您是主要数据库之一的程序员之一管理系统,您将无法做同样的事情)。