第16章:选择特定字段与聚合查询
本章目标
学完本章后,你将能够:
- 使用
Select选择特定列或表达式 - 使用
Group+Having进行分组过滤 - 使用
Count、Sum、Avg、Max、Min进行聚合统计 - 将聚合结果映射到自定义结构体
- 理解
Select字段不匹配的常见问题
前置知识
- 需要先学习:第11-15章(查询基础)
- 需要了解:MySQL 的 SELECT、GROUP BY、聚合函数
16.1 Select:选择特定字段
选择指定列
go
// 默认查询所有列
db.Find(&users) // SELECT * FROM `users`
// 只选择特定列(减少数据传输量)
db.Select("name", "age").Find(&users)
// SQL: SELECT `name`,`age` FROM `users`
// 结果:user 结构体中只有 Name 和 Age 有值,其他字段为零值选择表达式
go
// 使用 SQL 表达式
db.Select("name, age * 2 as double_age").Find(&users)
// SQL: SELECT name, age * 2 as double_age FROM `users`
// 数据库函数
db.Select("name, TIMESTAMPDIFF(YEAR, birthday, NOW()) as age_from_birthday").Find(&users)
// COALESCE:如果 name 为 NULL,则用 '匿名' 替代
db.Select("COALESCE(name, '匿名') as display_name").Find(&users)Select + Scan:灵活组合
go
// Select 的列和 Scan 的目标结构体不需要匹配模型
type UserStat struct {
Name string
Count int64
}
var stats []UserStat
db.Model(&User{}).Select("name, COUNT(*) as count").Group("name").Scan(&stats)
// SQL: SELECT name, COUNT(*) as count FROM `users` GROUP BY `name`16.2 Group + Having:分组过滤
基本分组
go
// 按城市分组
type CityCount struct {
City string
Count int64
}
var results []CityCount
db.Model(&User{}).Select("city, COUNT(*) as count").Group("city").Scan(&results)
// SQL: SELECT city, COUNT(*) as count FROM `users` GROUP BY `city`Having:分组后的过滤
go
// 查询人数超过 5 的城市
db.Model(&User{}).
Select("city, COUNT(*) as count").
Group("city").
Having("COUNT(*) > ?", 5).
Scan(&results)
// SQL:
// SELECT city, COUNT(*) as count FROM `users`
// GROUP BY `city`
// HAVING COUNT(*) > 5📌
WHEREvsHAVING:WHERE 在分组前过滤行,HAVING 在分组后过滤组。WHERE不能使用聚合函数,HAVING可以。
go
// WHERE:过滤原始行
// HAVING:过滤分组结果
db.Model(&User{}).
Select("city, COUNT(*) as count").
Where("age >= ?", 18). // 先过滤成年用户
Group("city").
Having("COUNT(*) > ?", 5). // 再过滤人数超过 5 的城市
Scan(&results)16.3 聚合函数
Count:计数
go
var count int64
// 总行数
db.Model(&User{}).Count(&count)
// 条件计数
db.Model(&User{}).Where("age > ?", 18).Count(&count)
// 分组计数
db.Model(&User{}).Select("status, COUNT(*)").Group("status").Scan(&results)Sum:求和
go
// GORM 没有直接的 Sum() 方法,用 Select + Scan
type SumResult struct {
Total float64
}
var result SumResult
db.Model(&Order{}).Select("SUM(amount) as total").Scan(&result)
// SQL: SELECT SUM(amount) as total FROM `orders`
// 分组求和
db.Model(&Order{}).
Select("user_id, SUM(amount) as total_amount").
Group("user_id").
Scan(&results)Avg:平均值
go
var avg float64
db.Model(&User{}).Select("AVG(age)").Scan(&avg)
// 或者用 Pluck
var ages []float64
db.Model(&User{}).Select("AVG(age)").Pluck("AVG(age)", &ages)
fmt.Println("平均年龄:", ages[0])Max / Min:最大 / 最小值
go
// 最大年龄
var maxAge int
db.Model(&User{}).Select("MAX(age)").Scan(&maxAge)
// 最小年龄
var minAge int
db.Model(&User{}).Select("MIN(age)").Scan(&minAge)
// 分组最大/最小
type GroupMax struct {
City string
MaxAge int
MinAge int
}
var groupResults []GroupMax
db.Model(&User{}).
Select("city, MAX(age) as max_age, MIN(age) as min_age").
Group("city").
Scan(&groupResults)16.4 完整聚合查询示例
用户统计仪表板
go
// 一次查询获取多维度的统计信息
type DashboardStats struct {
TotalUsers int64
ActiveUsers int64
AvgAge float64
MaxAge int
MinAge int
}
func GetDashboardStats(db *gorm.DB) (*DashboardStats, error) {
var stats DashboardStats
// 方式一:一条复杂 SQL
db.Model(&User{}).Select(`
COUNT(*) as total_users,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_users,
AVG(age) as avg_age,
MAX(age) as max_age,
MIN(age) as min_age
`).Scan(&stats)
return &stats, nil
}每城市用户统计
go
type CityStats struct {
City string
UserCount int64
AvgAge float64
}
var cityStats []CityStats
db.Model(&User{}).
Select("city, COUNT(*) as user_count, AVG(age) as avg_age").
Group("city").
Order("user_count desc").
Limit(10).
Scan(&cityStats)
// 输出:用户最多的前 10 个城市16.5 Select 常见问题
字段不匹配
go
// ❌ Select 了 name 和 age,但结果存在 User 结构体里
// User 的其他字段(Email、Phone)为零值
var users []User
db.Select("name", "age").Find(&users)
// users[0].Email = ""(没查,自然是零值)
// ✅ 如果只查部分字段,用自定义结构体 Scan
type NameAge struct {
Name string
Age int
}
var nameAges []NameAge
db.Model(&User{}).Select("name", "age").Scan(&nameAges)Select 和 Find 的类型不匹配
go
// ❌ Select 返回 double_age,但 User 结构体没有这个字段
var users []User
db.Select("name, age * 2 as double_age").Find(&users)
// double_age 被忽略
// ✅ 用自定义结构体
type DoubleAgeResult struct {
Name string
DoubleAge int
}
var results []DoubleAgeResult
db.Model(&User{}).Select("name, age * 2 as double_age").Scan(&results)常见错误
错误1:Group 后选了非分组字段
go
// ❌ MySQL 严格模式(ONLY_FULL_GROUP_BY)下会报错
db.Model(&User{}).Select("name, city, COUNT(*)").Group("city")
// name 不在 GROUP BY 中,也不是聚合函数参数,MySQL 不知道选哪个 name
// ✅ 只选分组字段和聚合结果
db.Model(&User{}).Select("city, COUNT(*)").Group("city")错误2:Having 用了 WHERE 的写法
go
// ❌ 在 Having 中用原始列条件
db.Model(&User{}).Group("city").Having("age > ?", 18)
// Having 中只能用聚合函数或分组字段
// ✅ 原始条件用 Where
db.Model(&User{}).Where("age > ?", 18).Group("city").Having("COUNT(*) > ?", 5)错误3:聚合结果类型不匹配
go
// ❌ AVG 返回 float,用 int 接收会截断
var avgAge int
db.Model(&User{}).Select("AVG(age)").Scan(&avgAge)
// 25.7 → 25(小数丢失)
// ✅ 用 float64
var avgAge float64
db.Model(&User{}).Select("AVG(age)").Scan(&avgAge)本章小结
Select("col1", "col2")只查需要的列,减少数据传输Select()+Scan()是聚合查询的标准组合Group("col")+Having("cond")分组过滤Count/Sum/Avg/Max/Min五大聚合函数- 聚合结果用自定义结构体接收(不用 GORM 模型)
练习题
- 写查询:只选择用户名和邮箱,不查其他字段。
- 统计每个城市的用户数量和平均年龄,按人数降序排列。
- 查询订单总金额(SUM)、平均金额(AVG)、最高金额(MAX)。
- 查询至少拥有 3 个用户的城市(HAVING)。
- 写一条 SQL(用 GORM Raw 或 Select+Scan),统计不同年龄段的用户数量(0-18 / 19-30 / 31-50 / 50+)。
- (思考题)什么时候应该用
Select限制返回字段?查询所有字段有什么坏处?