454 lines
15 KiB
Markdown
454 lines
15 KiB
Markdown
|
|
---
|
|||
|
|
tags: [GORM, Go, ORM, 性能优化, N+1, Preload, Index, QueryPlan]
|
|||
|
|
create time: 2026-04-28 00:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 性能优化
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
GORM 的功能丰富但有一定运行时开销——相比于手写 SQL,它的查询构建和反射机制会带来额外的 CPU/内存消耗。了解这些瓶颈并针对性优化,是 GORM 在生产环境中稳定运行的高并发场景下的关键。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
A["🐌 慢的原因"] --> B{"类别"}
|
|||
|
|
B --> |查询次数多| SlowQuery["N+1 / 过多 SQL"]
|
|||
|
|
B --> |数据量大| LargeResult["未分页 / 全表扫描"]
|
|||
|
|
B --> |缺少索引| NoIndex["WHERE / JOIN / ORDER BY 无索引"]
|
|||
|
|
B --> |框架开销| FrameworkOverhead["反射 + 对象映射"]
|
|||
|
|
|
|||
|
|
SlowQuery --> FixPreload["Preload 预加载"]
|
|||
|
|
LargeResult --> FixPaging["Limit / Offset"]
|
|||
|
|
NoIndex --> FixIndex["添加数据库索引"]
|
|||
|
|
FrameworkOverhead --> FixRaw["Raw SQL / Select 指定字段"]
|
|||
|
|
|
|||
|
|
style A fill:#EF4444,color:#fff
|
|||
|
|
style FixPreload fill:#3B82F6,color:#fff
|
|||
|
|
style FixPaging fill:#4FC08D,color:#fff
|
|||
|
|
style FixIndex fill:#EAB308,color:#fff
|
|||
|
|
style FixRaw fill:#8B5CF6,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## N+1 问题排查与解决
|
|||
|
|
|
|||
|
|
### 什么是 N+1?
|
|||
|
|
|
|||
|
|
N+1 是最常见的 ORM 性能杀手——为了获取关联数据,先执行一条查询拿主记录,然后对每条主记录再发起一次查询取关联数据。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ❌ N+1:查 100 个用户需要 101 条 SQL
|
|||
|
|
users := []User{}
|
|||
|
|
db.Find(&users) // SQL 1: SELECT * FROM users;
|
|||
|
|
for _, u := range users {
|
|||
|
|
db.First(&u.Profile, u.ID) // SQL 2~101: SELECT * FROM profiles WHERE user_id = ?
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 解决方案一:Preload(变成 2 条 SQL)
|
|||
|
|
var users []User
|
|||
|
|
db.Preload("Profile").Find(&users)
|
|||
|
|
// SQL 1: SELECT * FROM users;
|
|||
|
|
// SQL 2: SELECT * FROM profiles WHERE user_id IN (1,2,3,...,100);
|
|||
|
|
|
|||
|
|
// ✅ 解决方案二:Joins(变成 1 条 SQL)
|
|||
|
|
db.Joins("Profile").Find(&users)
|
|||
|
|
// SELECT users.*, profiles.* FROM users LEFT JOIN profiles ON ...;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 使用 Debug() 检测 N+1
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
db.Debug().Find(&users)
|
|||
|
|
// 输出所有生成的 SQL:
|
|||
|
|
// [info] ... [0.5ms] [rows:100] SELECT * FROM users
|
|||
|
|
// [info] ... [1.2ms] [rows:100] SELECT * FROM profiles WHERE user_id IN (...)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 如何判断是否该用 Preload 还是 Joins?
|
|||
|
|
|
|||
|
|
| 维度 | Preload | Joins |
|
|||
|
|
|------|---------|-------|
|
|||
|
|
| SQL 数量 | N+1 | 通常 1 |
|
|||
|
|
| 内存占用 | 适中 | JOIN 膨胀时较高 |
|
|||
|
|
| 控制力 | 可分别过滤关联 | 难以精细化 |
|
|||
|
|
| 推荐场景 | 大多数情况 | 只需少量关联字段的简单场景 |
|
|||
|
|
|
|||
|
|
## 预加载策略优化
|
|||
|
|
|
|||
|
|
### 按需预加载(只查需要的字段)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ❌ 加载整个 Profile,浪费带宽
|
|||
|
|
db.Preload("Profile").Find(&users)
|
|||
|
|
|
|||
|
|
// ✅ 只取需要的列
|
|||
|
|
db.Preload("Profile", "user_id, avatar_url").Find(&users)
|
|||
|
|
// SELECT * FROM users;
|
|||
|
|
// SELECT user_id, avatar_url FROM profiles WHERE user_id IN (...);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 条件预加载
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 只预加载状态为 active 的订单
|
|||
|
|
db.Preload("Orders", "status = ?", "active").Find(&users)
|
|||
|
|
|
|||
|
|
// 复杂条件
|
|||
|
|
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
|
|||
|
|
return db.Where("amount > ?", 100).Order("created_at DESC").Limit(5)
|
|||
|
|
}).Find(&users)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 嵌套预加载的深度控制
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ⚠️ 四层嵌套:User → Orders → OrderItems → Product → Category
|
|||
|
|
// SQL 数量 = 1 + 4 = 5 条,但每次 IN 列表可能非常大
|
|||
|
|
// 当每层平均有 100 条记录时 → IN 列表可能有 10^6 个值!
|
|||
|
|
|
|||
|
|
// 解决方案:分层查询而非一次性全部预加载
|
|||
|
|
func GetUserWithTopOrders(db *gorm.DB, id uint) (*User, error) {
|
|||
|
|
var user User
|
|||
|
|
if err := db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
|
|||
|
|
return db.Order("created_at DESC").Limit(10)
|
|||
|
|
}).First(&user, id).Error; err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &user, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!warning] IN 列表大小限制
|
|||
|
|
> 不同数据库对 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 := min(i+batch, len(ids))
|
|||
|
|
> chunk := ids[i:end]
|
|||
|
|
>
|
|||
|
|
> 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
|
|||
|
|
|
|||
|
|
### Select 白名单
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ❌ 加载所有字段(包括不想暴露的密码、内部标记等)
|
|||
|
|
db.Find(&users)
|
|||
|
|
|
|||
|
|
// ✅ 只选需要的字段
|
|||
|
|
db.Select("id", "name", "email").Find(&users)
|
|||
|
|
|
|||
|
|
// ✅ 甚至可以用别名做字段转换
|
|||
|
|
db.Select("id AS user_id, name AS display_name").Find(&users)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Scan 替代 Find
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 只需要统计信息,没必要实例化整个模型
|
|||
|
|
type UserCount struct {
|
|||
|
|
Count int64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
|||
|
|
PrepareStmt: true, // 开启预编译语句缓存
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 效果:相同 SQL 的第二次执行会使用 cached statement
|
|||
|
|
// 显著降低 SQL 解析和计划编译的开销
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!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 中声明索引
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type Article struct {
|
|||
|
|
ID uint `gorm:"primaryKey"`
|
|||
|
|
Title string `gorm:"size:128;not null;index:idx_title"` // 普通索引
|
|||
|
|
Status string `gorm:"size:16;not null;index:idx_status"` // 普通索引
|
|||
|
|
UserID uint `gorm:"index:idx_article_user"` // 外键索引
|
|||
|
|
CreatedAt time.Time `gorm:"index:idx_created_at"` // 时间索引
|
|||
|
|
|
|||
|
|
// 复合索引
|
|||
|
|
Slug string `gorm:"size:128;uniqueIndex:idx_slug_owner"` // 联合唯一索引
|
|||
|
|
|
|||
|
|
// 覆盖索引(PostgreSQL 支持 INCLUDE)
|
|||
|
|
ViewCount int `gorm:"index:idx_status_views,type:int"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 何时加索引
|
|||
|
|
|
|||
|
|
| 场景 | 是否需索引 | 理由 |
|
|||
|
|
|------|-----------|------|
|
|||
|
|
| WHERE 高频查询的列 | ✅ | 避免全表扫描 |
|
|||
|
|
| JOIN 关联字段 | ✅ | 外键几乎都应该有索引 |
|
|||
|
|
| ORDER BY 排序字段 | ✅ | 避免 filesort |
|
|||
|
|
| 写频率高的字段 | ❌ 谨慎 | 每个索引增加写入成本(INSERT/UPDATE 要维护索引树) |
|
|||
|
|
| 低基数列(如 gender) | ❌ | 区分度太低,优化器不走索引 |
|
|||
|
|
| LIKE '%xxx' 前通配 | ❌ | 索引失效 |
|
|||
|
|
|
|||
|
|
### 查看执行计划
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// MySQL EXPLAIN
|
|||
|
|
db.Debug().Where("status = ? AND created_at > ?", "active", cutoff).Find(&users)
|
|||
|
|
// 从日志中找到生成的 SQL,在 MySQL 客户端执行:
|
|||
|
|
// EXPLAIN SELECT * FROM users WHERE status = 'active' AND created_at > '...';
|
|||
|
|
|
|||
|
|
// 关注点:
|
|||
|
|
// - type: 应该是 range 或 ref,不能是 ALL(全表扫描)
|
|||
|
|
// - rows: 预估扫描行数,越小越好
|
|||
|
|
// - Extra: 出现 Using filesort 说明需要额外排序;Using temporary 说明用了临时表
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 事务粒度优化
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ❌ 事务范围过大 —— HTTP 请求也包在里面
|
|||
|
|
tx := db.Begin()
|
|||
|
|
defer tx.Rollback()
|
|||
|
|
|
|||
|
|
user, _ := fetchFromAPI() // 网络请求 200ms
|
|||
|
|
if err := tx.Create(&user).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
order, _ := fetchFromAnotherAPI() // 另一个网络请求 300ms
|
|||
|
|
if err := tx.Create(&order).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return tx.Commit().Error
|
|||
|
|
// 总耗时:200 + DB + 300 + DB ≈ 500ms+
|
|||
|
|
// 期间行锁一直持有,其他事务只能等待
|
|||
|
|
|
|||
|
|
// ✅ 事务范围仅包含 DB 操作
|
|||
|
|
func SaveToDB(tx *gorm.DB, user, order any) error {
|
|||
|
|
if err := tx.Create(user).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return tx.Create(order).Error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 外层只包数据库操作
|
|||
|
|
func HandleRequest() error {
|
|||
|
|
user, _ := fetchFromAPI()
|
|||
|
|
order, _ := fetchFromAnotherAPI()
|
|||
|
|
|
|||
|
|
return db.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
return SaveToDB(tx, user, order)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
// 总耗时:DB + DB ≈ 几十 ms
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] Transaction API 简洁写法
|
|||
|
|
> GORM 提供了一个闭包形式的 `Transaction`,比手动的 Begin/Rollback/Commit 更安全:
|
|||
|
|
> ```go
|
|||
|
|
> err := db.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
> // 返回非 nil 自动回滚
|
|||
|
|
> if err := tx.Create(&user).Error; err != nil {
|
|||
|
|
> return err
|
|||
|
|
> }
|
|||
|
|
> return tx.Create(&order).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
|
|||
|
|
flowchart LR
|
|||
|
|
A["性能瓶颈"] --> Q1{"SQL 数量过多?"}
|
|||
|
|
Q1 -->|是| B["用 Preload / Joins 合并"]
|
|||
|
|
Q1 -->|否| Q2{"结果集太大?"}
|
|||
|
|
Q2 -->|是| C["加 Limit / 筛选字段"]
|
|||
|
|
Q2 -->|否| Q3{"扫描行数多?"}
|
|||
|
|
Q3 -->|是| D["检查索引"]
|
|||
|
|
Q3 -->|否| Q4{"单条 SQL 太慢?"}
|
|||
|
|
Q4 -->|是| E["EXPLAIN 分析执行计划"]
|
|||
|
|
Q4 -->|否| F{"框架开销占比高?"}
|
|||
|
|
F -->|是| G["启用 PrepareStmt / 改用 Raw SQL"]
|
|||
|
|
F -->|否| H["已达标 ✓"]
|
|||
|
|
|
|||
|
|
style A fill:#EF4444,color:#fff
|
|||
|
|
style H fill:#4FC08D,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| ✅ 优化项 | 收益 | 复杂度 |
|
|||
|
|
|-----------|------|--------|
|
|||
|
|
| 消除 N+1(Preload) | ⭐⭐⭐⭐⭐ | 低 |
|
|||
|
|
| Select 白名单 | ⭐⭐⭐⭐ | 低 |
|
|||
|
|
| 添加索引 | ⭐⭐⭐⭐⭐ | 低 |
|
|||
|
|
| 缩小事务范围 | ⭐⭐⭐⭐ | 中 |
|
|||
|
|
| PrepareStmt 缓存 | ⭐⭐⭐ | 低 |
|
|||
|
|
| Raw SQL 替换热点查询 | ⭐⭐⭐⭐⭐ | 高 |
|
|||
|
|
| 批量操作替代循环 | ⭐⭐⭐⭐⭐ | 低 |
|
|||
|
|
|
|||
|
|
## 常见坑点速查
|
|||
|
|
|
|||
|
|
| 问题 | 原因 | 解决方案 |
|
|||
|
|
|------|------|---------|
|
|||
|
|
| Preload 后接口极慢 | IN 列表过大 | 限制关联数量或用 Join 替代 |
|
|||
|
|
| 查询响应不稳定 | 没有索引导致偶发全表扫描 | 用 EXPLAIN 找出缺失索引 |
|
|||
|
|
| 插入速度慢 | 逐条插入 + 事务未复用 | CreateInBatches |
|
|||
|
|
| 连接池耗尽 | 事务内调用外部服务 | 将 IO 操作移出事务 |
|
|||
|
|
| 预编译缓存泄漏 | PrepareStmt=true 但 SQL 都是随机构造的 | 只对固定模式查询开启 |
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[03-CRUD 操作]]
|
|||
|
|
- [[04-条件查询]]
|
|||
|
|
- [[05-关联查询]]
|
|||
|
|
- [[08-事务管理]]
|
|||
|
|
- [[11-批量操作]]
|