--- tags: [GORM, Go, ORM, 子查询, Group, Having, SubQuery, 聚合] create time: 2026-04-28 00:00 --- # 子查询与分组 ## 概述 当简单的单表查询无法满足需求时,就需要用到**子查询**(SQL 里嵌套的 SELECT)和**分组聚合**(GROUP BY + HAVING)。这两类查询通常用于统计分析——比如「找出每个部门收入最高的员工」或「统计过去一个月每天的新增订单数」。 ```mermaid flowchart TD Start[复杂查询需求] --> Type{哪种场景_} Type --> |按维度汇总| GroupBy["GROUP BY + 聚合函数
COUNT / SUM / AVG / MAX / MIN"] Type --> |过滤聚合结果| Having["HAVING 条件
替代 WHERE"] Type --> |嵌套查询| SubQ{"子查询类型?"} SubQ --> |IN/EXISTS 过滤| FilterSub["WHERE field IN (SELECT ...)"] SubQ --> |关联计算| Correlated["相关子查询
外层行影响内层查询"] SubQ --> |派生表| DerivedTable["FROM (SELECT ...) AS t
将子查询作为临时表"] 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` 实现分组: ```go // 按部门统计人数 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; ``` ### 多字段分组 ```go // 按部门和月份统计活跃用户数 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` 中显式声明: > ```go > // ❌ 错误 —— 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`: ```go // 找平均薪资大于 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`),也可以直接写字符串: > ```go > db.Model(&User{}).Group("dept_id").Having("COUNT(*) > ?", 5).Find(&results) > ``` > 多个 `Having` 会自动用 AND 连接。 ## 子查询 GORM 提供了几种方式来构建子查询。 ### 派生表子查询(From Subquery) 将子查询作为一个临时表放入 FROM 子句: ```go // 找出年龄大于全体员工平均年龄的用户 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 子查询 ```go // 找到有订单的用户 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 子查询 ```go // 找到至少有 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) 内层查询引用外层表的列,每一行都重新执行一次内层查询: ```go // 对每个用户,查出他的最新订单 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: > ```go > // 改写为高效 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) > ``` ## 实用统计模式 ### 按月趋势统计 ```go 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 问题 ```go // 找出消费金额最高的前 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 条件聚合 ```go // 按状态统计订单数量 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) ``` ## 子查询决策图 ```mermaid 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+ | ## 关联笔记 - [[03-CRUD 操作]] - [[04-条件查询]] - [[05-关联查询]] - [[06-排序与分页]] - [[15-性能优化]]