createinbatches + findinbatches 组合需手动控制批次、禁用钩子、显式事务并接受主键不回填,否则易致数据错位、oom或部分成功;findinbatches基于主键游标查询(where id > ? order by id limit ?),依赖索引实现恒定扫描行数,避免offset分页性能雪崩。

CreateInBatches + FindInBatches 组合不是“自动缓解内存占用”的银弹,必须手动控制批次、禁用钩子、显式事务,并接受主键不回填——否则极易出现数据错位、OOM 或部分成功。
FindInBatches 为什么比 OFFSET 分页抗压
它不靠 OFFSET 跳过前 N 行,而是基于主键(默认 id)做游标查询:WHERE id > ? ORDER BY id LIMIT ?。数据库能直接走主键索引,扫描行数恒定,不受总数据量影响。
常见错误是漏写 ORDER BY:GORM 虽会自动补 ORDER BY id,但若模型主键非 id(比如叫 user_id),或用了复合主键,GORM 就不会补,导致结果乱序、重复或遗漏。
使用时注意:
- 必须确保查询条件能命中索引,否则游标失效(例如
WHERE status = ? AND id > ?,需联合索引(status, id)) - 不要在回调函数里修改传入的切片地址,
FindInBatches(&results, 100, fn)中results每次都会被重置,不能缓存引用 - 回调函数内禁止调用
tx.First()等会干扰当前事务状态的操作
CreateInBatches 写入时主键为何还是 0
MySQL 不支持 RETURNING,CreateInBatches 插入后结构体字段不会被自动更新。PostgreSQL 支持,但仅限单批;多批时 clause.Returning 返回值会错位,users[0].ID 可能对应第 2 批的第 1 条记录。
典型现象:
- 插入后打印
users[0].ID是 0,但查库发现已写入 - 后续逻辑依赖刚插入的 ID 做关联写入,直接 panic 或写错外键
解决路径有限:
- MySQL 场景:放弃回填,改用业务唯一键(如
order_no)做后续关联 - PostgreSQL 场景:限制
batchSize为 1,或改用原生 SQL +RETURNING手动处理 - 绝对不要用
LAST_INSERT_ID()推算——并发写入时不可靠
批量读+分片写必须关掉三样东西
默认配置下,FindInBatches + CreateInBatches 组合极易吃光内存或拖垮 DB:
-
db.Session(&gorm.Session{SkipHooks: true}):关闭BeforeCreate等钩子,避免每条记录都触发校验逻辑(批量校验应前置) -
db.Session(&gorm.Session{SkipDefaultTransaction: true}):GORM 默认为每个CreateInBatches开事务,嵌套在FindInBatches回调里会导致长事务锁表 -
db.Session(&gorm.Session{PrepareStmt: true}):启用预编译语句复用,否则每批都重新解析 SQL,CPU 白耗
漏关任意一项,10 万行就可能触发连接池打满、GC 频繁或事务超时。
批次大小设成多少才不崩
100–500 是经验值,不是魔法数字。实际要按数据单行体积和数据库配置反推:
- MySQL:检查
max_allowed_packet(默认 4MB),估算单行平均字节数 ×batchSize≤ 3MB 安全线 - PostgreSQL:注意
work_mem,单批排序/聚合若超限会落盘,性能断崖下跌 - 内存侧:每批加载进 Go 内存的结构体 + 切片头开销,建议单批对象总内存 ≤ 16MB(避免大对象堆碎片)
最稳妥做法:先用 batchSize=100 跑通流程,再逐步加到 300,同时监控 pprof 的 inuse_space 和数据库慢日志。
真正卡住多数人的,从来不是“怎么调 API”,而是忘了事务边界在哪、钩子有没有静默吃掉 CPU、以及——坚信主键一定会回填到原始切片里。











