数据库大批量更新,能一次干完的活,千万别分成一万次来干。把单条循环的写法摆在百万级数据跟前,那不是慢,是典型的自毁式SQL。批量更新的核心其实就一句话:让数据库一次性把该干的活干完,而不是反复叫它起来、躺下、再起来。

说几个核心判断,先把它理清了。
WHERE 条件走索引,这是死线
没有索引的 UPDATE,形同全表扫描加全表加锁——哪怕只改一行,数据库也得把整张表翻一遍。这路子,你再怎么分批优化也救不回来。
正确做法是,先跑 EXPLAIN FORMAT=TREE(MySQL 8.0+)或 EXPLAIN ANALYZE(PostgreSQL),确认 type 不是 ALL。如果看到 type=ALL,说明你的查询已经在全表扫了,拿啥优化都没用。
- 复合条件优先建联合索引,索引字段的排列顺序按区分度从高到低来。比如
WHERE status = ? AND updated_at > ?,索引可以设计为(status, updated_at) - 函数用法要警惕:
WHERE DATE(created_at) = '2024-01-01'这种写法,直接让函数对索引字段动手,索引立刻失效。改用created_at >= '2024-01-01' AND created_at < '2024-01-02' - 状态类字段(比如
status)单独建索引意义不大,因为区分度太低。它得搭上时间或ID这类高区分度字段,才能发挥索引的效果
分批更新走游标式推进,别用偏移分页
很多人一上来就是 LIMIT offset, size 的分页写法——刚一用还行,越往后越慢,而且容易丢数据。正确做法是按索引字段“游标式”推进,每次查的起点交给上一次的最大ID来定。
- 推荐写法:
WHERE id > 100000 ORDER BY id LIMIT 5000,记录本轮最大id作为下轮起点 - 每批必须显式
COMMIT,这样行锁和 undo log 才能及时释放,避免事务堆积拖垮系统 - 每批的大小控制在 1000–5000 行比较理想。太小了,事务开销占比高;太大了,锁持有时间长、日志压力也大
- 应用层加个
SLEEP(0.1)缓冲,对主从架构尤其重要——可以有效防止复制延迟突然飙升
合并更新:CASE WHEN 或临时表 JOIN,比逐条循环快十倍
要更新1万行不同ID的不同值?别写1万个 UPDATE,也别拼一条超长SQL。两套主流方案就够用:
CASE WHEN单语句:适合离散ID的简单更新,SQL长度可控时最轻量。
UPDATE users SET score = CASE id WHEN 1 THEN 95 WHEN 2 THEN 87 ELSE score END WHERE id IN (1,2);临时表 +
JOIN:适合数据来源复杂的情况,比如来自CSV、API或其他表。
先CREATE TEMPORARY TABLE tmp_updates (id INT PRIMARY KEY, new_score INT);,插入数据后不要忘记建索引,再执行:UPDATE users u JOIN tmp_updates t ON u.id = t.id SET u.score = t.new_score;
参数调优别乱动,但以下三个确实有用
临时调整数据库配置能提速,但多数人调错了地方。以下三个才是真正影响批量 UPDATE 的关键:
innodb_flush_log_at_trx_commit = 2:仅限于非金融业务,这样可以降低 redo log 刷盘频率,写入速度可提升3–5倍SET FOREIGN_KEY_CHECKS = 0和SET UNIQUE_CHECKS = 0:关掉约束校验,更新完立刻恢复。注意外键级联行为会失效,这是代价- 别乱动
innodb_log_file_size:如果频繁报 “log is full”,说明这个值确实小了,但调整需要重启数据库,那属于运维范畴,不是SQL优化能解决的
最后,最容易被忽略的一步:更新前先 SELECT COUNT(*) 统计影响行数。没有这一步,你根本不知道这批要跑多久,会占用多少 undo log,会不会触发锁升级——所有优化都等于盲打。
