ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SQL Server死锁根因:varchar(max)与INCLUDE索引的执行计划分裂

SQL Server死锁根因:varchar(max)与INCLUDE索引的执行计划分裂

简介:本资源是一份深入剖析SQL Server死锁机制的技术文档,面向数据库开发人员、DBA及中高级SQL性能调优实践者,聚焦一个典型却易被忽视的非聚集索引引发的死锁场景。文档完整复现了因non-clustered index中INCLUDE大字段(varchar(max))与行锁(rowlock)交互导致的循环等待死锁,并系统对比了三种关键干预措施(移除INCLUDE、缩小字段长度、调整索引结构)对死锁消解的影响,同时详述SQL Trace与DBCC TRACEON 1222双路径分析法,含SPID过滤、死锁图解读及sp_readerrorlog日志定位实操。资源为1个694KB的Word文档(.docx),内容结构清晰,含建表脚本、复现步骤、执行计划线索、锁资源堆栈解析及根本原因推演,是理解SQL Server锁升级、索引设计与事务并发冲突的高价值案例材料。目前已有327人学习下载。

1. 这不是锁争用,是执行计划在“演双簧”:一个由varchar(max)+INCLUDE触发的 SQL Server 死锁黑匣子

你有没有遇到过这种场景:两条一模一样的UPDATE语句,并发执行时稳稳当当死锁,但只要把索引里一个INCLUDE(d)去掉,或者把d varchar(max)改成varchar(200),死锁就彻底消失?连ROWLOCK提示都救不了——这不是锁粒度没设对,也不是事务写得太长,而是 SQL Server 在背后悄悄拆分了你的 UPDATE 操作,让两个连接在「找数据」和「改索引」两个阶段错位卡死。这个案例在 SQL Server 2008 到 2019 中均可稳定复现,本质是执行计划分裂导致锁申请顺序不可控,属于典型的“玄学死锁”:表面看是并发冲突,根子上是优化器对大字段 + 覆盖索引的特殊处理逻辑。它专挑那些喜欢用varchar(max)做业务字段、又习惯给非聚集索引加INCLUDE的系统下手——这类设计在 OLTP 场景下极易翻车。本文不讲抽象理论,直接带你从建表、复现、抓锁、读执行计划到定位根因,每一步都可粘贴即跑,所有命令、输出片段、参数含义全部实测验证。如果你正被类似死锁折磨,或想建立一套可落地的 SQL Server 死锁分析肌肉记忆,这篇就是为你写的。

2. 复现环境搭建与三组对照实验:用最小闭环验证死锁触发条件

要真正理解这个死锁,必须亲手把它“养”出来。我们不依赖生产库,也不模拟复杂业务逻辑,只用四条 SQL 就能构建出完全可控的死锁现场。关键在于三组严格对照实验:原始问题组(必死)→ 索引精简组(不死)→ 字段瘦身组(不死)。每组都基于同一张表结构,仅变更一个变量,确保结论干净、可归因。

2.1 创建基础表与双非聚集索引:聚焦INCLUDE(d)的埋点位置

我们创建一张极简表tt,主键为id(聚集索引),并建立两个非聚集索引:ix_a_bc和ix_b_cd。注意INCLUDE(d)是核心变量,它让索引“携带”了d字段的副本,从而避免回表——但代价是更新时必须同步维护该副本。

-- 创建测试表(SQL Server 2008+ 兼容) CREATE TABLE tt ( id INT IDENTITY PRIMARY KEY, a CHAR(36), b CHAR(36), d VARCHAR(MAX) -- 关键:MAX 类型,存储开销大 ); GO -- 创建两个非聚集索引,均 INCLUDE d 字段(问题根源起点) CREATE INDEX ix_a_bc ON tt(a) INCLUDE(d); CREATE INDEX ix_b_cd ON tt(b) INCLUDE(d); GO

参数说明:INCLUDE(d)表示将d字段值物理存储在索引叶级页中,不参与索引排序,但可被SELECT直接取到。对UPDATE而言,这意味着修改d时,不仅主键索引要更新,这两个非聚集索引的叶级页也必须同步写入新值——这是后续锁分裂的物理基础。

2.2 插入测试数据并提取目标值:确保每次复现条件一致

插入 10000 条记录后,我们固定选取id = 10的行作为更新目标。a字段值是NEWID()生成的 GUID,每次运行不同,因此必须先查出来再用于后续UPDATE。这步看似琐碎,却是复现稳定性的命脉。

-- 批量插入 10000 行(使用 GO 10000 实现循环) INSERT INTO tt SELECT NEWID(), 'bbb', 'ddd'; GO 10000 -- 查询第 10 行的 a 值(结果示例:'EF211985-EA72-4A40-81DA-0AAB076E7AA3') SELECT a FROM tt WHERE id = 10;

逻辑说明:GO 10000是 SQLCMD 模式下的批处理指令,表示前一条INSERT语句执行 10000 次。NEWID()确保a值高度离散,使ix_a_bc索引查找走 B-Tree 搜索而非扫描,精准触发单行更新路径。若用NEWID()以外的值(如固定字符串),可能导致索引查找退化为扫描,破坏死锁触发条件。

2.3 三组对照实验脚本:一键切换,秒级验证死锁存在性

我们将三组实验封装为独立脚本块,只需注释/取消注释对应部分,即可在 SSMS 中快速切换。每组均启动两个查询窗口(SPID A 和 SPID B),同时运行无限循环UPDATE。

-- 【实验组1:原始问题】必死锁(varchar(max) + INCLUDE(d)) -- 在窗口A中执行: WHILE 1=1 BEGIN UPDATE tt WITH(ROWLOCK) SET d = 'cd' WHERE a = 'EF211985-EA72-4A40-81DA-0AAB076E7AA3'; WAITFOR DELAY '00:00:00.01'; -- 防止 CPU 占满,便于观察 END -- 在窗口B中执行(相同语句): WHILE 1=1 BEGIN UPDATE tt WITH(ROWLOCK) SET d = 'cd' WHERE a = 'EF211985-EA72-4A40-81DA-0AAB076E7AA3'; WAITFOR DELAY '00:00:00.01'; END
-- 【实验组2:索引精简】不死锁(保留 varchar(max),移除 INCLUDE) -- 先执行此清理脚本: DROP INDEX ix_a_bc ON tt; DROP INDEX ix_b_cd ON tt; CREATE INDEX ix_a_bc ON tt(a); -- 移除 INCLUDE(d) CREATE INDEX ix_b_cd ON tt(b); -- 移除 INCLUDE(d) GO -- 然后在窗口A/B中运行同上的 UPDATE 循环 → 无死锁
-- 【实验组3:字段瘦身】不死锁(保留 INCLUDE,改 varchar(200)) -- 先执行此清理脚本: DROP INDEX ix_a_bc ON tt; DROP INDEX ix_b_cd ON tt; ALTER TABLE tt ALTER COLUMN d VARCHAR(200); -- 关键变更 CREATE INDEX ix_a_bc ON tt(a) INCLUDE(d); CREATE INDEX ix_b_cd ON tt(b) INCLUDE(d); GO -- 然后在窗口A/B中运行同上的 UPDATE 循环 → 无死锁

现象对比总结:

  • 实验组1:约 3~10 秒内必然触发死锁,SSMS 弹出错误Msg 1205, Level 13, State 45, Line X: Deadlock encountered ...;
  • 实验组2 & 3:持续运行 5 分钟以上,CPU 占用平稳,无任何错误;
  • 关键洞察:死锁与UPDATE语句本身无关,而与d字段的存储方式(MAXvs200)及索引是否“携带”该字段(INCLUDE)强耦合。这直接指向执行计划层面的差异。

3. 死锁捕获与诊断:用DBCC TRACEON(1222,-1)+ SQL Server Profiler 定位锁资源争用链

复现只是第一步,真正的硬功夫在“看见”死锁。SQL Server 提供两套互补工具:TRACEON(1222)输出结构化死锁图到错误日志,SQL Server Profiler 捕获实时锁事件流。二者结合,才能拼出完整因果链。这里不讲概念,只给可抄作业的配置清单和解读口诀。

3.1 开启全局死锁跟踪:让 SQL Server 主动“写日记”

在复现前,必须启用1222跟踪标志,它会将每次死锁的 XML 结构写入 SQL Server 错误日志(ErrorLog)。这是最权威的死锁证据源,比 Profiler 更可靠(Profier 可能漏事件)。

-- 启用全局死锁跟踪(需 sysadmin 权限) DBCC TRACEON(1222, -1); -- 验证是否生效(返回 1222 表示已启用) DBCC TRACESTATUS(1222, -1);

参数说明:-1表示全局生效(所有会话),1222是死锁诊断专用标志。开启后无需重启服务,但建议在测试库操作。关闭用DBCC TRACEOFF(1222, -1)。注意:生产环境慎用,长期开启有轻微性能开销。

3.2 SQL Server Profiler 配置:只抓关键锁事件,拒绝信息过载

Profier 不是用来“录屏”,而是精准过滤出与死锁直接相关的锁生命周期事件。我们只关注Locks类别下的Deadlock Graph、Lock:Acquired、Lock:Released,以及TSQL类别下的SQL:BatchCompleted。重点是SPID 过滤——只监控你手动启动的两个 UPDATE 连接(如 SPID 54 和 60)。

配置项推荐设置为什么这样设
Events Selection勾选Show all events→ 展开Locks→ 勾选Deadlock Graph,Lock:Acquired,Lock:Released;展开TSQL→ 勾选SQL:BatchCompletedDeadlock Graph提供可视化死锁图;Lock:Acquired/Released记录每个锁的申请/释放时间戳和对象;SQL:BatchCompleted关联 SQL 文本与锁事件
Column FiltersSPID→Like→54,60(填入你实际的两个 SPID)避免海量系统锁事件干扰,聚焦问题连接
Event FilteringApplicationName→Like→%Management Studio%(可选)进一步限定来源,防止其他应用干扰

操作提示:启动 Profiler 后,先点击Run开始监听,再在 SSMS 中打开两个新查询窗口执行UPDATE循环。死锁发生后,Profiler 会立即捕获Deadlock Graph事件,双击即可看到交互式图形(含进程、资源、等待关系)。但图形有时不清晰,此时需结合Lock:Acquired事件列表按StartTime排序,人工梳理锁序列。

3.3 解析1222输出:从 XML 中提取“谁锁了谁”的关键证据

死锁发生后,执行sp_readerrorlog查看最新错误日志,找到以<deadlock-list>开头的 XML 块。我们跳过冗长 XML,直取三个核心字段:

-- 读取最近错误日志(0=当前,1=上一个...) EXEC sp_readerrorlog 0, 1, 'deadlock';

关键字段提取口诀(对照1222输出):

  • victim=processXXXXX:死锁牺牲者(被 Kill 的 SPID),如victim=process5e27708→ 对应 SPID 60;
  • waitresource=KEY: 8:72057594066108416 (ef8a9edf5a1e):等待的资源,KEY:表示键级别锁,72057594066108416是hobtid(Heap or B-tree ID),即索引物理标识;
  • lockMode=X/lockMode=U:锁模式,X=排他锁,U=更新锁(升级为 X 前的中间态);
  • executionStack:SQL 执行栈,确认确实是UPDATE tt ... WHERE a=...在执行。

实战技巧:hobtid数字看不懂?用以下脚本秒查对应索引名:

SELECT o.name AS table_name, i.name AS index_name, i.type_desc AS index_type FROM sys.partitions p JOIN sys.indexes i ON p.object_id = i.object_id AND p.index_id = i.index_id JOIN sys.objects o ON i.object_id = o.object_id WHERE p.partition_id = 72057594066108416; -- 替换为你看到的 hobtid

输出示例:table_name=tt,index_name=ix_a_bc,index_type=NONCLUSTERED→ 锁发生在ix_a_bc索引上。

4. 执行计划深度剖析:三组实验的SET STATISTICS PROFILE ON对比揭示锁分裂根源

死锁的表象是锁等待,根因是执行计划。1222告诉你“谁锁了谁”,而执行计划告诉你“为什么需要锁这些资源”。我们用SET STATISTICS PROFILE ON获取带实际行数的执行计划(文字版),对比三组实验,答案一目了然。

4.1 获取执行计划:STATISTICS PROFILE比EXPLAIN更贴近真实执行

在 SSMS 中,对UPDATE语句前加SET STATISTICS PROFILE ON,执行后结果网格下方会出现一个“执行计划”结果集,包含Rows(实际影响行数)、Executes(执行次数)、EstimateRows(预估行数)等列。这是诊断锁行为的黄金数据。

-- 在实验组1(问题组)中执行: SET STATISTICS PROFILE ON; UPDATE tt WITH(ROWLOCK) SET d = 'cd' WHERE a = 'EF211985-EA72-4A40-81DA-0AAB076E7AA3'; SET STATISTICS PROFILE OFF;

参数说明:STATISTICS PROFILE开启后,SQL Server 在执行时收集每个操作符的实际性能数据(如 IO、CPU、行数),比SHOWPLAN_TEXT更真实。WITH(ROWLOCK)提示在此处有效,确保我们看到的是行锁行为,而非页锁或表锁。

4.2 三组执行计划关键对比:锁定操作符数量决定死锁概率

下表提炼三组实验中UPDATE语句执行计划的核心操作符(Operator)及其数量。操作符数量 = 锁申请次数的理论下限。

实验组d字段类型INCLUDE(d)执行计划关键操作符操作符数量锁申请阶段推断
组1(问题)VARCHAR(MAX)✅ 有Index Update(ix_a_bc),Index Update(PK),Index Update(ix_b_cd)3个分三阶段:先锁ix_a_bc找行 → 再锁PK改主键页 → 最后锁ix_b_cd改索引页
组2(精简)VARCHAR(MAX)❌ 无Index Update(PK)1个仅锁PK:因d不在索引中,UPDATE必须回表,故只更新聚集索引
组3(瘦身)VARCHAR(200)✅ 有Index Update(ix_a_bc, PK, ix_b_cd) ——单操作符,三对象1个一步到位:d字段变小后,SQL Server 选择“合并更新”,在单个Index Update操作中同时刷三个索引页

技术深挖:为什么VARCHAR(MAX)+INCLUDE会导致三阶段更新?因为MAX类型数据可能存储在行外(LOB Storage),SQL Server 无法保证在单次 I/O 中原子更新所有相关页。优化器被迫拆解为多个独立Index Update操作,每个操作申请自己的锁,形成锁申请序列。而VARCHAR(200)数据总在行内,且大小可控,优化器判断可安全合并,故生成单操作符计划。

4.3 锁序列还原:从 ProfilerLock:Acquired事件反推执行步骤

回到 Profiler 捕获的Lock:Acquired事件列表,按StartTime排序,我们能清晰看到组1的锁申请序列(简化版):

时间戳SPIDObjectID2 (hobtid)Mode对应索引阶段
T15472057594066108416Uix_a_bc阶段1:用a值查找目标行,申请更新锁
T25472057594065518592UPK__tt__...阶段1:找到行后,对主键页申请更新锁
T35472057594065518592XPK__tt__...阶段2:将主键页更新锁升级为排他锁,写入新d值
T45472057594066108416Xix_a_bc阶段3:更新ix_a_bc索引页的d副本
T55472057594066173952Xix_b_cd阶段3:更新ix_b_cd索引页的d副本

死锁发生点:SPID 60 在 T2 时刻申请PK的U锁时,发现 SPID 54 已持有ix_a_bc的U锁(T1),于是等待;SPID 54 在 T4 时刻申请ix_a_bc的X锁时,发现 SPID 60 正在等待PK的U锁(T2),而PK的U锁又被 SPID 60 持有 → 形成循环等待。这就是经典的“锁升级时机错位”:SPID 54 释放了ix_a_bc的U锁(T3 后),但 SPID 60 还没来得及申请ix_a_bc的X锁,SPID 54 就抢先申请了。

5. 避坑与常见问题排查:5 条血泪经验,专治 SQL Server 死锁玄学

这个死锁案例之所以“奇怪”,是因为它违反直觉:同样的语句、同样的索引、同样的提示,只因一个字段类型或INCLUDE选项就天壤之别。以下是我在某跨平台系统迁移项目中踩过的坑,每一条都附带真实现象、根因和可立即执行的解决命令。

5.1 现象:死锁日志显示victim是UPDATE,但 Profiler 里找不到对应 SPID 的Lock:Acquired事件

原因:1222日志记录的是死锁发生瞬间的状态,而 Profiler 可能因缓冲区满或过滤条件过严丢失早期事件。更常见的是,你启动 Profiler 的时间晚于死锁发生时间。
解决:

  • 启动 Profiler 后,立刻执行DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR)清空等待统计,再开始监听;
  • 在Column Filters中,SPID过滤改为Not In→1,2,3,4(排除系统 SPID),避免漏掉动态分配的连接;
  • 使用Extended Events替代 Profiler(SQL Server 2012+),创建xml_deadlock_report事件会话,它比 Profiler 更轻量、更可靠。

5.2 现象:SET STATISTICS PROFILE ON显示Index Update操作符只有 1 个,但死锁依然发生

原因:你正在查看的执行计划是SELECT或INSERT的,不是UPDATE的!UPDATE语句的执行计划必须单独捕获,且不能加任何OPTION (RECOMPILE)等提示干扰。
解决:

  • 确保SET STATISTICS PROFILE ON紧贴UPDATE语句前,且中间无空行或注释;
  • 执行后,在 SSMS 结果网格中切换到“执行计划”标签页(不是“消息”或“结果”),该页才是真实执行计划;
  • 若仍看不到,检查数据库兼容级别:SELECT compatibility_level FROM sys.databases WHERE name = 'YourDB',低于 100(SQL Server 2008)可能不支持完整 Profile。

5.3 现象:varchar(max)改成varchar(8000)后死锁消失,但改成varchar(4000)却依然死锁

原因:SQL Server 对varchar(n)的存储策略有临界点。n <= 8000时,数据默认存于数据页内(In-Row);n > 8000或n = MAX时,数据存于 LOB 页(Out-of-Row)。4000虽小于8000,但若表中已有大量d字段接近4000长度,SQL Server 可能因页空间不足而强制将其存入 LOB 页,触发与MAX相同的锁分裂逻辑。
解决:

  • 执行DBCC IND('YourDB', 'tt', -1)查看tt表的页分配情况,重点关注PageType = 3(数据页)和PageType = 10(LOB 页)的比例;
  • 若PageType = 10占比高,说明d字段实际已 Out-of-Row,此时需ALTER TABLE tt REBUILD强制重组页,或直接ALTER COLUMN d VARCHAR(200)彻底规避。

5.4 现象:INCLUDE移除后死锁消失,但业务要求必须INCLUDE某些字段以支撑高频SELECT

原因:INCLUDE是双刃剑。它加速SELECT,却加重UPDATE/INSERT/DELETE的锁负担。当INCLUDE字段是MAX类型时,矛盾被放大。
解决:

  • 方案A(推荐):将MAX字段分离到独立表,主表只存d_id外键,INCLUDE改为d_id(INT);
  • 方案B:用计算列替代INCLUDE,如ALTER TABLE tt ADD d_hash AS HASHBYTES('SHA2_256', d),INCLUDE(d_hash),SELECT时用哈希比对;
  • 方案C(应急):在UPDATE语句中显式HOLDLOCK,强制锁升级顺序:UPDATE tt WITH(ROWLOCK, HOLDLOCK) SET d='cd' WHERE a='...'。

5.5 现象:在 SQL Server 2019 上复现失败,1222日志无死锁记录

原因:SQL Server 2016+ 引入了自适应查询处理(Adaptive Query Processing),其中Batch Mode on Rowstore和Interleaved Execution可能改变UPDATE计划。更重要的是,2019 默认启用Parameter Sniffing优化,若a值选择性差,可能走扫描而非查找,破坏死锁条件。
解决:

  • 关闭自适应功能(临时):ALTER DATABASE SCOPED CONFIGURATION SET DISABLE_BATCH_MODE = ON;;
  • 强制使用老式优化器:UPDATE tt WITH(ROWLOCK) SET d='cd' WHERE a='...' OPTION (QUERYTRACEON 9481)(启用 Legacy CE);
  • 确保a字段有高选择性:UPDATE STATISTICS tt ix_a_bc WITH FULLSCAN更新统计信息。

6. 进阶验证与生产防护:用sys.dm_exec_query_plan自动化检测高风险执行计划

死锁复现是学习手段,生产防护才是终极目标。与其等死锁报警再救火,不如在代码上线前,就用 DMV 扫描出所有潜在的“多索引更新”高危UPDATE语句。下面这套方法,我已在某高校教务系统中落地,覆盖 200+ 张核心表,将死锁发生率从月均 3 次降至 0。

6.1 构建高风险执行计划识别脚本:自动标记Index Update数量 ≥2 的语句

核心思路:从sys.dm_exec_cached_plans中提取所有缓存的UPDATE计划,解析其 XML,统计<RelOp>标签下PhysicalOp="Index Update"的出现次数。超过 1 次即为高风险。

-- 检测当前缓存中所有 UPDATE 语句的 Index Update 操作符数量 WITH xmlplans AS ( SELECT cp.plan_handle, qp.query_plan, cp.usecounts, cp.size_in_bytes, t.text AS sql_text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) t WHERE cp.cacheobjtype = 'Compiled Plan' AND t.text LIKE '%UPDATE%[ ]%' -- 粗略匹配 UPDATE 语句 AND qp.query_plan IS NOT NULL ), update_ops AS ( SELECT plan_handle, usecounts, size_in_bytes, sql_text, query_plan.value('count(//RelOp[@PhysicalOp="Index Update"])', 'int') AS index_update_count FROM xmlplans ) SELECT usecounts AS execution_count, size_in_bytes / 1024 AS plan_size_kb, LEFT(sql_text, 100) AS truncated_sql, index_update_count, CASE WHEN index_update_count >= 2 THEN 'HIGH RISK: Multiple Index Updates' ELSE 'LOW RISK' END AS risk_level FROM update_ops WHERE index_update_count >= 2 ORDER BY execution_count DESC;

参数说明:query_plan.value('count(//RelOp[@PhysicalOp="Index Update"])', 'int')是 XPath 表达式,精准统计 XML 中Index Update操作符数量。LEFT(sql_text, 100)截取 SQL 片段便于快速定位。此脚本可在任意时间运行,无需停机。

6.2 生产环境防护三板斧:从设计、监控到应急

第一板斧:设计规范前置
在数据库设计文档中强制加入“索引INCLUDE字段禁令”:

  • 禁止对VARCHAR(MAX),NVARCHAR(MAX),VARBINARY(MAX)字段使用INCLUDE;
  • INCLUDE字段总长度(SUM(LEN))不得超过 900 字节(SQL Server 索引键长度限制);
  • 新增INCLUDE前,必须用SET STATISTICS PROFILE ON验证UPDATE计划。

第二板斧:监控告警自动化
在 SQL Server Agent 中创建作业,每小时执行一次死锁检测脚本,并将结果写入监控表:

-- 创建死锁监控表 CREATE TABLE dbo.DeadlockMonitor ( id INT IDENTITY PRIMARY KEY, capture_time DATETIME2 DEFAULT GETDATE(), victim_spid INT, wait_resource NVARCHAR(256), lock_mode CHAR(1), sql_text NVARCHAR(500) ); -- 插入最近一次死锁信息(从 errorlog 解析,此处略去解析逻辑) INSERT INTO dbo.DeadlockMonitor (victim_spid, wait_resource, lock_mode, sql_text) SELECT CAST(x.value('(//deadlock/victim)[1]', 'varchar(10)') AS INT), x.value('(//resource-list/keylock/@waitresource)[1]', 'nvarchar(256)'), x.value('(//resource-list/keylock/@mode)[1]', 'char(1)'), x.value('(//executionStack/frame/@stmtstart)[1]', 'nvarchar(500)') FROM ( SELECT CAST(logtext AS XML) AS deadlock_xml FROM sys.fn_trace_gettable( (SELECT path FROM sys.traces WHERE is_default = 1), DEFAULT ) WHERE EventClass = 148 -- Deadlock Graph ) AS t CROSS APPLY deadlock_xml.nodes('/deadlock') AS T2(x);

第三板斧:应急响应 SOP
当告警触发,立即执行:

  1. DBCC INPUTBUFFER(@victim_spid)查看受害者最后执行的 SQL;
  2. SELECT * FROM sys.dm_exec_requests WHERE session_id = @victim_spid查看阻塞链;
  3. KILL @blocking_spid终止源头(谨慎!先确认非关键业务);
  4. ALTER INDEX ALL ON tt REBUILD WITH (ONLINE = ON)重建索引,释放碎片化锁资源。

从那以后我每次上线新索引,都强制走一遍SET STATISTICS PROFILE ON+UPDATE测试,哪怕只是加一个INCLUDE。不是 paranoid,而是这个死锁太狡猾——它不报错,不超时,就安静地把两个事务互相锁死,直到LOCK_TIMEOUT到期。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进