锁范围由执行计划实际扫描的索引范围决定,而非WHERE条件;无索引、索引失效或扫描行数过多会导致行锁退化为表锁或大面积间隙锁。

直接结论:锁范围不是由UPDATE语句写的WHERE条件决定的,而是由执行计划实际扫描的索引范围决定的;没索引、索引失效、或扫描行数过多,都可能让行锁退化成表锁或大面积间隙锁。
WHERE条件没走索引 → 锁全表
哪怕你写的是UPDATE orders SET status = 'done' WHERE order_no = 'NO123',只要order_no没建索引,InnoDB就会全表扫描,对所有扫描到的行加记录锁——效果等同于锁表。常见诱因包括:
-
WHERE DATE(created_at) = '2024-01-01'→ 函数导致索引失效,应改写为created_at >= '2024-01-01' AND created_at - 联合索引
(a, b),但查询写成WHERE b = 1→ 不满足最左前缀,退化为全索引扫描 - 隐式类型转换:
WHERE user_id = '123'(字段是INT)→ 触发全表扫描
验证方式:跑EXPLAIN FORMAT=TREE,看type是不是ALL或index,rows是否远超预期匹配数。
批量UPDATE必须用主键/有序索引分段
UPDATE ... LIMIT 1000不解决锁问题——它只限制修改行数,WHERE仍可能扫全表。真正释放锁的方式是让每次事务短、扫描范围小、边界确定。
- 必须基于有索引的单调字段切片,如
id或created_at,写成WHERE id > 10000 AND id - 每批单独事务,执行完立刻
COMMIT,锁立刻释放 - 批大小建议1000–5000行:太小事务开销高;太大单次锁持有时间仍长
- 别用
OFFSET分页式分批,数据变动会导致漏改或重复
示例安全写法:UPDATE users SET status = 'archived' WHERE id > 10000 AND id ,下一批改id > 15000 AND id 。
隔离级别影响锁类型与范围
同一句UPDATE在RC和RR下锁行为完全不同:
- 读已提交(RC):只锁命中的行,无间隙锁,锁持续到事务结束
- 可重复读(RR):除记录锁外,还会加间隙锁(Gap Lock),锁住索引区间,比如
WHERE id BETWEEN 10 AND 20可能锁住(5, 10)、(20, 25)等空隙 - 若必须用RR,优先让WHERE走唯一索引(如主键),此时通常只加记录锁,不加间隙锁
- 高并发写场景可考虑降级到RC,前提是业务能接受“不可重复读”
SELECT ... FOR UPDATE在RR下也会触发间隙锁,慎用;且必须确保其WHERE条件也走索引,否则照样锁表。
真正难的是判断哪部分该甩给应用层
不是所有“更新”都该用UPDATE硬扛。当本质是用新数据集覆盖旧值时,集合式操作更快更安全:
- MySQL:
INSERT INTO t (id, col) SELECT id, new_val FROM temp ON DUPLICATE KEY UPDATE col = VALUES(col) - PostgreSQL:
MERGE INTO t USING temp ON t.id = temp.id WHEN MATCHED THEN UPDATE SET col = temp.col
前提:temp表有索引,主表id是主键或唯一键;否则性能反不如分批UPDATE。最容易被忽略的点是——锁范围控制的成败,不在SQL怎么写,而在执行计划是否真的只扫了你想锁的那一小块。











