不清楚更新冲突

Pav*_* Zv 13 sql-server database-internals locking snapshot-isolation sql-server-2016

我有两个问题:

1. 为什么在这种情况下会出现更新冲突而不是阻塞:

-- prepare
drop database if exists [TestSI];
go
create database [TestSI];
go
alter database [TestSI] set READ_COMMITTED_SNAPSHOT ON;
alter database [TestSI] set ALLOW_SNAPSHOT_ISOLATION ON;
go
use [TestSI];
go
drop table if exists dbo.call_test;
create table dbo.call_test ( Id bigint CONSTRAINT [PK_Call] PRIMARY KEY CLUSTERED ( [Id] ASC ), additional int, incl int );
create index ix_Call on dbo.call_test ( additional ) include( incl );
insert into dbo.call_test select 1, 2, 3;
go
Run Code Online (Sandbox Code Playgroud)

第一节:

use [TestSI];
go
set transaction isolation level snapshot
begin tran

   UPDATE dbo.call_test SET additional = 22 WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

第二次会议:

use [TestSI];
go
set transaction isolation level snapshot

   UPDATE dbo.call_test SET additional = 222 WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

在第二个会话中,我立即得到:

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

如果我更新包含列incl而不是非聚集索引键,我也会有这种行为。

在这种情况下,非聚集索引对更新冲突有什么影响?为什么在这种情况下不使用锁?

2. 第二个理论问题:

SQL Server 如何处理包含列更新?

我的意思是当我们更新这个值时,SQL Server 如何更新所有包含列的非聚集索引?我在查询计划中没有看到任何相关内容。

select @@version
Run Code Online (Sandbox Code Playgroud)

Microsoft SQL Server 2016 (SP2) (KB4052908) - 13.0.5026.0 (X64) Mar 18 2018 09:11:49 版权所有 (c) Microsoft Corporation Developer Edition(64 位),Windows 10 Pro 10.0(Build 18363: )(Hy )

我在 SQL Server 2019 上检查了这个示例,该服务器上的行为与我预期的一样:第二个会话被锁定。这是一个错误还是我做错了什么?

Pau*_*ite 17

为什么在这种情况下会出现更新冲突,而不仅仅是阻塞

这是一个产品缺陷,已在 SQL Server 2019 中修复。

当快照事务尝试修改已被在快照事务开始后提交的另一个事务修改的行时,会发生快照写入冲突。

您的示例中行为不正确的原因有点深奥。更新计划使用称为Rowset Sharing 的东西。这意味着聚集索引查找聚集索引更新共享一个公共行集。

这是一种优化,因此聚集索引更新不需要通过正常的查找操作来定位要更新的行。Clustered Index Seek已经正确定位了公共行集。更新运算符在行集中的“当前行”上执行其工作。

这会导致错误消息,因为查找看到的行版本(未提交更改之前的行)与更新操作符共享。更新发现它尝试更新的行已更改,并(错误地)断定发生了更新冲突。

可以通过多种方式获得正确的行为。重写更新以使行集无法共享的一种方法是强制搜索使用不同的索引。使用不同的访问方法,没有可以共享的公共行集:

UPDATE CT
SET CT.additional = 222
FROM dbo.call_test AS CT WITH (INDEX(ix_Call))
WHERE CT.Id = 1;
Run Code Online (Sandbox Code Playgroud)

不同的索引

更直接的方法是使用未记录且不受支持的跟踪标志来禁用行集共享优化(这仅用于演示目的,请勿在真实数据库上使用):

UPDATE dbo.call_test 
SET additional = 222 
WHERE [Id] = 1
OPTION (QUERYTRACEON 8746);
Run Code Online (Sandbox Code Playgroud)

该计划看起来与原始计划相同(默认情况下不公开行集共享属性),但它会正确阻止而不是引发更新冲突错误。

您还可以通过强制执行广泛的(每个索引)更新计划来避免错误(并保留Clustered Index Update 的行集共享):

UPDATE dbo.call_test 
SET additional = 222 
WHERE [Id] = 1
OPTION (QUERYTRACEON 8790);
Run Code Online (Sandbox Code Playgroud)

广泛的更新计划

遇到该错误需要行集共享基表更新,该更新还维护二级索引(窄或每行更新)。

如果此行为导致您遇到实际问题,您应该向 Microsoft 提交支持案例。


乔希正确回答了你的第二个问题。我将补充一点,您可以在 SSMS 中的Clustered Index Update操作符上看到非集群索引维护——您需要查看 Properties 窗口并展开 Object 节点:

安全管理系统


Jos*_*ell 10

  1. 第二个理论问题:

SQL Server 如何处理包含列更新?
我的意思是当我们更新这个值时,SQL Server 如何更新所有包含列的非聚集索引?我在查询计划中没有看到任何相关内容。

我不确定我是否理解第一点发生了什么,我发现 SQL Server 2017 和 2019 之间的行为差​​异更有趣,但我可以帮助消除这里的谜团。

非聚集索引更新不会显示在 SSMS 图形执行计划中,但您可以在 XML 中看到它提到:

  <Update DMLRequestSort="false">
    <Object Database="[TestSI]" Schema="[dbo]" Table="[call_test]" Index="[PK_Call]" IndexKind="Clustered" Storage="RowStore" />
    <Object Database="[TestSI]" Schema="[dbo]" Table="[call_test]" Index="[ix_Call]" IndexKind="NonClustered" Storage="RowStore" />
Run Code Online (Sandbox Code Playgroud)

此外,Sentry One Plan Explorer 在更新图标上放置了一个漂亮的小指示器,让您知道非聚集索引正在“幕后”更新:

显示 NC 索引更新的计划资源管理器屏幕截图

这被称为“狭义更新计划”,至少通俗地说(我在任何地方的官方文档中都没有看到)。您可以在 Paul White 的这篇博文中看到窄更新计划和宽更新计划之间差异的示例:优化更改数据的 T-SQL 查询