大更新触发OOM而非变慢,是因为数据库或客户端将整批结果集、事务日志、排序/连接缓冲全加载进内存,如MySQL的sort_buffer_size、PostgreSQL的work_mem及JDBC默认fetchSize=-1导致堆内存飙升。

为什么大更新会 OOM,而不是慢?
SQL 大批量 UPDATE 触发内存溢出(OOM),往往不是因为语句写错了,而是数据库或客户端默认把整批结果集、事务日志、临时排序/连接缓冲全加载进内存。比如 MySQL 的 sort_buffer_size、PostgreSQL 的 work_mem,甚至 JDBC 驱动默认启用的 fetchSize 为 -1(即拉取全部),都会让应用端堆内存瞬间飙高。
常见错误现象:java.lang.OutOfMemoryError: Java heap space 出现在执行完 executeUpdate() 后、但还没 commit 前;或者 PostgreSQL 报 ERROR: out of memory 在 UPDATE ... FROM 场景里。
- 别依赖“单条 SQL 更快”——它可能更快地压垮内存
- 确认你的驱动是否自动缓存结果:MySQL Connector/J 默认
useCursorFetch=true才能流式读,否则ResultSet会预加载全部 - UPDATE 本身不返回结果集,但如果你用
SELECT ... FOR UPDATE+ 循环更新,就极易触发客户端 OOM
MySQL 分段 UPDATE 怎么写才不锁表太久
直接 WHERE id BETWEEN ? AND ? 是最稳妥的分段方式,前提是 id 是主键或有高效索引。避免用 LIMIT + OFFSET,因为偏移越大,扫描行数越多,性能断崖式下跌。
示例(安全可控):
UPDATE orders SET status = 'shipped' WHERE id >= 10000 AND id < 20000 AND status = 'pending';
- 每次更新控制在 1k–5k 行以内,具体看单行数据大小和索引效率
- 务必在
WHERE条件里复用已有索引字段,不要写成WHERE status = 'pending' LIMIT 1000—— 这会强制全表扫描 - 加
AND status = 'pending'这类过滤条件,防止重复更新已处理的行 - 注意 MySQL 5.7+ 的
innodb_lock_wait_timeout,长事务容易被 kill,建议每批后COMMIT
JDBC 流式处理 UPDATE 的三个硬开关
JDBC 本身不支持“流式 UPDATE”,但你可以通过控制查询 + 更新的节奏,模拟流式行为。关键不在 SQL,而在连接配置和执行逻辑。
- 设置
statement.setFetchSize(Integer.MIN_VALUE)或setFetchSize(1)(取决于驱动),强制游标式读取 —— 对SELECT有效,是分段前提 - 禁用自动提交:
connection.setAutoCommit(false),自己控制commit()频率 - 使用
PreparedStatement而非Statement,避免每次编译,减少元数据内存占用 - 别在循环里反复
prepareStatement(),复用同一个实例
典型结构不是“一条 SQL 更新百万行”,而是:
SELECT id FROM orders WHERE status = 'pending' ORDER BY id LIMIT 1000<br>→ 拿到这批 id 列表 →<br>UPDATE orders SET status = 'shipped' WHERE id IN (?, ?, ...)
注意:IN 列表长度别超 1000 项,否则 MySQL 可能退化为全表扫描或报 max_allowed_packet 错误。
PostgreSQL 的 UPDATE ... FROM 和游标方案怎么选
UPDATE ... FROM 看似简洁,但底层会构建中间哈希表或嵌套循环,如果关联子查询返回几十万行,work_mem 不够就会写磁盘甚至 OOM。这时候游标(DECLARE / FETCH / UPDATE WHERE CURRENT OF)反而更省内存,因为它按需取、逐行更新。
- 小批量(UPDATE ... FROM (VALUES (...)) AS t(id),显式传 ID 列表
- 大批量且 ID 已落库:建临时表
CREATE TEMP TABLE batch_ids (id bigint),再UPDATE ... FROM batch_ids,记得建索引 - 必须强一致性且不能锁全表:用游标,配合
FOR UPDATE SKIP LOCKED避免竞争 - 别信
EXPLAIN里的“cost”,实际内存占用要看work_mem * 并发数,线上建议设为 8–16MB
真正容易被忽略的是:PostgreSQL 的 VACUUM 压力。分段更新产生大量死元组,如果没及时清理,后续查询会越来越慢,甚至触发 autovacuum IO 飙升 —— 这不是 OOM,但效果类似。

















