innodb从不主动升级行锁为表锁,所谓“升级”实为where条件未走索引导致全表扫描、逐行加锁(含间隙锁)的误判;真表锁仅来自lock tables、ddl或myisam引擎。

行级锁升级为表级锁,本质是数据库在资源压力或扫描范围过大时的自动保护行为,不是SQL写错了,而是事务控制、索引设计或批量逻辑没对齐引擎机制——直接加 WITH (NOLOCK) 或调隔离级别,解决不了根本问题。
WHERE 条件必须走索引,且不能让索引失效
没索引或索引失效,InnoDB 和 SQL Server 都会全表扫描,行锁数量暴增,触发锁升级阈值(SQL Server 默认 5000 行,MySQL 在间隙锁累积多时也等效表锁)。
- 避免函数包裹字段:
WHERE DATE(created_at) = '2024-01-01'→ 改成WHERE created_at >= '2024-01-01' AND created_at - 避免隐式类型转换:
WHERE user_id = '123'(user_id 是 INT)→ 改成WHERE user_id = 123 - 复合索引要注意最左前缀:若用
WHERE status = 'pending' AND updated_at > '2026-01-01',索引应建为(status, updated_at),而非反过来
SQL Server:用 UPDATE TOP(n) + 显式事务 + 范围推进
UPDATE TOP(n) 本身不保证安全,关键在事务边界和推进逻辑。裸写 UPDATE TOP(5000) ... WHERE status = 'pending' 仍可能每批都扫全表。
- 必须包在
BEGIN TRANSACTION/COMMIT中,否则每条都是独立小事务,开销更大 - 不能依赖
ORDER BY(UPDATE 语法不支持),改用带索引的范围条件推进,例如:WHERE status = 'pending' AND id > @last_id - 每次更新后用
SELECT @last_id = MAX(id) FROM ...获取下一批起点,确保不漏不重 - 终止条件必须是
IF @@ROWCOUNT = 0 BREAK,别用预估总行数硬循环
MySQL:LIMIT 必须配合索引 WHERE,且禁用子查询 IN
MySQL 5.7+ 允许 UPDATE ... LIMIT,但仅当 WHERE 走索引时才真正只锁目标行;否则仍先加锁再裁剪。
- 正确写法:
UPDATE orders SET status = 'shipped' WHERE status = 'pending' AND id > 10000 ORDER BY id LIMIT 2000(status和id需联合索引) - 绝对避开:
UPDATE t1 SET x=1 WHERE id IN (SELECT id FROM t2 WHERE ...)—— 子查询结果未落地前,InnoDB 可能对全表加意向锁 - 替代方案:先
CREATE TEMPORARY TABLE tmp_ids AS SELECT id FROM ...,再UPDATE t1 JOIN tmp_ids ON t1.id = tmp_ids.id SET ...
查锁源比改 SQL 更快定位真因
UPDATE 卡住,90% 不是这条语句的问题,而是被别的事务堵住了。不查锁源就优化 SQL,纯属蒙眼调参。
- SQL Server:立刻跑
SELECT blocking_session_id, wait_type, wait_resource FROM sys.dm_exec_requests WHERE blocking_session_id > 0 - MySQL:查
SELECT * FROM information_schema.INNODB_TRX ORDER BY TRX_STARTED DESC,重点关注TRX_STATE = 'LOCK WAIT'的记录 - 看到长事务(
TRX_STARTED几分钟前)、或TRX_QUERY是未提交的SELECT ... FOR UPDATE,直接 kill 或联系业务方收尾
锁升级的临界点很隐蔽:比如 SQL Server 里单语句锁超 5000 行、或锁内存占缓冲池 24% 以上就会触发。这些数值没法靠肉眼预判,得靠实时视图确认——别等它发生,要盯着它怎么发生的。










