JOIN在SQL Server中更易死锁,因其加锁非原子且顺序不固定,驱动表与被驱动表加锁顺序受统计信息、参数嗅探或索引缺失影响,易形成循环等待;单表UPDATE锁序固定,不易死锁。

为什么JOIN在SQL Server里更容易触发死锁
因为SQL Server对JOIN执行时的加锁不是原子、线性的——它会根据执行计划,对驱动表和被驱动表分别加锁,且顺序可能因统计信息、参数嗅探或索引缺失而动态变化。两个事务若以不同顺序扫描同一组orders和users行(比如事务A先锁orders.id=100再锁users.id=5,事务B反过来),就立刻构成循环等待。单表UPDATE则锁序固定(按WHERE条件扫描顺序),天然不易死锁。
检查执行计划里有没有全表扫描(ALL/index)
死锁高频源头往往藏在看似正常的JOIN语句里:一旦ON列或WHERE列没走索引,SQL Server就会回退到嵌套循环的全表扫描模式,导致对被驱动表大量随机加锁。用SET STATISTICS XML ON跑一遍,重点看<RelOp>节点里的PhysicalOp="Clustered Index Scan"或EstimateRows远超实际匹配数。
- 驱动表的
WHERE字段必须有索引(如created_at);更优是建联合索引(如(created_at, user_id)),让JOIN列也能复用索引下推 - 被驱动表的
ON字段(如users.id)主键默认有索引,但若WHERE里还有state = 'active',就必须补state索引,否则全表扫 - 禁止在
ON或WHERE里用函数,如ON CAST(o.user_id AS VARCHAR) = u.uid——索引直接失效
用sys.dm_exec_requests + sys.dm_os_waiting_tasks定位实时阻塞链
报错Deadlock encountered ... try restarting transaction只是结果,真正要抓的是“谁正在等什么资源”。别只查sys.dm_tran_locks,它只告诉你锁了啥,不告诉你谁在等。
执行这段查询(需VIEW SERVER STATE权限):
SELECT r.session_id AS 被阻塞者, r.blocking_session_id AS 阻塞者, t.text AS 被阻塞SQL, b.text AS 阻塞SQL, wt.wait_type, wt.wait_duration_ms FROM sys.dm_exec_requests r JOIN sys.dm_os_waiting_tasks wt ON r.session_id = wt.session_id JOIN sys.dm_exec_requests br ON wt.blocking_session_id = br.session_id CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t CROSS APPLY sys.dm_exec_sql_text(br.sql_handle) b WHERE r.blocking_session_id <> 0 AND wt.wait_type LIKE 'LCK%';
重点关注LCK_M_X(排他锁等待)和LCK_M_U(更新锁等待)——它们大概率对应UPDATE/DELETE JOIN语句。如果发现多个会话反复出现在“阻塞者”位置,说明那个会话持有的锁没及时释放,极可能是长事务或未提交的批量操作。
system_health扩展事件里捞原始死锁图
system_health是SQL Server自带的零开销监控,死锁发生后30秒内就能捕获完整XML死锁图。手动查比Profiler轻量,比日志准确。
执行以下命令提取最近一次死锁:
SELECT
XEvent.value('(/event/@timestamp)[1]', 'datetime2') AS DeadlockTime,
XEvent.query('/event/data/value/deadlock') AS DeadlockGraph
FROM (
SELECT CAST(event_data AS XML) AS XEvent
FROM sys.fn_xe_file_target_read_file(
'system_health_*.xel', NULL, NULL, NULL
)
WHERE object_name = 'xml_deadlock_report'
) AS src;
返回的DeadlockGraph里,<process-list>会明确标出两个事务的spid、SQL文本、等待对象(如KEY: 7:72057594038321152 (8194443284a0)),<resource-list>则显示被争抢的具体行锁。注意比对两个process的inputbuf内容——90%的JOIN死锁,问题都出在两个事务的JOIN顺序或WHERE条件覆盖范围不一致上。
最易被忽略的一点:死锁图里executionStack显示的SQL,未必是你代码里写的原句——它可能是存储过程里拼接的动态SQL,或是ORM生成的带参数化占位符的语句。务必结合sys.dm_exec_query_stats查query_hash,确认是不是同一类查询在反复触发。

















