vault backup: 2026-04-28 20:56:51
This commit is contained in:
+134
-5
@@ -121,20 +121,37 @@ func GetUserWithTopOrders(db *gorm.DB, id uint) (*User, error) {
|
||||
> 不同数据库对 IN 列表大小有限制:MySQL 默认 `max_allowed_packet` 约 4MB,超过会被截断或报错。对于大规模预加载,考虑分批查询:
|
||||
> ```go
|
||||
> func preloadInBatches(db *gorm.DB, ids []uint, dest any) error {
|
||||
> // dest 必须是切片类型的指针,如 *[]Profile
|
||||
> batch := 500
|
||||
> slices := reflect.ValueOf(dest).Elem() // 获取切片本身
|
||||
>
|
||||
> for i := 0; i < len(ids); i += batch {
|
||||
> end := i + batch
|
||||
> if end > len(ids) {
|
||||
> end = len(ids)
|
||||
> }
|
||||
> end := min(i+batch, len(ids))
|
||||
> chunk := ids[i:end]
|
||||
> if err := db.Where("user_id IN ?", chunk).Find(dest).Error; err != nil {
|
||||
>
|
||||
> var batchResult []map[string]any
|
||||
> if err := db.Where("user_id IN ?", chunk).Find(&batchResult).Error; err != nil {
|
||||
> return err
|
||||
> }
|
||||
>
|
||||
> // 追加到原有切片中
|
||||
> for _, item := range batchResult {
|
||||
> val := reflect.New(slices.Type().Elem())
|
||||
> // 将 map 赋值到结构体字段...(此处略)
|
||||
> slices.Set(reflect.Append(slices, val.Elem()))
|
||||
> }
|
||||
> }
|
||||
> return nil
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> > [!tip] 更简单的替代方案
|
||||
> > 如果关联数据量可控,直接用 Preload + Limit 限制每条主记录的关联数量,避免大规模 IN 查询:
|
||||
> > ```go
|
||||
> > db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
|
||||
> > return db.Order("created_at DESC").Limit(20)
|
||||
> > }).Find(&users)
|
||||
> > ```
|
||||
|
||||
## 减少不必要的 SELECT
|
||||
|
||||
@@ -163,6 +180,46 @@ var stat UserCount
|
||||
db.Model(&User{}).Select("COUNT(*) as count").Scan(&stat)
|
||||
```
|
||||
|
||||
## 批量操作替代循环
|
||||
|
||||
### CreateInBatches
|
||||
|
||||
```go
|
||||
// ❌ 逐条插入:N 次 INSERT
|
||||
for _, user := range users {
|
||||
db.Create(&user)
|
||||
}
|
||||
|
||||
// ✅ 批量插入:单次 SQL(底层拆分为 INSERT INTO ... VALUES (...), (...), (...))
|
||||
users := []User{{Name: "A"}, {Name: "B"}, {Name: "C"}}
|
||||
db.CreateInBatches(users, 100) // 每批 100 条,共 3 条 SQL
|
||||
```
|
||||
|
||||
> [!tip] 默认批次大小
|
||||
> `CreateInBatches` 的第二个参数是**批次大小**而非总数。如果省略或用 0,GORM 会按模型切片长度自动决定——全量一次性插入可能超出 `max_allowed_packet`。建议显式指定 100~500。
|
||||
|
||||
### 批量更新与删除
|
||||
|
||||
```go
|
||||
// 批量更新:使用 SELECT + CASE WHEN 实现多行不同值更新
|
||||
db.Model(&Product{}).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "sku"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"price", "stock"}),
|
||||
}).Create(&products)
|
||||
// UPSERT 语义:存在则更新,不存在则插入
|
||||
|
||||
// 批量删除(条件一致时)
|
||||
db.Where("status = ?", "cancelled").Delete(&Order{})
|
||||
// DELETE FROM orders WHERE status = 'cancelled';
|
||||
```
|
||||
|
||||
> [!note] 批量 vs 单条的性能对比
|
||||
> | 操作 | 1000 条记录 | 说明 |
|
||||
> |------|-----------|------|
|
||||
> | 逐条 Create | ~800ms | N 次网络往返 + N 次事务提交 |
|
||||
> | CreateInBatches(500) | ~50ms | 2 条 SQL,2 次提交 |
|
||||
> | Raw SQL Batch | ~30ms | 绕过 ORM 映射,最快但有维护成本 |
|
||||
|
||||
## 启用预编译语句缓存
|
||||
|
||||
```go
|
||||
@@ -177,6 +234,38 @@ db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
> [!note] 缓存机制
|
||||
> GORM 内置一个基于 `sync.Map` 的简单缓存,存储已经编译好的 SQL 语句。适合重复执行的固定模式查询(如根据 ID 查用户)。不适合大量参数不同的动态查询——缓存命中率低会浪费内存。
|
||||
|
||||
## 连接池配置
|
||||
|
||||
```go
|
||||
import (
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
|
||||
sqlDB, _ := db.DB() // 获取底层 *sql.DB
|
||||
|
||||
// ⚙️ 连接池调优
|
||||
sqlDB.SetMaxOpenConns(50) // 最大打开连接数(含使用中 + 空闲)
|
||||
sqlDB.SetMaxIdleConns(25) // 最大空闲连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间,防 MySQL wait_timeout 断开
|
||||
sqlDB.SetConnMaxIdleTime(30 * time.Minute) // Go 1.15+,空闲超时主动回收
|
||||
```
|
||||
|
||||
> [!warning] 常见陷阱
|
||||
> - **默认 MaxOpenConns = 0(无限制)**:高并发时可能瞬间创建数百连接,压垮数据库。务必显式设置!
|
||||
> - **PreparedStmt 缓存不计入连接池**:开启 `PrepareStmt: true` 后,GORM 使用独立的缓存结构,但 `.DB()` 返回的还是底层连接池。
|
||||
> - **连接泄漏**:事务忘记 Commit/Rollback 会占用连接。配合前面提到的「缩小事务粒度」一起使用效果最佳。
|
||||
|
||||
> [!tip] 如何确定合适的连接池大小?
|
||||
> ```
|
||||
> 推荐公式: MaxOpenConns = CPU核心数 × 2 + 磁盘数
|
||||
> 例: 4核 2盘 → 4×2+2 = 10 (I/O 密集型可适当放宽到 20~50)
|
||||
> ```
|
||||
> 更准确的方式是通过压测观察数据库的活跃连接数和等待队列。
|
||||
|
||||
## 索引优化建议
|
||||
|
||||
### 在 tag 中声明索引
|
||||
@@ -275,6 +364,46 @@ func HandleRequest() error {
|
||||
> })
|
||||
> ```
|
||||
|
||||
## 软删除的性能陷阱
|
||||
|
||||
GORM 的 `SoftDelete` 功能非常便捷,但在高频查询场景下会带来隐式开销。
|
||||
|
||||
### 自动注入 WHERE deleted_at IS NULL
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
gorm.Model // 包含 DeletedAt *time.Time
|
||||
}
|
||||
|
||||
// 无论你是否主动写条件,GORM 自动附加:
|
||||
db.Where("status = ?", "active").Find(&users)
|
||||
// 实际执行:SELECT * FROM users WHERE status = 'active' AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
这意味着**每一个查询都多了一个索引列的判断**。
|
||||
|
||||
### 优化策略
|
||||
|
||||
```go
|
||||
// ❌ 带软删除字段的表,WHERE 子句变复杂时走不了覆盖索引
|
||||
// ✅ 策略一:为 (deleted_at, 查询字段) 建复合索引
|
||||
type Article struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Status string `gorm:"size:16;not null;index:idx_status_deleted"`
|
||||
DeletedAt time.Time `gorm:"index:idx_status_deleted"`
|
||||
}
|
||||
|
||||
// ✅ 策略二:不需要软删除的场景用 Unscoped 跳过过滤
|
||||
db.Unscoped().Where("id = ?", 1).First(&user)
|
||||
// SELECT * FROM users WHERE id = 1; (不含 deleted_at 条件)
|
||||
|
||||
// ✅ 策略三:归档历史数据到独立表,避免主表膨胀
|
||||
// archived_users 表不设软删除,定期将旧数据迁移过去
|
||||
```
|
||||
|
||||
> [!danger] 数据量敏感
|
||||
> 当单表超过千万级记录且开启软删除时,未加合适索引的查询会从 index scan 退化为全表扫描(需要扫描并判断每一行的 `deleted_at` 值)。务必用 `EXPLAIN` 验证核心查询路径。
|
||||
|
||||
## 性能优化检查清单
|
||||
|
||||
```mermaid
|
||||
|
||||
Reference in New Issue
Block a user