锁表主因是事务失控、索引缺失或批量未拆分;sql server宜用update top(n)分批更新,需配begin transaction/commit及order by,批次1000–5000;mysql 5.7+可用update...limit,where字段必须有索引且避免函数索引失效。

锁表不是SQL语句本身的问题,而是事务边界失控、索引缺失或批量逻辑没拆分导致的——直接执行 UPDATE 时卡住,大概率是行锁升级成了表锁,或者被另一个长事务堵住了。
SQL Server 用 UPDATE TOP(n) 分批更新最稳妥
这是兼容性最好、落地最快的方案,不需要改应用层,也不依赖主键连续性。关键点不在“怎么写”,而在“怎么控制节奏”:
- 必须显式用
BEGIN TRANSACTION和COMMIT,否则每条UPDATE TOP都是独立事务,反而增加开销 -
TOP(n)后一定要跟ORDER BY(比如ORDER BY id),不然同一批可能重复更新或漏更 - 批次大小设为
1000–5000:小于 1000 容易因提交太频繁拖慢整体;大于 5000 可能触发锁升级为页锁甚至表锁 - 循环终止条件用
IF @@ROWCOUNT = 0 BREAK,别靠预估总行数硬算——WHERE 条件可能随更新动态变化
MySQL 用 LIMIT 控制单次影响行数
MySQL 5.7+ 支持在 UPDATE 里直接加 LIMIT,比子查询安全得多:
-
UPDATE orders SET status = 'shipped' WHERE status = 'pending' ORDER BY id LIMIT 2000—— 这样只锁实际更新的行,不会扫描全表 - 确保
WHERE字段有索引,否则LIMIT也救不了:全表扫描下,InnoDB 会先对所有匹配行加记录锁+间隙锁,LIMIT只是最后不改而已 - 避免
WHERE DATE(created_at) = '2024-01-01'这类写法,函数会让索引失效;改成created_at >= '2024-01-01' AND created_at
别碰 WHERE id IN (SELECT ...) 批量更新
看着简洁,但 SQL Server 和 MySQL 都容易在这里翻车:
- SQL Server 会把子查询结果缓存在 tempdb 或内存里,几十万 ID 就可能吃光内存,还引发锁范围扩大——它会对子查询扫描的所有行加意向锁,哪怕最终没更新
- MySQL 在 5.7 以前对这类写法常走临时表+全表扫描,锁整张表;即使加了索引,优化器也可能放弃使用
- 真正可控的做法是先查出 ID 列表存进表变量(SQL Server)或临时表(MySQL),再用
JOIN更新,明确控制每次操作的行集合
查锁源比优化 SQL 更优先
UPDATE 卡住时,90% 的问题不在那条语句本身,而在谁在前面占着锁不放:
- SQL Server:跑
SELECT blocking_session_id, wait_type, wait_resource FROM sys.dm_exec_requests WHERE blocking_session_id 0,一眼看出谁在堵你 - MySQL:查
information_schema.INNODB_TRX,按TRX_STARTED排序,重点关注状态为LOCK WAIT的事务,再顺藤摸瓜找阻塞源头 - 别急着
KILL,先看那个长事务在干啥——可能是应用层忘了commit,也可能是报表查询用了WITH (TABLOCK, HOLDLOCK)持锁几十秒
锁的本质是资源竞争,不是语法缺陷。真正难处理的从来不是“怎么写 UPDATE”,而是“怎么让事务不跨 IO、不混非 DB 操作、不依赖模糊的隐式行为”。一旦事务边界模糊,再细的索引、再小的批次也兜不住。











