GORM的Create(&users)并非批量插入,而是逐条执行INSERT;应使用CreateInBatches(传值、批次100–500)或原生SQL(控制长度≤1MB、防注入)实现真正批量写入。

GORM 的批量插入不是简单把切片传给 Create 就完事的——它会逐条执行 INSERT,性能差、易超时、还可能触发事务锁。真要批量写入,得用 CreateInBatches 或绕过 ORM 拼原生 SQL。
为什么 Create(&users) 不是批量插入
很多人看到 db.Create(&users) 传的是切片,就默认它是批量操作。实际上 GORM v2 默认把它当“多个单条插入”处理:
- 每条记录都走一遍 prepare → exec 流程,生成 N 条独立
INSERT INTO ... VALUES (...) - 若没显式开启事务,每条都在自己的隐式事务里提交,IO 和锁开销翻倍
- 当
users超过几百条,MySQL 可能报Packet too large或连接超时
这跟数据库层面的“批量插入”(单条 SQL 插入多行)完全不是一回事。
CreateInBatches 的正确用法和参数陷阱
CreateInBatches 是 GORM 提供的真正批量方案,但必须注意两个关键点:
- 第一个参数是值(不是指针),传
users,不是&users - 第二个参数是每批行数,建议设为 100–500;设太大可能触发 MySQL
max_allowed_packet限制 - 它内部会把切片分组,每组拼成一条
INSERT INTO ... VALUES (...), (...), (...)
示例:
var users = []User{
{Username: "a"},
{Username: "b"},
{Username: "c"},
}
db.CreateInBatches(users, 100) // ✅ 正确:传值 + 指定批次大小
// db.CreateInBatches(&users, 100) // ❌ 错误:不能传指针
执行后,GORM 会自动填充主键 ID 到每个 User 实例中(前提是模型定义了 ID 字段且数据库支持自增)。
超大批量(>10 万行)该用原生 SQL
当数据量达到十万级,CreateInBatches 仍可能因内存占用高、GC 压力大、日志刷屏而变慢。这时应切换到原生 SQL 批量插入:
- 用
strings.Builder拼接VALUES部分,避免fmt.Sprintf频繁分配 - 手动处理 NULL、时间格式、字符串转义(尤其是单引号和反斜杠)
- 务必控制单条 SQL 总长度,建议单次不超过 1MB,对应约 2000–5000 行(视字段数而定)
- 用
db.Exec()执行,不经过 GORM 的钩子和日志,减少干扰
例如插入带时间字段的记录:
values := strings.Builder{}
values.WriteString("INSERT INTO `users` (`username`, `created_at`) VALUES ")
for i, u := range users {
if i > 0 {
values.WriteString(", ")
}
// 注意:实际使用需用 sql.Named 或参数化防止注入,此处仅为示意结构
values.WriteString(fmt.Sprintf(`('%s', '%s')`, u.Username, u.CreatedAt.Format("2006-01-02 15:04:05")))
}
db.Exec(values.String())
容易被忽略的细节
批量插入不是“只要快就行”,以下三点常被跳过,但线上出问题基本都栽在这儿:
-
CreateInBatches不会触发BeforeCreate钩子——如果你依赖钩子里的逻辑(比如生成 UUID、设置默认状态),得提前在切片里赋好值 - MySQL 默认事务隔离级别下,大批量插入可能长时间持有表级或行级锁,影响其他读写;必要时加
db.Session(&gorm.Session{AllowGlobalUpdate: true})控制会话行为 - 别在循环里反复调用
CreateInBatches插入小批次(如每次 10 条)——函数调用和 SQL 解析开销反而比单次大批次更高











