Skip to content

第33章:批量操作与性能优化 ​

本章目标 ​

学完本章后,你将能够:

  1. 使用 CreateInBatches 高效批量插入
  2. 使用 FindInBatches 分批处理大数据集
  3. 理解 PrepareStmt 和 SkipDefaultTransaction 对性能的影响
  4. 识别和解决 N+1 问题
  5. 优化批量操作性能

前置知识 ​

  • 需要先学习:第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~30s100000 条
切片 Create~0.5s1 条(可能超长)
CreateInBatches(1000)~3s100 条

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 缓存预编译 SQL
  • SkipDefaultTransaction: true 写入性能提升 30%
  • N+1 → Preload 或 Joins

练习题 ​

  1. 用 CreateInBatches 插入 10000 条测试数据,对比逐条插入的性能。
  2. 用 FindInBatches 分批处理查询结果。
  3. 写代码验证 SkipDefaultTransaction 对写入性能的影响。
  4. (思考题)批量插入时,用一个大事务包裹所有分批 vs 每批独立事务,各有什么优劣?

把零散知识,整理成可以反复走的路。