2

这项工作是从由于 FK 检查导致 SQL Server 中的更新冲突而中止的快照隔离事务之后进行的

看完这篇优秀的文章(https://sqlperformance.com/2021/06/sql-performance/foreign-keys-blocking-update-conflicts#comment-167645

我运行此代码来创建数据库:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Parent]
(
    [ParentID] [int] NOT NULL,
    [UpdateTime] [datetime] NOT NULL,

    CONSTRAINT [PK dbo.Parent ParentID] 
        PRIMARY KEY CLUSTERED ([ParentID] ASC)
                WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                      ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Child]
(
    [ChildID] [int] NOT NULL,
    [ParentID] [int] NULL,
    [UpdateTime] [datetime] NULL,

    CONSTRAINT [PK dbo.Child ChildID] 
        PRIMARY KEY CLUSTERED ([ChildID] ASC)
                WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                      ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Child] WITH CHECK 
    ADD CONSTRAINT [FK dbo.Child to dbo.Parent] 
        FOREIGN KEY([ParentID]) REFERENCES [dbo].[Parent] ([ParentID])
GO

ALTER TABLE [dbo].[Child] CHECK CONSTRAINT [FK dbo.Child to dbo.Parent]
GO

CREATE TABLE [dbo].[Dummy]
(
    [x] [int] NULL
) ON [PRIMARY]
GO

数据库必须允许快照隔离并启用已提交的读取快照。

填充数据库如下:

DELETE FROM dbo.Child
DELETE FROM dbo.Parent
GO

-- Insert parent rows
INSERT INTO dbo.Parent (ParentID, UpdateTime) VALUES (1, GETUTCDATE());
INSERT INTO dbo.Parent (ParentID, UpdateTime) VALUES (2, GETUTCDATE());
INSERT INTO dbo.Parent (ParentID, UpdateTime) VALUES (3, GETUTCDATE());

-- Insertion a child row
INSERT INTO dbo.Child select 101, 2, GetUTCDate()
go

然后运行这两个脚本(按照指示):

-- Session 1 - part one (1st bit to run)
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
 
-- Ensure snapshot transaction is started
SELECT COUNT_BIG(*) FROM dbo.Dummy AS D;

-- Session 1 - part two (3rd bit to run)
DELETE FROM dbo.Parent WHERE ParentID = 3
-- Session 2 - part one (2nd bit to run)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRANSACTION;

UPDATE dbo.Child
SET UpdateTime = GETUTCDATE()
WHERE ParentID = 1

INSERT INTO dbo.Child 
    SELECT 201, 2, GetUTCDate()

-- Session 2 - part two (4th bit to run)
COMMIT TRANSACTION;

会话 1 按预期生成更新错误:

消息 3960,级别 16,状态 1,第 10 行
快照隔离事务因更新冲突而中止。您不能使用快照隔离直接或间接访问数据库“Example Changed”中的表“dbo.Child”来更新、删除或插入已被另一个事务修改或删除的行。重试事务或更改更新/删除语句的隔离级别。

根据文章,这应该通过将 PK 更改为非聚集并添加唯一的聚集索引来消除,但会产生相同的错误。这样做应该可以解决 FK 问题,但似乎并没有解决,即使执行计划暗示它应该这样做。

为什么现在是这样?

谢谢伊恩

4

1 回答 1

1

您的问题是外键ParentID没有索引,因此每个DELETEon都Parent需要扫描整个Child表以确保没有 FK 一致性问题。这会导致隔离中的锁定冲突SNAPSHOT,而在其他隔离级别会导致死锁。

将索引添加到Child

CREATE INDEX IX_Parent ON Child (ParentID);

您会看到锁定冲突已经消失。

所有主键和外键都必须有一个索引(这些列作为前导键)。如果您缺少外键上的索引,那么您将在父表的主键上遇到锁定UPDATE问题DELETE。如果您缺少主键上的索引,那么您将遇到子表上的INSERT锁定问题。UPDATE

您可以将其他列添加为 key 的一部分或 as INCLUDE,但 PK 或 FK 必须是索引中的前导列。

你可以在这个 fiddle中看到索引的效果。


将主键更改Parent为非聚集键并在同一列上添加另一个聚集键是一个坏主意,而且完全没有意义。

那篇文章中的特定示例是指每个表上有两个唯一键,一组之间有一个外键的情况。在这种情况下,在某些情况下创建两个单独的索引是明智的。但是,您应该始终对所有主键、唯一键和外键都有一个索引。

于 2022-01-23T20:56:37.500 回答