This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/07-子查询与分组.md
T
2026-04-28 20:23:33 +08:00

9.4 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
子查询
Group
Having
SubQuery
聚合
2026-04-28 00:00

子查询与分组

概述

当简单的单表查询无法满足需求时,就需要用到子查询(SQL 里嵌套的 SELECT)和分组聚合(GROUP BY + HAVING)。这两类查询通常用于统计分析——比如「找出每个部门收入最高的员工」或「统计过去一个月每天的新增订单数」。

flowchart TD
    Start[复杂查询需求] --> Type{哪种场景_}
    
    Type --> |按维度汇总| GroupBy["GROUP BY + 聚合函数<br/>COUNT / SUM / AVG / MAX / MIN"]
    Type --> |过滤聚合结果| Having["HAVING 条件<br/>替代 WHERE"]
    Type --> |嵌套查询| SubQ{"子查询类型?"}
    SubQ --> |IN/EXISTS 过滤| FilterSub["WHERE field IN (SELECT ...)"]
    SubQ --> |关联计算| Correlated["相关子查询<br/>外层行影响内层查询"]
    SubQ --> |派生表| DerivedTable["FROM (SELECT ...) AS t<br/>将子查询作为临时表"]
    
    style Start fill:#4FC08D,color:#fff
    style GroupBy fill:#3B82F6,color:#fff
    style Having fill:#F59E0B,color:#000
    style DerivedTable fill:#EC4899,color:#fff

GROUP BY — 分组统计

基本用法

GORM 通过 Select + Group 实现分组:

// 按部门统计人数
type DeptStats struct {
    DeptID   uint   `gorm:"column:dept_id"`
    DeptName string `gorm:"column:dept_name"`
    Count    int64  `gorm:"column:count"`
}

var stats []DeptStats
db.Model(&User{}).
    Select("dept_id, dept_name, COUNT(*) as count").
    Group("dept_id").
    Find(&stats)
// SELECT dept_id, dept_name, COUNT(*) as count 
// FROM users GROUP BY dept_id;

多字段分组

// 按部门和月份统计活跃用户数
type MonthlyDeptStats struct {
    DeptID     uint      `gorm:"column:dept_id"`
    Month      string    `gorm:"column:month"`
    ActiveCount int64    `gorm:"column:active_count"`
}

db.Model(&User{}).
    Select("dept_id, DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as active_count").
    Group("dept_id, DATE_FORMAT(created_at, '%Y-%m')").
    Order("month DESC").
    Scan(&monthlyStats)

[!tip] Group 的陷阱 GORM 在调用 Group 后默认不再添加任何其他字段到 SELECT——这是 SQL 标准要求。如果需要额外字段,必须在 Select 中显式声明:

// ❌ 错误 —— GORM 可能报错或产生不确定的 SELECT
db.Model(&User{}).Group("dept_id").Find(&users)

// ✅ 正确 —— 明确指定需要的字段
db.Model(&User{}).Select("dept_id, MAX(age)").Group("dept_id").Scan(&stats)

HAVING — 过滤分组结果

WHERE 过滤的是行级别,HAVING 过滤的是分组后的聚合结果。两者的执行顺序是 WHERE → GROUP BY → HAVING:

// 找平均薪资大于 10000 的部门
type AvgDept struct {
    DeptID uint    `gorm:"column:dept_id"`
    AvgSal float64 `gorm:"column:avg_salary"`
}

db.Model(&User{}).
    Select("dept_id, AVG(salary) as avg_salary").
    Group("dept_id").
    Having("AVG(salary) > ?", 10000).
    Scan(&result)

// 组合使用 WHERE 和 HAVING
// 先过滤入职满 1 年的员工,再按部门分组,最后筛选平均薪资超过 10000 的部门
db.Model(&User{}).
    Select("dept_id, COUNT(*) as headcount, AVG(salary) as avg_salary").
    Where("created_at < ?", time.Now().AddDate(-1, 0, 0)).
    Group("dept_id").
    Having("COUNT(*) >= 3").                    // 至少 3 人
    Having("AVG(salary) > ?", 10000).           // 且平均薪资超过 10000
    Scan(&qualifiedDepts)

[!example] WHERE vs HAVING 的核心区别

特性 WHERE HAVING
作用对象 单个行记录 分组后的聚合结果
能否用聚合函数 ❌ 不能 ✅ 可以
执行时机 GROUP BY 之前 GROUP BY 之后
性能差异 更优(提前过滤减少分组数据量) 较次(需先完成分组再过滤)

[!important] HAVING 的简洁写法 如果你只需要最简单的 HAVING 条件(如 HAVING COUNT(*) > 5),也可以直接写字符串:

db.Model(&User{}).Group("dept_id").Having("COUNT(*) > ?", 5).Find(&results)

多个 Having 会自动用 AND 连接。

子查询

GORM 提供了几种方式来构建子查询。

派生表子查询(From Subquery)

将子查询作为一个临时表放入 FROM 子句:

// 找出年龄大于全体员工平均年龄的用户
avgAgeSubQuery := db.Table("users").Select("AVG(age)")

var users []User
db.Where("age > (?)", avgAgeSubQuery).Find(&users)
// SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users);

IN 子查询

// 找到有订单的用户
orderSubQuery := db.Model(&Order{}).Select("DISTINCT user_id")

var users []User
db.Where("id IN (?)", orderSubQuery).Find(&users)
// SELECT * FROM users WHERE id IN (SELECT DISTINCT user_id FROM orders);

[!tip] 为什么用 DISTINCT? 一个用户可能有多个订单,如果不加 DISTINCT,子查询会返回重复的 user_id。虽然 IN (...) 能自动去重,但加上 DISTINCT 可以让数据库优化器更高效地处理。

EXISTS 子查询

// 找到至少有 1 个未支付订单的用户
db.Where("EXISTS (?)", 
    db.Model(&Order{}).Select("id").
        Where("user_id = users.id AND status = 'pending'"),
).Find(&users)
// SELECT * FROM users 
// WHERE EXISTS (SELECT id FROM orders WHERE user_id = users.id AND status = 'pending');

[!note] EXISTS vs IN 的选择

  • 外表大、内表小 → 用 IN,子查询先查好再用
  • 外表小、内表大 → 用 EXISTS,找到一条就停止扫描
  • 在实际生产中,MySQL 优化器通常能自动选择最优方案,不用太纠结

相关子查询(Correlated Subquery)

内层查询引用外层表的列,每一行都重新执行一次内层查询:

// 对每个用户,查出他的最新订单
var users []User
db.Select(`*,
    (SELECT created_at FROM orders 
     WHERE user_id = users.id 
     ORDER BY created_at DESC LIMIT 1) as latest_order_date`).
    Find(&users)

[!warning] 相关子查询的性能警告 相关子查询每处理一行外层数据就要执行一次内层查询——复杂度接近 O(n × m)。如果数据量大,建议改写为 JOIN + GROUP BY:

// 改写为高效 JOIN:
db.Table("users u").
    Select("u.*, o.created_at as latest_order_date").
    Joins("JOIN orders o ON o.id = (SELECT id FROM orders WHERE user_id = u.id ORDER BY created_at DESC LIMIT 1)").
    Find(&users)

实用统计模式

按月趋势统计

type DailyOrderStat struct {
    Day       string  `gorm:"column:day"`
    OrderNum  int64   `gorm:"column:order_num"`
    TotalAmt  float64 `gorm:"column:total_amt"`
}

db.Model(&Order{}).
    Select("DATE(created_at) as day, COUNT(*) as order_num, COALESCE(SUM(amount), 0) as total_amt").
    Group("DATE(created_at)").
    Order("day ASC").
    Scan(&dailyStats)

TOP N 问题

// 找出消费金额最高的前 10 名用户
var topUsers []struct {
    UserID uint
    UserName string
    TotalSpent float64
}

db.Table("users u").
    Select("u.id as user_id, u.name as user_name, SUM(o.amount) as total_spent").
    Joins("JOIN orders o ON o.user_id = u.id").
    Group("u.id, u.name").
    Order("total_spent DESC").
    Limit(10).
    Scan(&topUsers)

CASE WHEN 条件聚合

// 按状态统计订单数量
type StatusCount struct {
    Pending   int64 `gorm:"column:pending"`
    Completed int64 `gorm:"column:completed"`
    Cancelled int64 `gorm:"column:cancelled"`
}

var sc StatusCount
db.Raw(`
    SELECT 
        SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
        SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
        SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled
    FROM orders
`).Scan(&sc)

子查询决策图

flowchart TD
    Start[需要子查询] --> Pattern{查询模式_}
    
    Pattern --> |聚合统计| GroupByQ{是否要过滤聚合结果?}
    GroupByQ --> |不需要| BasicGroup["GROUP BY + Select 聚合函数"]
    GroupByQ --> |需要| HavingCheck["GROUP BY + HAVING"]
    
    Pattern --> |条件过滤| FilterType{"IN 还是 EXISTS?"}
    FilterType --> |精确匹配值集合| InSub["WHERE col IN (SELECT ...)"]
    FilterType --> |存在性判断| ExistsSub["WHERE EXISTS (SELECT ...)"]
    
    Pattern --> |数据源嵌套| FromSub["FROM (SELECT ...) AS alias"]
    
    style Start fill:#4FC08D,color:#fff
    style HavingCheck fill:#3B82F6,color:#fff
    style InSub fill:#F59E0B,color:#000
    style FromSub fill:#EC4899,color:#fff

常见坑点速查

问题 原因 解决方案
Group 后字段丢失 未在 Select 中声明所需字段 显式列出所有 SELECT 字段
Having 写了聚合函数但被 WHERE 过滤 WHERE 在 GROUP BY 之前执行,无法访问聚合 把聚合条件移到 Having
子查询导致笛卡尔积 JOIN 没有正确的关联条件 检查外键关联,或用 EXISTS 替代
ORDER BY + GROUP BY 顺序搞反 SQL 语法要求 GROUP BY 在前 按 WHERE → GROUP BY → HAVING → ORDER BY 顺序写
MySQL 5.7 ONLY_FULL_GROUP_BY 非聚合字段不能在 SELECT 中出现 开启 SQL 严格模式或使用 MySQL 8.0+

关联笔记