批量操作优化

批量操作是数据库性能优化的重头戏。一条一条插入和批量插入,性能可能差几十倍。

批量插入

GORM 的 Create 方法支持批量插入:

users := []User{
    {Name: "张三", Email: "zhangsan@example.com"},
    {Name: "李四", Email: "lisi@example.com"},
    {Name: "王五", Email: "wangwu@example.com"},
}

// 批量插入
db.Create(&users)

GORM 会生成一条 INSERT 语句插入所有记录。

分批插入

数据量大时,一次插入太多会出问题:

  • SQL 语句太长,超过数据库限制
  • 单个事务太大,锁表时间长
  • 内存占用过高

GORM 提供了分批插入的方式:

users := make([]User, 10000)
// ... 填充数据

// 每批 100 条
db.CreateInBatches(users, 100)

CreateInBatches 会把数据分成多批,每批执行一条 INSERT。

批量插入的性能对比

做个简单测试:

// 单条插入
for _, user := range users {
    db.Create(&user)
}

// 批量插入
db.Create(&users)

// 分批插入
db.CreateInBatches(users, 100)

插入 1000 条数据:

  • 单条插入:约 10 秒
  • 批量插入:约 0.1 秒
  • 分批插入:约 0.2 秒

差距明显。批量插入省去了每次建立连接、解析 SQL、写日志的开销。

批量更新

GORM 的 Updates 支持批量更新:

// 更新所有记录的某个字段
db.Model(&User{}).Where("status = ?", "pending").Update("status", "active")

// 根据 ID 批量更新
db.Model(&User{}).Where("id IN ?", ids).Update("status", "active")

// 更新多个字段
db.Model(&User{}).Where("id IN ?", ids).Updates(User{Status: "active", UpdatedAt: time.Now()})

注意批量更新不会触发钩子函数,如果需要钩子,要用 Save 或 Update:

// 会触发 BeforeUpdate、AfterUpdate
for _, id := range ids {
    var user User
    db.First(&user, id)
    user.Status = "active"
    db.Save(&user)
}

但这样又回到了循环操作,性能差。需要权衡业务需求。

批量删除

// 批量删除
db.Where("status = ?", "inactive").Delete(&User{})

// 根据 ID 批量删除
db.Where("id IN ?", ids).Delete(&User{})

同样,批量删除不触发钩子。软删除模型要注意:

// 软删除模型,批量 Delete 只是设置 deleted_at
db.Where("id IN ?", ids).Delete(&User{})

// 真正删除
db.Unscoped().Where("id IN ?", ids).Delete(&User{})

Upsert 操作

Upsert 是 INSERT ON CONFLICT 的简称,存在则更新,不存在则插入:

// MySQL: ON DUPLICATE KEY UPDATE
// PostgreSQL: ON CONFLICT
user := User{Name: "张三", Email: "zhangsan@example.com"}
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "email"}},
    DoUpdates: clause.AssignmentColumns([]string{"name"}),
}).Create(&user)

批量 Upsert:

users := []User{...}
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "email"}},
    DoUpdates: clause.AssignmentColumns([]string{"name", "updated_at"}),
}).Create(&users)

Upsert 比先查再决定插入或更新效率高很多。

批量操作的内存问题

批量操作时要注意内存:

// 从文件读取数据批量插入
file, _ := os.Open("users.csv")
reader := csv.NewReader(file)

batchSize := 100
batch := make([]User, 0, batchSize)

for {
    record, err := reader.Read()
    if err == io.EOF {
        break
    }
    
    batch = append(batch, User{Name: record[0], Email: record[1]})
    
    if len(batch) >= batchSize {
        db.Create(&batch)
        batch = batch[:0]  // 清空,复用底层数组
    }
}

// 处理剩余数据
if len(batch) > 0 {
    db.Create(&batch)
}

这种方式内存占用稳定,不会因为文件大小而变化。

使用原生 SQL

对于超大数据量,原生 SQL 可能更快:

// MySQL 的 LOAD DATA INFILE
db.Exec(`LOAD DATA LOCAL INFILE 'users.csv' 
         INTO TABLE users 
         FIELDS TERMINATED BY ',' 
         (name, email)`)

// PostgreSQL 的 COPY
db.Exec(`COPY users(name, email) FROM '/path/to/users.csv' DELIMITER ',' CSV`)

这些数据库特有的批量导入命令,比 INSERT 快一个数量级。

批量操作与事务

批量操作放在事务里,要么全成功,要么全回滚:

err := db.Transaction(func(tx *gorm.DB) error {
    if err := tx.CreateInBatches(users, 100).Error; err != nil {
        return err
    }
    if err := tx.Where("status = ?", "pending").Update("status", "processed").Error; err != nil {
        return err
    }
    return nil
})

但事务太大也有问题:锁持有时间长、回滚代价大、主从延迟。大数据量操作考虑分批提交。

总结

批量操作的核心原则:

  1. 能批量就批量,避免循环单条操作
  2. 大批量要分批,控制每批大小
  3. 需要钩子就接受性能代价,或用其他方式补偿
  4. 超大数据量考虑原生 SQL 导入
  5. 注意内存控制,流式处理大文件

养成批量思维,代码性能会有质的提升。