用 findinbatches,别用 limit+offset 做大数据分页读取。因为 limit+offset 会全表扫描跳过前 n 行,导致 i/o 和 cpu 浪费、响应超时;而 findinbatches 基于主键游标分批拉取,支持中断恢复、内存可控、性能稳定。

直接结论:用 FindInBatches,别用 Limit+Offset 做大数据分页读取。 否则查到第几万页时,数据库会卡住、内存暴涨、响应超时,而且越往后越慢——这不是代码写得不好,是 SQL 本身在跳过巨量数据时就注定低效。
为什么 Limit+Offset 在大数据下会崩
数据库执行 SELECT * FROM users ORDER BY id LIMIT 100 OFFSET 99900 时,并不会“直接跳到第 100000 条”,而是从头扫描并计数,跳过前 99900 行后才取 100 条。主键索引帮不上忙,I/O 和 CPU 都白耗。
- OFFSET 超过 10 万后,MySQL/PostgreSQL 查询耗时通常呈线性上升
- 应用内存持续持有上一批结果(哪怕只读不改),GC 压力大
- 事务连接长时间占用,阻塞其他写操作
- 无法稳定预估单次耗时,下游服务容易触发重试雪崩
FindInBatches 的真实调用姿势
它不是“自动分页”,而是基于游标(通常是主键 ID)的连续拉取,每次只查下一段,且支持中断恢复。关键点在于:必须显式传入处理函数,不能只取数据。
- 调用时必须提供
batchSize int(如 1000),GORM 会按此切分 - 处理函数签名固定:
func(tx *gorm.DB, batch interface{}) error -
batch是切片类型(如[]User),不是单个结构体 - 若处理中返回 error,整个批次停止,后续批次不再执行
- 默认按主键升序游标推进;如需降序或非主键字段,得手动加
Order("created_at DESC")并确保该字段有索引
err := db.Model(&User{}).Where("status = ?", "active").
FindInBatches(&users, 1000, func(tx *gorm.DB, batch int) error {
// users 是当前批次的 []User 切片
for _, u := range users {
process(u)
}
return nil // 继续下一批
})
容易被忽略的三个坑
很多团队踩过之后才意识到,FindInBatches 不是开箱即用的银弹:
- 它不保证原子性:每批是独立事务,若某批失败,前面成功的批次已提交,需自行设计幂等或补偿逻辑
- 并发安全要自己兜底:多个 goroutine 同时调用同一个
FindInBatches会互相干扰游标,必须串行或加锁 - 关联预加载(
Preload)在批次内生效,但跨批次不共享缓存,高频关联查询建议拆成两阶段:先批量查主表 ID,再用IN批量查关联数据
真正难的从来不是“怎么分批”,而是“怎么让每一批都稳、快、可中断、可重试”。游标位点管理、错误回滚粒度、下游消费节奏对齐——这些细节没对齐,再好的分批也撑不住百万级持续读取。











