第33章:批量操作与性能优化
本章目标
学完本章后,你将能够:
- 使用
CreateInBatches高效批量插入 - 使用
FindInBatches分批处理大数据集 - 理解
PrepareStmt和SkipDefaultTransaction对性能的影响 - 识别和解决 N+1 问题
- 优化批量操作性能
前置知识
- 需要先学习:第09章(Create)、第25章(Preload)、第32章(连接池)
33.1 CreateInBatches:分批插入
go
// 准备 10 万条数据
var users []User
for i := 0; i < 100000; i++ {
users = append(users, User{
Name: fmt.Sprintf("用户%d", i),
Age: rand.Intn(50) + 18,
})
}
// 每批 1000 条,分 100 次插入
db.CreateInBatches(users, 1000)性能对比:
| 方式 | 10万条耗时 | SQL 条数 |
|---|---|---|
| 逐条 Create | ~30s | 100000 条 |
| 切片 Create | ~0.5s | 1 条(可能超长) |
| CreateInBatches(1000) | ~3s | 100 条 |
33.2 FindInBatches:分批查询
go
// 分批处理 100 万条数据,每批 1000 条
var results []User
db.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error {
// 处理这一批数据
for _, user := range results {
// 处理 user
}
fmt.Printf("处理了第 %d 批\n", batch)
return nil
})33.3 性能配置优化
PrepareStmt:预编译缓存
go
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
PrepareStmt: true, // 缓存预编译 SQL
})
// 每条 SQL 只在第一次执行时编译,后续直接换参数
db.Where("name = ?", "张三").Find(&users) // 编译 + 缓存
db.Where("name = ?", "李四").Find(&users) // 直接用缓存SkipDefaultTransaction:跳过默认事务
go
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 写入性能提升约 30%
})33.4 N+1 问题识别与解决
go
// ❌ N+1:每条主记录都单独查关联
var users []User
db.Find(&users)
for _, u := range users {
db.Where("user_id = ?", u.ID).Find(&u.Orders)
}
// ✅ Preload:2 条 SQL 全部加载
db.Preload("Orders").Find(&users)
// ✅ Joins:1 条 SQL(仅限 BelongsTo/HasOne)
db.Joins("Company").Find(&users)33.5 实战:导入 10 万条数据优化
go
func ImportUsers(db *gorm.DB, count int) error {
batchSize := 1000
// 关闭默认事务提升性能
db = db.Session(&gorm.Session{
SkipDefaultTransaction: true,
PrepareStmt: true,
})
return db.Transaction(func(tx *gorm.DB) error {
for i := 0; i < count; i += batchSize {
end := i + batchSize
if end > count {
end = count
}
// 生成一批数据
users := make([]User, end-i)
for j := range users {
users[j] = User{
Name: fmt.Sprintf("用户%d", i+j),
Age: rand.Intn(50) + 18,
}
}
// 分批插入
if err := tx.Create(&users).Error; err != nil {
return err
}
fmt.Printf("进度: %d/%d\n", end, count)
}
return nil
})
}常见错误
错误1:逐条 Create 10 万条数据
go
// ❌ 10 万次数据库往返,太慢
for i := 0; i < 100000; i++ {
db.Create(&User{Name: fmt.Sprintf("用户%d", i)})
}
// ✅ 分批插入
db.CreateInBatches(users, 1000)错误2:N+1 未被发现
go
// Preload 没加,循环中悄悄 N+1
var users []User
db.Find(&users)
for _, u := range users {
fmt.Println(len(u.Orders)) // 每次循环触发新查询!
}本章小结
CreateInBatches(data, batchSize)分批插入FindInBatches(&dest, batchSize, callback)分批处理查询PrepareStmt: true缓存预编译 SQLSkipDefaultTransaction: true写入性能提升 30%- N+1 → Preload 或 Joins
练习题
- 用
CreateInBatches插入 10000 条测试数据,对比逐条插入的性能。 - 用
FindInBatches分批处理查询结果。 - 写代码验证
SkipDefaultTransaction对写入性能的影响。 - (思考题)批量插入时,用一个大事务包裹所有分批 vs 每批独立事务,各有什么优劣?