291 lines
9.4 KiB
Markdown
291 lines
9.4 KiB
Markdown
|
|
---
|
|||
|
|
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 + 聚合函数<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` 实现分组:
|
|||
|
|
|
|||
|
|
```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-性能优化]]
|