SQL Server触发器未走索引导致全表扫描,在RR隔离级别下会加Next-Key Lock,等效锁表;根本原因包括缺失索引、函数包装、联合索引未满足最左前缀、字符集不匹配及嵌套调用放大锁范围。

触发器里没走索引,UPDATE 就会扫全表
触发器本身不加表锁,但它执行的 SQL 会。最常见的情况是:触发器内有一句 UPDATE inventory SET stock = stock - 1 WHERE sku = @sku,但 sku 字段没有索引。InnoDB 只能全表扫描,而 RR 隔离级别下会为**所有扫描过的聚集索引记录 + 间隙**都加上 Next-Key Lock——效果等同于锁住整张表。
验证方式很简单:EXPLAIN 触发器里的那条 UPDATE 语句(可先用 sp_helptext 或 sys.dm_exec_sql_text 提取触发器定义),看 type 是 ALL 还是 ref/eq_ref;再查 SHOW CREATE TABLE inventory 确认 sku 是否在某个索引的最左列。
- 即使有索引,如果用了函数包装(如
WHERE UPPER(sku) = UPPER(@sku)),也会失效 - 联合索引
(warehouse_id, sku)对WHERE sku = ?无效,必须满足最左前缀 - 字符集不一致(比如字段是
utf8mb4_0900_as_cs,参数却是utf8mb4_general_ci)也会导致索引跳过
触发器嵌套调用存储过程,锁范围被悄悄放大
一个看似安全的触发器,可能通过 EXEC update_stock_log @sku, @qty 调用另一个过程,而该过程内部又做了 INSERT INTO stock_log_history SELECT * FROM inserted —— 这个 SELECT 如果没加 WITH (NOLOCK) 或明确限定主键,就可能触发对 stock_log_history 的全表扫描加锁,再反过来阻塞主表。
关键点在于:触发器和它调用的所有代码,共享同一个事务上下文,锁不会自动释放,直到整个事务提交或回滚。你看到的“锁表”,往往是这一长串隐式 DML 累积的结果。
- 用
SELECT * FROM sys.dm_exec_trigger_stats只能看到执行次数,看不出锁行为 - 真正要查的是
sys.dm_tran_locks,过滤resource_type = 'OBJECT'或resource_description包含大范围 KEY 值 - 若发现
request_mode是SCH_M(架构修改锁),说明有 DDL 操作混进来了,比如触发器里执行了ALTER TABLE
SQL Server 中触发器没有独立事务,@@TRANCOUNT 永远不为 0
很多开发者误以为可以在触发器里用 IF @@TRANCOUNT = 0 BEGIN BEGIN TRAN ... 来“兜底”,这是危险的。触发器永远运行在外部事务中,@@TRANCOUNT 至少为 1。一旦触发器内抛异常且没 TRY/CATCH,整个外部事务直接回滚;如果加了 SAVE TRAN 又没正确 ROLLBACK TO,还可能留下未清理的锁。
更隐蔽的问题是:触发器里调用的 UDF(用户定义函数)如果包含 SELECT,SQL Server 会为每次调用都申请共享锁,大量行级查询叠加起来,也可能撑满锁内存、触发升级。
- 避免在触发器中调用含数据访问的标量函数(scalar UDF)
- 改用内联表值函数(ITVF)替代,它会被展开为 JOIN,不单独加锁
- 用
SET XACT_ABORT ON开头,确保异常时事务状态干净
死锁图里看不到触发器名?得手动反查 executionStack
SQL Server 死锁 XML 中的 executionStack 通常只显示最外层语句(比如 UPDATE orders ...),不会直接写出 trg_after_update_order。但每个 frame 都有 handle 和 line,你可以用它去查真实文本:
SELECT TEXT FROM sys.dm_exec_sql_text(0x01000A...)
结果里往往藏着触发器名或被调用的存储过程名。这才是确认“是不是触发器惹的祸”的唯一可靠路径。
别依赖应用日志里那句 Deadlock encountered ... 1205,它不告诉你锁在哪张表、哪几行、谁在等谁——那些信息全压在死锁图的 XML 里,而且藏得深。

















