写 GORM 代码时,能跑通只是第一步。当数据量上来后,一条慢查询就能拖垮整个服务。这一章聊聊查询优化的实战经验。
最常见的坑就是 SELECT *。GORM 默认查询所有字段,但很多时候你只需要几个字段:
// 不好的做法
db.Find(&users)
// 好的做法:只查需要的字段
db.Select("id", "name", "email").Find(&users)
// 单条记录也一样
db.Select("id", "status").First(&user, 1)
字段越多,数据库要读的数据页就越多,网络传输也越大。特别是有 TEXT、BLOB 类型字段时,影响更明显。
除了显式指定字段,还要注意关联查询时的隐式全字段查询:
// 预加载时会查询所有字段
db.Preload("Orders").Find(&users)
// 可以给 Preload 指定字段
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Select("id", "user_id", "amount")
}).Find(&users)
分页查询看起来简单,但 OFFSET 在大数据量时性能很差:
// 小数据量没问题
db.Offset(10).Limit(10).Find(&users)
// 大数据量时,OFFSET 100000 会扫描前 100010 条记录
db.Offset(100000).Limit(10).Find(&users) // 慢!
更好的做法是用 WHERE 替代 OFFSET:
// 记住上一页最后一条的 ID
lastID := 100000
db.Where("id > ?", lastID).Limit(10).Find(&users)
这种方式不管翻到第几页,查询复杂度都是 O(1)。
如果查询的字段都在索引上,数据库直接从索引读取,不用回表:
// 假设有索引 idx_user_status (status, name)
type User struct {
ID uint
Name string `gorm:"index:idx_user_status"`
Status string `gorm:"index:idx_user_status"`
Email string
}
// 这个查询可以用索引覆盖
db.Select("id", "name", "status").Where("status = ?", "active").Find(&users)
// 这个用不了索引
db.Where("name LIKE ?", "%张%").Find(&users)
// 前缀匹配可以用索引
db.Where("name LIKE ?", "张%").Find(&users)
如果确实需要全文搜索,考虑用专门的搜索引擎,或者数据库的全文索引功能。
GORM 可以直接打印 SQL,配合数据库的 EXPLAIN 分析:
// 打印 SQL
db.Debug().Where("status = ?", "active").Find(&users)
// 或者用 Raw 执行 EXPLAIN
var result []map[string]interface{}
db.Raw("EXPLAIN SELECT * FROM users WHERE status = ?", "active").Scan(&result)
关注 EXPLAIN 输出的 type、key、rows、Extra 字段,type 为 ALL 说明全表扫描,需要加索引。
Count 查询在大表上很慢,尤其是带条件的 Count:
// 全表 Count 很慢
var count int64
db.Model(&User{}).Count(&count)
// 带复杂条件的 Count 更慢
db.Model(&User{}).Where("status = ? AND created_at > ?", "active", lastWeek).Count(&count)
一些优化思路:
经典的 N+1 问题:
// 糟糕的做法
for _, order := range orders {
db.First(&user, order.UserID)
}
应该用 IN 查询:
// 收集所有 UserID
userIDs := make([]uint, len(orders))
for i, order := range orders {
userIDs[i] = order.UserID
}
// 一次查询
var users []User
db.Where("id IN ?", userIDs).Find(&users)
或者用预加载:
db.Preload("User").Find(&orders)
查询前想清楚需要多少数据:
// 检查是否存在,用 Limit 1
var exists int64
db.Model(&User{}).Where("email = ?", email).Limit(1).Count(&exists)
// 或者用 First 的变体
var user User
err := db.Where("email = ?", email).Select("id").First(&user).Error
exists := err == nil
查询优化的核心原则:
这些优化不需要等出问题再做,养成习惯,代码质量自然就上去了。