单条大范围UPDATE会卡死数据库,因其在千万级表上触发全表扫描、行锁堆积甚至锁升级为表锁,导致undo/redo log压力激增、主从延迟数小时;必须分批按有索引的单调字段(如id)切片更新,每批后COMMIT并休眠,并确保WHERE条件走索引。

为什么单条大范围 UPDATE 会卡死数据库
直接执行 UPDATE big_table SET status = 1 WHERE created_at < '2024-01-01' 这类语句,在千万级表上大概率触发全表扫描、长时间行锁堆积,甚至锁升级为表锁。MySQL 的 InnoDB 在事务中持有 undo log 和 redo log 的压力会指数级上升,同时从库同步延迟可能飙升到数小时——这不是慢,是阻塞。
分批更新必须带确定性边界,不能只靠 LIMIT
LIMIT 在无 ORDER BY 时行为不确定,容易漏更新或重复更新。正确做法是按有索引的单调字段(如主键 id 或时间戳 updated_at)切片:
- 先查出最小和最大 ID:
SELECT MIN(id), MAX(id) FROM big_table WHERE created_at < '2024-01-01' - 每次更新固定范围:
UPDATE big_table SET status = 1 WHERE id BETWEEN 100001 AND 105000 AND created_at < '2024-01-01' - 每批后加
COMMIT,并休眠 50–200ms 缓解系统压力 - 用应用层循环或存储过程控制,避免在数据库里写复杂 WHILE(易超时)
WHERE 条件不走索引,优化就等于没做
执行前必须跑 EXPLAIN FORMAT=TREE(MySQL 8.0+)确认 type 不是 ALL。常见陷阱包括:
-
WHERE DATE(created_at) = '2024-01-01'→ 改成created_at >= '2024-01-01' AND created_at < '2024-01-02' - 多条件组合时,联合索引顺序错位,例如
WHERE status = 0 AND updated_at > '2024-01-01'应建(status, updated_at),而非反过来 - 字段上有触发器、外键约束或生成列,也会拖慢速度;临时禁用需评估业务一致性风险
MERGE INTO 或 INSERT … ON DUPLICATE KEY UPDATE 更快,但有前提
当你的“更新”本质是用新数据集覆盖旧值(比如从临时表 temp_updates 同步到主表),集合式写法比逐条 UPDATE 快得多:
- MySQL:
INSERT INTO big_table (id, name, status) SELECT id, name, 1 FROM temp_updates ON DUPLICATE KEY UPDATE status = VALUES(status) - PostgreSQL:
MERGE INTO big_table USING temp_updates ON big_table.id = temp_updates.id WHEN MATCHED THEN UPDATE SET status = temp_updates.status - 前提是
temp_updates已建好索引,且主表id是主键或唯一键;否则性能反不如分批 UPDATE
真正难的不是选哪种语法,而是判断哪部分该由数据库做、哪部分该甩给应用层——比如聚合后更新(UPDATE ... = (SELECT SUM() FROM ...))几乎必然慢,应拆成两步:先聚合写入临时表,再用 MERGE 覆盖。这个边界,很多人一开始根本没意识到。


















