Skip to content

第16章:选择特定字段与聚合查询 ​

本章目标 ​

学完本章后,你将能够:

  1. 使用 Select 选择特定列或表达式
  2. 使用 Group + Having 进行分组过滤
  3. 使用 Count、Sum、Avg、Max、Min 进行聚合统计
  4. 将聚合结果映射到自定义结构体
  5. 理解 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

📌 WHERE vs HAVING: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 模型)

练习题 ​

  1. 写查询:只选择用户名和邮箱,不查其他字段。
  2. 统计每个城市的用户数量和平均年龄,按人数降序排列。
  3. 查询订单总金额(SUM)、平均金额(AVG)、最高金额(MAX)。
  4. 查询至少拥有 3 个用户的城市(HAVING)。
  5. 写一条 SQL(用 GORM Raw 或 Select+Scan),统计不同年龄段的用户数量(0-18 / 19-30 / 31-50 / 50+)。
  6. (思考题)什么时候应该用 Select 限制返回字段?查询所有字段有什么坏处?

把零散知识,整理成可以反复走的路。