死锁是多个事务因循环等待对方持有的锁而永久阻塞,必须外力干预(如回滚)才能解除;阻塞则是单个事务等待资源释放,具有可恢复性。二者本质区别在于是否形成相互等待闭环。

死锁不是 JOIN 语法本身的问题,而是多个事务对同一组资源按不同顺序加锁导致的循环等待;诊断核心是定位“谁在等谁、等什么、为什么等”。
怎么快速区分死锁和普通阻塞
别一看到查询卡住就以为是死锁——先看现象再查原因。
- 错误日志里出现
Error 1205或Deadlock victim→ 确认为死锁,SQL Server 已主动终止一个会话 -
sys.dm_exec_requests中blocking_session_id > 0且wait_type是LCK_M_U、LCK_M_X等 → 大概率是阻塞,不是死锁 -
SELECT * FROM sys.sysprocesses WHERE blocked 0返回结果中,若某行blocked = spid(即自己阻塞自己)→ 很可能是 I/O 等待,不是死锁
如何从 system_health 提取真实死锁图
这是生产环境最可靠、零配置的方式。SQL Server 2012+ 默认启用,不用开 profiler,也不影响性能。
- 运行以下查询提取最近的死锁记录:
SELECT XEventData.XEvent.value('(@timestamp)[1]','datetime') AS DeadlockDateTime, XEventData.XEvent.query('(data/value/deadlock)[1]') AS DeadlockGraph FROM (SELECT CAST(target_data AS XML) AS TargetData FROM sys.dm_xe_session_targets st JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address WHERE s.name = 'system_health' AND st.target_name = 'ring_buffer') AS Data CROSS APPLY TargetData.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(XEvent) ORDER BY DeadlockDateTime DESC; - 在 SSMS 中点击结果里的
DeadlockGraph列,会自动打开图形化视图:椭圆是进程(SPID),矩形是资源(KEY、PAGE、OBJECT),箭头表示“持有”和“等待”关系 - 重点看每个进程的
inputbuf内容——它告诉你实际执行的是哪条 SQL;很多 JOIN 死锁其实发生在存储过程或 ORM 自动生成的事务里,不是裸写 JOIN 的问题
为什么 UPDATE ... FROM t1 LEFT JOIN t2 特别容易死锁
SQL Server 在这种写法下不保证两表加锁顺序一致。并发事务若一个先锁 t1 再等 t2,另一个先锁 t2 再等 t1,就构成循环等待。
- LEFT JOIN 在 UPDATE 场景中,驱动表选择受统计信息、索引覆盖、
WHERE条件位置影响,执行计划可能动态变化 - 没有索引的 JOIN 列会导致全表扫描 → 锁范围扩大,加剧冲突概率
- 避免在 JOIN 条件里用函数或类型转换,如
ON CAST(t1.id AS VARCHAR) = t2.ref_id,这会让索引完全失效 - 如果必须用 LEFT JOIN 更新,优先确保驱动表的
WHERE字段 + 被驱动表的ON字段都有高效索引,且尽量用 INNER JOIN 替代(语义允许时)
死锁图里最常被忽略的关键细节
图形界面看着直观,但真正定位根因要盯住三处:
-
victim-list里标出的牺牲 SPID 对应的inputbuf—— 它未必是问题语句,可能是调用它的存储过程第一行 - 每个
process节点的executionStack显示调用栈深度,注意是否来自sp_executesql或 ORM 框架封装层 -
resource-list中相同resource_associated_entity_id出现多次,且锁模式混杂(比如一个持X,另一个等U)→ 说明是同一行/页在不同索引路径上被交叉加锁,这类死锁最难复现也最难改










