批量操作

处理大量数据时,逐条操作效率太低。批量操作能显著提升性能,但也有一些注意事项。

批量插入

插入多条记录:

users := []User{
    {Name: "用户1", Age: 20},
    {Name: "用户2", Age: 25},
    {Name: "用户3", Age: 30},
}
db.Create(&users)

GORM 会生成一条 INSERT 语句,包含所有数据。

分批插入

数据量大时,单条 SQL 可能超出数据库限制。用 CreateInBatches 分批:

users := make([]User, 10000)
for i := range users {
    users[i] = User{Name: fmt.Sprintf("用户%d", i), Age: i % 100}
}
db.CreateInBatches(&users, 100)

每批 100 条,生成多条 INSERT 语句。

分批大小建议:

  • MySQL 默认 max_allowed_packet 是 4MB
  • 一般每批 100-1000 条
  • 单条记录较大时减小批次

批量更新

更新符合条件的所有记录:

db.Model(&User{}).Where("role = ?", "guest").Update("role", "member")

更新多个字段:

db.Model(&User{}).Where("age < ?", 18).Updates(map[string]interface{}{
    "status": "minor",
    "role":   "limited",
})

批量删除

删除符合条件的所有记录:

db.Where("status = ?", "inactive").Delete(&User{})

软删除模型同样适用:

db.Where("last_login < ?", time.Now().AddDate(-1, 0, 0)).Delete(&User{})

批量操作与钩子

批量操作默认不触发钩子:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    fmt.Println("创建前钩子")
    return nil
}

users := []User{{Name: "用户1"}, {Name: "用户2"}}
db.Create(&users)

钩子不会被调用。

要触发钩子,需要逐条操作或使用迭代:

for _, user := range users {
    db.Create(&user)
}

性能会下降,但钩子会执行。

批量操作与事务

大批量操作应该放在事务中:

err := db.Transaction(func(tx *gorm.DB) error {
    if err := tx.CreateInBatches(&users, 100).Error; err != nil {
        return err
    }
    if err := tx.Model(&User{}).Where("age < ?", 18).Update("status", "minor").Error; err != nil {
        return err
    }
    return nil
})

事务保证原子性,失败时回滚。

使用原生 SQL

超大数据量时,原生 SQL 更高效:

db.Exec(`
    INSERT INTO users (name, age, created_at)
    VALUES (?, ?, NOW())
`, "测试", 20)

批量插入用 UNION ALL

db.Exec(`
    INSERT INTO users (name, age)
    SELECT ?, ? UNION ALL SELECT ?, ?
`, "用户1", 20, "用户2", 25)

或者用数据库特有的批量语法:

// MySQL
db.Exec(`
    INSERT INTO users (name, age) VALUES 
    ('用户1', 20),
    ('用户2', 25),
    ('用户3', 30)
`)

// PostgreSQL
db.Exec(`
    INSERT INTO users (name, age) VALUES 
    ($1, $2), ($3, $4), ($5, $6)
`, "用户1", 20, "用户2", 25, "用户3", 30)

从文件导入

MySQL 的 LOAD DATA INFILE

db.Exec(`
    LOAD DATA LOCAL INFILE '/path/to/users.csv'
    INTO TABLE users
    FIELDS TERMINATED BY ','
    LINES TERMINATED BY '\n'
    (name, age)
`)

PostgreSQL 的 COPY

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

这是最快的导入方式,适合初始化大量数据。

批量操作性能对比

以插入 10000 条记录为例:

方式耗时
逐条插入30s+
单条 INSERT 多值0.5s
CreateInBatches(100)0.8s
LOAD DATA INFILE0.1s

差距明显,选择合适的方式很重要。

批量查询

查询大量数据时,避免一次性加载到内存:

rows, err := db.Model(&User{}).Rows()
if err != nil {
    panic(err)
}
defer rows.Close()

for rows.Next() {
    var user User
    db.ScanRows(rows, &user)
    // 处理 user
}

流式处理,内存占用恒定。

分批处理

大数据量处理时分批进行:

pageSize := 1000
page := 0

for {
    var users []User
    result := db.Offset(page * pageSize).Limit(pageSize).Find(&users)
    if result.Error != nil || len(users) == 0 {
        break
    }
    
    for _, user := range users {
        // 处理 user
    }
    
    page++
}

常见问题

内存溢出

一次性加载太多数据:

var users []User
db.Find(&users)

数据量大时内存爆炸。用分批查询或流式处理。

超时

批量操作耗时太长:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

db.WithContext(ctx).CreateInBatches(&users, 100)

设置合理的超时时间。

锁等待

大批量更新可能阻塞其他操作:

db.Transaction(func(tx *gorm.DB) error {
    return tx.Model(&User{}).Where("1=1").Update("status", "inactive").Error
})

分批处理,减少单次锁定范围。

小结

批量操作是性能优化的关键。选择合适的批次大小,配合事务和流式处理,能高效处理大量数据。