FindInBatches是大批量更新的内存安全入口,按批次回调处理,每批仅持有当前数据;需设非零batchSize,避免累积全量切片,及时Save/Updates,必要时外层加事务;Updates比Save省内存但需注意零值陷阱;极大量简单更新可选原生SQL+Exec。

FindInBatches 是大批量更新的内存安全入口
直接 db.Find(&users) 加循环 Save 更新,50 万条数据会把整个结果集加载进内存,GORM 还要反射赋值,极易触发 OOM。正确起点是 FindInBatches —— 它不返回完整切片,而是按批次回调处理,每批只 hold 住当前批次的数据。
- 必须传入非零的
batchSize,比如1000;设为0或不传会退化成全量加载 - 回调函数内不要累积所有记录到一个大切片里,否则等于没用:错误写法
allUsers = append(allUsers, users...) - 每批次处理完立即调用
tx.Save(&users)或tx.Updates(&users),避免跨批次持有引用 - 注意
FindInBatches默认不开启事务,如需原子性,得在外层显式db.Transaction包裹
Updates 批量更新比 Save 更省内存但有字段陷阱
Updates 接收 struct 或 map,只生成 SET 字段的 SQL,不读取原记录,自然比先 Find 再 Save 少一次全量查询和内存占用。但它对零值(0、""、false)默认忽略 —— 这在“想把某字段明确设为零”时会出错。
- 用 map 参数可绕过零值过滤:
db.Model(&User{}).Where("status = ?", "pending").Updates(map[string]interface{}{"status": "done", "retry_count": 0}) - struct 参数下若需更新零值,得配合
Select显式指定字段:db.Select("retry_count").Model(&User{}).Where(...).Updates(user) - 别在
Updates中混用 struct 和条件字段,容易漏掉 WHERE:错误写法db.Updates(user).Where(...)(Where 无效),应写成db.Model(&User{}).Where(...).Updates(user)
原生 SQL + Exec 避开 GORM 反射开销
当更新逻辑简单(比如统一设某个状态)、数据量极大(百万级以上)、且不依赖 GORM 钩子或关联时,绕过 ORM 直接执行 SQL 是最轻量的方式。GORM 的 Exec 方法返回 sql.Result,不解析行数据,内存占用趋近于常数。
- 用
db.Exec("UPDATE users SET status = ? WHERE created_at ,不构造任何 struct - 参数必须用问号占位符,别拼字符串,防 SQL 注入
- 无法自动触发
BeforeUpdate等钩子,业务逻辑强依赖钩子时慎用 - PostgreSQL/MySQL 的批量更新语法差异大,跨库项目需封装适配层,别硬写方言 SQL
Session 级别关闭默认事务和日志能降低 GC 压力
GORM 默认每个 Create/Save 都启事务、打日志,大批量更新时这些额外动作会拖慢速度、增加内存分配频次。用 Session 关掉它们,效果立竿见影。
- 关事务:
db.Session(&gorm.Session{SkipDefaultTransaction: true}),前提是业务能接受单条失败不影响其他条 - 关日志:
db.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)}),避免日志字符串拼接和 buffer 扩容 - 别全局关日志,只在批量更新链路中临时关闭,否则排查问题时抓瞎
-
SkipDefaultTransaction对Updates和Exec也生效,但FindInBatches回调里的操作仍需自己控制事务边界
for _, u := range users 循环时,u 是副本,&u 指向的是栈上同一地址,批量 Save(&u) 最后全存成同一条记录。必须用 for i := range users { db.Save(&users[i]) } 或直接传切片给 Updates。


















