sql server死锁本质是事务间循环等待资源,修复关键在于统一加锁顺序、减少锁持有时间、确保索引命中,并通过xml_deadlock_report和系统视图精准定位nested loops等操作中的争用行与锁序。

查死锁图时重点关注Nested Loops的锁获取顺序
SQL Server不会因为用了Nested Loops就自动死锁,但它的执行逻辑(外层驱动、内层逐行探测)会让锁申请顺序高度依赖数据访问路径。一旦两个并发事务的外层表扫描顺序不同,或内层索引查找范围重叠,就容易形成“先锁A再等B”和“先锁B再等A”的循环等待。
真正要盯的是xml_deadlock_report里executionStack节点下的每个frame:看哪个Nested Loops操作在申请X锁时被阻塞,它的inputTree里外层表是哪张、内层表是哪张、JOIN条件字段是否都有索引。特别注意waitresource值是否同时出现在两个victim的堆栈里——比如都卡在KEY: 7:72057594038321152 (b5a7e9c9e3d9),说明争用同一行版本。
- 用
SELECT * FROM sys.dm_exec_requests WHERE blocking_session_id > 0快速筛出阻塞链,再对session_id查sys.dm_exec_sql_text确认是否含Nested Loops计划 - 别只信图形执行计划里的“警告图标”,
Nested Loops没警告也可能死锁——它可能因内层未走索引导致全表扫描,进而升级为页锁甚至表锁 -
WAITFOR DELAY '00:00:05'这类人为延迟在复现时有用,但生产环境死锁往往发生在毫秒级竞争点,得靠system_health的环形缓冲区抓实时快照
为什么LEFT JOIN + Nested Loops比INNER JOIN更容易死锁
LEFT JOIN在Nested Loops下会强制扫描全部外层行,即使内层无匹配也要保留外层结果。这导致外层表锁持有时间更长,且内层表的探测行为不可预测——比如ON t1.id = t2.ref_id,若t2.ref_id没索引,SQL Server可能对t2全表加S锁再过滤,而另一个事务正对t2某行做UPDATE,就卡住了。
-
LEFT JOIN的Nested Loops无法像Hash Match那样把内层预建哈希表,锁是边找边拿,没有统一释放时机 - 当外层表有大量重复
ref_id值时,内层表同一行可能被反复加S锁,加剧与写操作冲突 - 用
SELECT * FROM sys.dm_tran_locks WHERE resource_type = 'KEY' AND request_mode IN ('U', 'X')查具体哪几行被争用,比看表名更有价值
用UPDLOCK + HOLDLOCK强制统一加锁顺序
对Nested Loops涉及的关键表,在事务开头显式加锁,能打破随机性。比如外层是orders、内层是customers,就在BEGIN TRAN后立刻执行:SELECT TOP 1 id FROM orders WITH (UPDLOCK, HOLDLOCK) WHERE customer_id = @cid。这样所有事务都按orders → customers顺序拿锁,避免反向争用。
-
UPDLOCK防止其他事务对该行加X锁,HOLDLOCK等价于SERIALIZABLE,锁住范围直到事务结束 - 不能只锁内层表——外层表才是
Nested Loops的驱动源,它决定扫描起点和行数 - 慎用
NOLOCK绕过死锁:读未提交可能返回脏数据,且不解决写-写冲突,反而掩盖真实瓶颈
替换Nested Loops为Merge Join或子查询的实操门槛
Merge Join要求两张表都按JOIN字段排序,如果已有orders(customer_id)和customers(id)聚集索引,SQL Server大概率自动选它;否则建非聚集索引+ORDER BY提示成本高,不如直接改写为子查询。
- 子查询替代
LEFT JOIN更新:UPDATE o SET status = 1 FROM orders o WHERE o.customer_id IN (SELECT id FROM customers WITH (NOLOCK) WHERE region = 'CN'),把内层结果固化为值列表,消除逐行探测 -
Merge Join在数据量大且有序时性能更好,但若JOIN字段存在大量NULL,它可能退化为Hash Match,锁行为又变复杂 - 强制指定连接算法(如
OPTION (MERGE JOIN))仅用于验证假设,上线前必须压测,避免执行计划僵化导致新问题
嵌套循环本身不危险,危险的是它把锁的时序暴露给了数据分布和索引质量。最常被忽略的点是:你看到的Nested Loops执行计划,可能根本不是死锁发生时的真实计划——缓存计划被复用,而实际运行时数据倾斜让内层扫描膨胀了十倍,锁升级随之发生。










