FindInBatches 必须显式指定稳定排序字段(如 id ASC 或 created_at DESC, id DESC),否则结果不可靠;handler 中需逐条更新、禁止整批 Save;游标分页优于 Offset,且错误只能由 handler 返回。

FindInBatches 必须显式指定 ORDER BY,否则结果不可靠
很多人调用 FindInBatches 时没写 Order,结果数据漏、重复、顺序乱——这不是 GORM 的 bug,是 SQL 标准行为。MySQL 和 PostgreSQL 对无 ORDER BY 的查询不保证返回顺序,哪怕表只读、没并发写入,同一条语句执行两次都可能拿到不同行。
- 必须写
DB.Order("id ASC")或DB.Order("created_at DESC, id DESC"),单用Order("status ASC")不行(status 值重复太多,排序不稳定) - 时间字段慎用
Order("created_at DESC"):高并发下毫秒级重复常见,补上id DESC才能兜底 - 如果主键是 UUID,别硬套
id > ?,改用复合游标:WHERE (created_at, id) > (?, ?),且索引必须包含这两个字段且顺序一致
handler 函数里直接 Save 整批数据会覆盖 ID,导致逻辑错乱
FindInBatches 的 handler 参数接收的是 *gorm.DB 和当前批次的切片,但它的设计不是“帮你自动更新”,而是“喂你一批数据,你自己处理”。很多人在 handler 里写 tx.Save(&results),结果整批记录的 id 全被设成同一个值,后续批次就全乱了。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 要逐条更新:
for i := range results { tx.Model(&results[i]).Where("id = ?", results[i].ID).Updates(map[string]interface{}{"status": "processed"}) } - 别在 handler 里修改
results后直接Save,GORM 会把整批当成新插入或覆盖更新 - 如果想跳过已处理记录,条件必须写在外部查询里,比如
DB.Where("processed = ?", false).Order("id ASC").FindInBatches(...)
游标分页比 Offset 分页快不是玄学,是数据库索引真实生效
当数据量超过 10 万行,OFFSET 100000 在 MySQL/PostgreSQL 中不是“跳过”,而是“扫描前 10 万行 + 排序 + 丢弃”。CPU 和 IO 都在为丢弃买单。而游标分页 WHERE id > 50000 ORDER BY id LIMIT 100 能直接命中主键索引,性能基本不随数据量增长。
- 确保
id字段有索引,否则WHERE id > ?会退化成全表扫描 - 禁止混用方向:
ORDER BY id DESC时,条件必须是id ,写成 <code>id > ?就查不到数据 - 首次请求不用 where,后续请求必须传上一批最后一条的
id(前端透传,后端不转换页码) - SQLite 对大 offset 几乎无优化,TiDB 会主动限流报错,游标分页在这两类库上不是可选项,是刚需
FindInBatches 的 error 只能从 handler 返回,外部 Error 永远是 nil
FindInBatches 返回的是 *gorm.DB,它的 Error 字段始终为 nil,哪怕 handler 里 return 了 error。错误只能通过 handler 的返回值捕获,而且一旦 handler 返回 error,整个遍历会立即终止。
- handler 必须显式返回 error:
return tx.Error或自定义错误,不能只 log 然后忽略 - 不要依赖
result.Error判断是否成功,它永远为空 - 如果某批处理失败需要重试,得自己在 handler 里加重试逻辑或记录断点(比如最后成功处理的
id) - 并发处理时,别在 handler 里起 goroutine 并发更新,事务上下文不跨 goroutine,容易 panic 或数据错乱
FindInBatches 的关键差异不在语法,而在对“排序字段稳定性”和“更新逻辑闭环”的要求——漏掉任意一个,数据就会在百万级规模下无声崩塌。

















