DELETE JOIN易死锁,因并发事务按不同顺序加锁形成循环等待;必须为JOIN字段和WHERE条件列添加覆盖索引,并推荐改用子查询或分步预锁控制执行路径。

DELETE JOIN 本身不直接引发死锁,但它是高危操作模式——只要涉及多表、无索引、事务内混用读写顺序不一致,就极易触发循环等待。
为什么 DELETE FROM t1 INNER JOIN t2 容易死锁?
SQL Server 执行 DELETE FROM t1 INNER JOIN t2 时,实际会按执行计划决定先锁哪张表。两个并发事务若一个先锁 t1 再等 t2,另一个先锁 t2 再等 t1,立刻形成死锁链。
- 现象:错误日志出现
1205(死锁牺牲品),sys.dm_exec_requests中wait_type为LCK_M_U或LCK_M_X,且blocking_session_id非零并互相指向 - 根本原因不是 JOIN 语法,而是锁获取顺序不可控 + 缺少覆盖索引导致扫描升级为页锁/表锁
- 特别危险场景:
DELETE FROM orders o INNER JOIN customers c ON o.customer_id = c.id WHERE c.status = 'inactive'—— 若customers(status)无索引,就会全表扫customers,顺带锁住大量无关行
必须加的索引和 WHERE 条件约束
没有索引的 JOIN 条件或过滤字段,等于给死锁发邀请函。
-
JOIN字段两边都必须有索引:例如orders(customer_id)和customers(id);缺一不可 -
WHERE过滤字段必须走索引查找,不能是LIKE '%abc'或函数包裹(如YEAR(created_at) = 2026) - 避免在
DELETE ... JOIN中使用OR、IN (subquery)等易导致索引失效的写法 - 用
SET STATISTICS XML ON看执行计划,确认是否走了Index Seek;出现Clustered Index Scan或Table Scan就得立刻优化
比 DELETE JOIN 更安全的替代写法
强制控制执行路径,绕开 SQL Server 自动选表顺序的不确定性。
- 改用子查询(最推荐):
DELETE FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE status = 'inactive')—— 先查后删,锁只集中在orders匹配行上 - 分两步手动控制顺序:先
SELECT TOP 1 id FROM customers WITH (UPDLOCK, HOLDLOCK) WHERE status = 'inactive'预锁主表关键行,再执行DELETE,确保所有事务按同一顺序加锁 - 对超大批量删除,拆成小批量(如每次 5000 行)+ 加
WAITFOR DELAY '00:00:00.01'降低争用,避免单次锁太久
READ COMMITTED SNAPSHOT 能否关掉这个死锁?
能缓解,但不能根治——它只消除读写冲突,不解决写-写冲突。
- 开启后,
SELECT不再申请共享锁,所以不会和DELETE互等;但两个并发DELETE JOIN仍可能因同时更新同一行而死锁 - 启用命令:
ALTER DATABASE YourDB SET READ_COMMITTED_SNAPSHOT ON,注意该操作需数据库无活动连接 - 副作用明显:
tempdb的version_store增长快,磁盘 IO 上升;若已有大事务未提交,开启会卡住 - 真正治本的是:把
DELETE JOIN拆解 + 加索引 + 控制事务粒度,而不是依赖隔离级别“兜底”
最容易被忽略的是:开发阶段用单线程测试看不出问题,压测时才暴露;而死锁日志里的 xml_deadlock_report 往往被当成“偶发”跳过——其实每条报告都对应着可复现的执行路径和缺失的索引。










