根本原因是事务持有锁时间过长,而非sql执行慢;若where未走索引,mysql在repeatable read下会全表扫描并加间隙锁与记录锁,postgresql和sql server也因锁升级或排他锁导致阻塞。

为什么UPDATE大表会锁太久?
根本原因不是SQL本身慢,而是事务持有行锁或表锁的时间太长。MySQL默认在REPEATABLE READ隔离级别下,UPDATE语句会为所有扫描到的行加间隙锁(gap lock)和记录锁(record lock),哪怕只改1行,如果WHERE条件没走索引,就会全表扫描+全表加锁。PostgreSQL虽用MVCC避免读阻塞写,但UPDATE仍需获取行级排他锁,且触发TOAST和VACUUM压力;SQL Server在默认READ COMMITTED下也会因锁升级(从行锁→页锁→表锁)导致意外阻塞。
WHERE条件必须走索引,否则锁表风险极高
这是最常被忽略的前提。没索引的WHERE会让优化器选择全表扫描,锁住所有扫描过的行——哪怕最终只更新1条。检查执行计划:EXPLAIN(MySQL/PostgreSQL)或SET STATISTICS IO ON(SQL Server)看是否出现type=all、Seq Scan或Table Scan。
- 确保WHERE字段有单列索引,或复合索引的最左前缀匹配(如
WHERE status='pending' AND created_at ,索引应为<code>(status, created_at)) - 避免在索引字段上用函数或表达式:
WHERE DATE(created_at) = '2024-01-01'会失效,改成WHERE created_at >= '2024-01-01' AND created_at - 字符串比较注意隐式类型转换:
WHERE user_id = 123(user_id是VARCHAR)会导致索引失效,应写成WHERE user_id = '123'
分批更新比单次大事务更安全
一次性更新百万行,事务日志暴涨、回滚段吃紧、锁持有时间不可控。分批的核心是“每次只锁一小块”,靠主键或索引字段切片,避免重复和遗漏。
MySQL示例(按主键分片):
SELECT MIN(id), MAX(id) FROM orders WHERE status = 'pending'; -- 先查范围 -- 然后循环执行: UPDATE orders SET status = 'processed' WHERE status = 'pending' AND id BETWEEN 100001 AND 110000 LIMIT 1000;
PostgreSQL建议用WHERE ctid IN (SELECT ctid FROM ... LIMIT 1000)或基于游标分页;SQL Server可用TOP (1000) + WHERE id > @last_id。
- 每批控制在1000–5000行,具体看单行数据大小和事务日志空间
- 批次间加
SLEEP(0.1)(MySQL)或pg_sleep(0.05)(PG),缓解主从延迟和连接堆积 - 务必用
ORDER BY id+LIMIT保证顺序,否则可能漏更新或重复更新
避开高峰期+监控锁等待,比优化SQL更关键
再好的分批逻辑,如果在业务高峰跑,照样拖垮应用。锁等待不是性能问题,是资源争用问题——它直接影响其他查询的响应。
- 用
SHOW PROCESSLIST(MySQL)、pg_stat_activity(PG)、sys.dm_exec_requests(SQL Server)实时查State为Locked或WAITING的会话 - 重点盯
blocking_session_id(SQL Server)或pg_blocking_pids()(PG),快速定位谁卡住了别人 - 生产环境严禁在上午9:30–11:30、下午2:00–4:00这类时段跑大更新,哪怕加了索引、分了批
真正难的不是写出能跑的SQL,而是判断“现在能不能跑”——这需要看监控图表里的锁等待数、事务提交延迟、从库延迟值,而不是只盯着执行时间。











