执行update/delete时where未走索引会导致全表扫描,进而引发表级锁;应通过执行计划确认索引使用、避免函数操作、遵守最左前缀原则、更新统计信息并拆分大事务。

UPDATE/DELETE 用 WHERE 却锁整张表?先看执行计划
不是语句写错了,是优化器没走索引,被迫全表扫描——锁就跟着从行级升级成表级。SQL Server 和 MySQL 都会这样干,只是触发条件略有不同。
实操建议:
- 在存储过程里跑
EXPLAIN(MySQL)或看实际执行计划(SQL Server),确认type是ref/range,不是ALL - WHERE 条件别对字段做函数操作,比如
WHERE YEAR(created_at) = 2024会让索引失效 - 复合索引要守最左前缀,
INDEX(user_id, status)不能只靠status = 'pending'去查 - SQL Server 上定期跑
sp_updatestats,统计信息过期会导致行数误估,提前触发锁升级
大事务批量删改必须拆批,别信“一次搞定”
单条 DELETE FROM LogMessages WHERE LogDate 很可能锁超 5000 行,SQL Server 自动升级为 TAB 锁,把整个表堵死。
正确做法是主动控制锁数量:
- 用
DELETE TOP(1000)+WHILE循环,每批最多 1000 行,锁不累积 - 每批后加
WAITFOR DELAY '00:00:00.01'(可选),给其他会话喘息机会 - 避免在循环里嵌套事务——每个
DELETE自带隐式事务就够了,显式BEGIN TRAN反而拉长锁持有时间
sp_getapplock 怎么调用才不残留?四条铁律
sp_getapplock 看似轻量,但默认绑定事务,连接池一复用,锁就挂着不放,新请求静默卡住。
必须同时满足:
- 锁操作必须包裹在
BEGIN TRANSACTION内,且存储过程结束前不能提前COMMIT或ROLLBACK -
@Resource参数带业务上下文,比如'order_pay_' + CAST(@order_id AS VARCHAR(20)),别用硬编码'mylock' -
@LockTimeout设具体毫秒值,例如5000;-1(无限等待)在生产环境等于埋雷 - 必须检查返回值:
IF @result 就失败(<code>-1=超时,-2=死锁牺牲品),不能只看@@ERROR
编译锁阻塞怎么识别?看 waitresource 里的 [[COMPILE]]
多个会话并发执行同一存储过程,却卡在 LCK_M_X、waitresource 显示 OBJECT: dbid:object_id [[COMPILE]],这就是编译锁争用。
根本原因是 SQL Server 不确定该用哪个缓存计划,被迫串行编译:
- 确保调用时用完全限定名,比如
EXEC dbo.usp_ProcessOrder,别只写EXEC usp_ProcessOrder - 让执行用户是存储过程所有者,或至少有
EXECUTE权限且不跨 schema 查找 - 高频过程考虑用
WITH RECOMPILE(慎用)或预热:首次调用后立刻DBCC FREEPROCCACHE清掉旧计划再重跑一次
sp_getapplock 的事务绑定——它们不报错,但会在后台悄悄拖垮并发。










