用 findinbatches 替代 limit+offset 是防止 gorm 大分页内存溢出最有效手段,因其基于主键游标分页避免全表扫描,而 limit+offset 在大数据量下会因加载并丢弃大量数据导致 oom。

直接结论:用 FindInBatches 替代 Limit+Offset 分页,是防止 GORM 大分页内存溢出最有效、最落地的手段。
为什么 Limit+Offset 在大数据量下必然OOM?
当你要查第 10 万页、每页 100 条时,OFFSET 9999900 不是“跳过 9999900 行”,而是让数据库先扫描并丢弃前 9999900 行——这些行仍要被加载进内存做排序/过滤,只是不返回给应用。GORM 的 Find 会把整批结果全塞进切片,几百万条记录瞬间吃光 Go runtime 的堆内存。
常见错误现象包括:runtime: out of memory、服务 CPU 持续 100% 但无响应、PostgreSQL 报 out of shared memory(尤其在连接数多时)。
这不是 GORM 的 bug,是 SQL 分页模型本身的缺陷。无论你用 MySQL 还是 PostgreSQL,只要走 OFFSET,数据越靠后,性能越断崖式下跌。
FindInBatches 的正确用法和关键参数
FindInBatches 本质是游标分页:它依赖主键(通常是 id)递增有序的特性,每次查 WHERE id > last_id ORDER BY id LIMIT N,跳过全表扫描。
- 必须确保查询条件能命中主键索引(例如
WHERE status = ? AND id > ?,不能写成WHERE created_at > ? AND id > ?后者可能失效) - 批次大小(第二个参数)不是越大越好:建议设为 100–1000;超过 5000 容易触发单次 DB 查询超时,也增加单次事务体积
- 回调函数里不要复用外部变量(如
var results []User),每次调用都会传入新切片,直接操作参数即可 - 如果需要按时间范围分批(比如导出某天日志),优先用
created_at字段 + 索引,再配合FindInBatches,而不是强行用id
示例:
err := db.Where("status = ?", "pending").FindInBatches(&orders, 500, func(tx *gorm.DB, batch int) error {
// tx 是带当前批次数据的新会话,可安全 Save/Update
for i := range orders {
orders[i].Status = "processing"
}
return tx.Save(&orders).Error
})
容易被忽略的三个坑
第一,FindInBatches 默认不开启事务,但你的处理逻辑(比如更新+写日志)往往需要原子性。务必手动包一层 db.Transaction(),否则某批失败会导致数据状态不一致。
第二,如果表没有自增主键或主键不连续(比如用了 UUID),FindInBatches 仍能跑,但性能退化为类似 OFFSET —— 因为数据库无法高效定位 WHERE id > ?。此时应改用时间字段分批,并确保该字段有索引。
第三,别在回调里启动 goroutine 并发处理批次。GORM 会话 tx 不是线程安全的,且每个批次的 tx 生命周期只到回调结束。并发写同一张表还可能引发死锁。
真正难的不是写出 FindInBatches,而是判断什么时候该用它——只要单次查询预期结果超过 1 万条,就该默认启用分批,而不是等 OOM 再补救。











