直接结论:单个事务更新不得超过5000行,WHERE条件必须走索引,SELECT FOR UPDATE须紧贴UPDATE执行,耗时操作须移出事务;UPDATE TOP(n)+主键推进比OFFSET/FETCH更稳,因后者需全量扫描前N行而前者仅查增量且可预测。

直接结论:别让单个事务扛超过 5000 行更新,WHERE 条件必须走索引,SELECT FOR UPDATE 必须紧贴 UPDATE 前执行,所有耗时操作(HTTP、日志、JSON 解析)一律移出事务。
UPDATE TOP (n) + 主键推进为什么比 OFFSET/FETCH 更稳?
OFFSET/FETCH 每次都得从头扫描前 N 行,10 万行后性能断崖下跌;主键推进(如 WHERE id > @last_id AND status = 'pending')每次只查增量,走索引快且可预测。
- SQL Server 示例中必须加
ORDER BY id,否则TOP行为无定义,可能漏更新或重复 - MySQL 不支持 UPDATE + LIMIT 子查询直连源表,得用变量游标或先查 ID 再 IN 更新
- PostgreSQL 的
UPDATE ... WHERE id IN (SELECT id FROM ... ORDER BY id LIMIT 1000)中,ORDER BY字段必须有索引,否则LIMIT失效且触发全表扫描
WHERE 条件不走索引=主动锁表
没索引的 WHERE status = 'pending' 在 MySQL/SQL Server 上大概率触发全表扫描,InnoDB 行锁升级为间隙锁甚至表锁,其他请求全部排队。
- 用
EXPLAIN UPDATE ...(MySQL)或EXPLAIN (ANALYZE) UPDATE ...(PostgreSQL)确认key列非NULL - 避免函数操作:
WHERE DATE(created_at) = '2024-01-01'→ 改成WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02' - 复合索引顺序要匹配查询:查
status = ? AND user_id = ?,索引必须是(status, user_id),反过来无效
事务边界错误是最隐蔽的阻塞源
很多人写 BEGIN TRANSACTION 后套整个 WHILE 循环,最后才 COMMIT——哪怕每行 UPDATE 只要 2ms,10 万行也锁超 3 分钟。
- 每批必须独立
BEGIN TRANSACTION→ 执行 →COMMIT或ROLLBACK,不能依赖自动提交 - SQL Server 必须包在
TRYCATCH里,捕获错误 1205(死锁)后重试,而不是崩掉整个过程 - MySQL 存储过程中要用
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION,否则异常直接中断循环 - 最容易被忽略的是“事务是否真结束了”:调试中断、连接池复用未清理、应用异常退出,都会留下
trx_state = 'RUNNING'的幽灵事务,它不争锁但拖慢 MVCC 清理、阻塞 DDL、占连接

















