Skip to content

第20章:查询总结与对比表 ​

本章目标 ​

学完本章后,你将能够:

  1. 快速查找到需要的方法和用法
  2. 根据场景选择正确的查询方法
  3. 了解各查询方法的性能特征
  4. 对比 GORM 和原生 SQL 的代码量

前置知识 ​

  • 需要先学习:第10-19章(所有查询相关章节)
  • 这是一个总结速查章节,覆盖 Phase 3-4 的全部内容

20.1 查询方法速查表 ​

获取记录 ​

方法SQL找不到时备注
First(&user)ORDER BY id LIMIT 1ErrRecordNotFound按主键第一条
First(&user, id)WHERE id=? LIMIT 1ErrRecordNotFound主键快捷查询
Take(&user)LIMIT 1ErrRecordNotFound不排序,任意一条
Last(&user)ORDER BY id DESC LIMIT 1ErrRecordNotFound按主键最后一条
Find(&users)SELECT *nil,空切片所有记录
Find(&users, ids)WHERE id IN ?nil,空切片IN 查询

条件查询 ​

方法示例
Where(string)db.Where("name = ?", "张三")
Where(struct)db.Where(&User{Name:"张三"}) ⚠️ 零值跳过
Where(map)db.Where(map[string]interface{}{"name":"张三","age":0}) ✅ 零值不跳过
Not()db.Not("age = ?", 25)
Or()db.Where("a=1").Or("b=2")
Where().Where()AND 链式组合

排序与分页 ​

方法示例
Order("col desc")db.Order("age desc, name asc")
Limit(n)db.Limit(10)
Offset(n)db.Offset(20)
Limit(-1)取消之前的 Limit

字段选择与聚合 ​

方法示例
Select("a", "b")db.Select("name, age").Find(&users)
Select("SUM(x)")db.Select("AVG(age)").Scan(&avg)
Group("col")db.Group("city").Find(&results)
Having("cond")db.Having("COUNT(*) > ?", 5)
Distinct("col")db.Distinct("status").Pluck("status", &list)

Join ​

方法示例
Joins("LEFT JOIN ...")db.Joins("LEFT JOIN c ON u.cid = c.id")
Joins("Relation")db.Joins("Company") 利用关联定义
InnerJoins("Relation")db.InnerJoins("Company") 内连接

其他 ​

方法示例
Count(&n)db.Model(&User{}).Count(&total)
Pluck("col", &slice)db.Pluck("name", &names)
Scan(&struct)db.Raw("...").Scan(&custom)
Scopes(s1, s2)db.Scopes(ActiveUsers, Paginate(1,10))

20.2 方法选择决策树 ​

你想做什么?
│
├─ 查一条记录
│   ├─ 知道主键 → db.First(&user, id)  ← 最简洁
│   ├─ 知道条件 → db.Where(...).First(&user)
│   └─ 任意一条  → db.Take(&user)
│
├─ 查多条记录
│   ├─ 查全部     → db.Find(&users)
│   ├─ 带条件     → db.Where(...).Find(&users)
│   ├─ 需要分页   → db.Scopes(Paginate(1,10)).Find(&users)
│   ├─ 需要排序   → db.Order("col desc").Find(&users)
│   └─ 需要分组   → db.Select("col, COUNT(*)").Group("col").Scan(&result)
│
├─ 只查一列
│   └─ db.Pluck("col", &slice)
│
├─ 只计数
│   └─ db.Model(&User{}).Count(&total)
│
├─ 多表关联
│   ├─ 需要过滤关联表 → db.Joins("Relation").Where(...).Find(&users)
│   └─ 纯展示关联数据 → db.Preload("Relation").Find(&users)  (Phase 5)
│
└─ GORM API 不够用
    └─ db.Raw("SQL").Scan(&result)

20.3 查询性能参考 ​

查询类型数据量期望时间优化建议
主键查询任意< 1ms已是最优
WHERE 索引列100万< 5ms确保索引覆盖
WHERE 无索引列10万> 100ms添加索引
LIKE '%x%'100万> 1s考虑全文索引
OFFSET 10000 LIMIT 10100万100ms+改用游标分页
JOIN 2-3表(有索引)10万10-50ms确保 JOIN 列有索引
GROUP BY(有索引)100万50-500ms考虑汇总表
COUNT(*)100万取决于引擎MyISAM 快(精确计数),InnoDB 慢(扫描索引)

20.4 GORM vs 原生 SQL 代码量对比 ​

简单查询 ​

go
// 原生 SQL(database/sql)
rows, _ := db.Query("SELECT id, name, age, email, created_at, updated_at FROM users WHERE age > ?", 18)
defer rows.Close()
var users []User
for rows.Next() {
	var u User
	rows.Scan(&u.ID, &u.Name, &u.Age, &u.Email, &u.CreatedAt, &u.UpdatedAt)
	users = append(users, u)
}

// GORM(等价)
db.Where("age > ?", 18).Find(&users)
// 9行 → 1行

带分页的查询 ​

go
// 原生 SQL
var total int64
db.QueryRow("SELECT COUNT(*) FROM users WHERE age > ?", 18).Scan(&total)
rows, _ := db.Query("SELECT * FROM users WHERE age > ? LIMIT 10 OFFSET 20", 18)
defer rows.Close()
// ...扫描...

// GORM(等价)
db.Model(&User{}).Where("age > ?", 18).Count(&total)
db.Where("age > ?", 18).Limit(10).Offset(20).Find(&users)

复杂关联查询 ​

go
// 原生 SQL
rows, _ := db.Query(`
	SELECT u.id, u.name, COUNT(o.id) as order_count, SUM(o.amount) as total
	FROM users u
	LEFT JOIN orders o ON u.id = o.user_id
	WHERE u.created_at > ?
	GROUP BY u.id
	HAVING order_count > 0
	ORDER BY total DESC
	LIMIT 10
`, startDate)
defer rows.Close()
// ...定义结构体,手动扫描...

// GORM(等价)
type UserStats struct {
	ID         uint
	Name       string
	OrderCount int64
	Total      float64
}
var stats []UserStats
db.Model(&User{}).Select(`
	users.id, users.name,
	COUNT(orders.id) as order_count,
	COALESCE(SUM(orders.amount), 0) as total
`).
Joins("LEFT JOIN orders ON users.id = orders.user_id").
Where("users.created_at > ?", startDate).
Group("users.id").
Having("order_count > 0").
Order("total DESC").
Limit(10).
Scan(&stats)

20.5 常见查询模式汇总 ​

模式1:分页查询(最常用) ​

go
func ListUsers(db *gorm.DB, page, pageSize int, filters map[string]interface{}) ([]User, int64, error) {
	var users []User
	var total int64

	query := db.Model(&User{})
	for k, v := range filters {
		query = query.Where(k+" = ?", v)
	}

	query.Count(&total)
	err := query.Scopes(Paginate(page, pageSize)).Order("id desc").Find(&users).Error
	return users, total, err
}

模式2:条件筛选 ​

go
func SearchUsers(db *gorm.DB, req SearchRequest) ([]User, error) {
	var users []User
	query := db.Model(&User{})

	if req.Keyword != "" {
		query = query.Where("name LIKE ? OR email LIKE ?",
			"%"+req.Keyword+"%", "%"+req.Keyword+"%")
	}
	if req.Status != "" {
		query = query.Where("status = ?", req.Status)
	}
	if req.MinAge > 0 {
		query = query.Where("age >= ?", req.MinAge)
	}

	err := query.Find(&users).Error
	return users, err
}

模式3:聚合统计 ​

go
type StatsResult struct {
	Label string
	Count int64
}

func CountByStatus(db *gorm.DB) []StatsResult {
	var results []StatsResult
	db.Model(&User{}).Select("status as label, COUNT(*) as count").
		Group("status").Scan(&results)
	return results
}

模式4:关联查询 ​

go
func GetUserWithCompany(db *gorm.DB, userID uint) (*UserWithCompany, error) {
	var result UserWithCompany
	err := db.Model(&User{}).
		Select("users.*, companies.name as company_name").
		Joins("LEFT JOIN companies ON users.company_id = companies.id").
		Where("users.id = ?", userID).
		Scan(&result).Error
	return &result, err
}

本章小结 ​

本章是 Phase 4 的终点站。回顾一下你已经掌握的查询技能:

  • ✅ 单条查询:First、Take、Last、Find
  • ✅ 条件查询:Where(字符串/结构体/Map)、Not、Or
  • ✅ 排序分页:Order、Limit、Offset
  • ✅ 字段选择:Select、Omit
  • ✅ 聚合统计:Group、Having、Count、Sum、Avg、Max、Min
  • ✅ 连接查询:Joins、InnerJoins
  • ✅ 子查询:WHERE/ FROM/ SELECT 中的子查询
  • ✅ Scope 封装:可复用的查询逻辑
  • ✅ 原生 SQL:Raw + Scan
  • ✅ 调试工具:DryRun、Debug、Logger

从下一章开始,进入 Phase 5——GORM 最强大的功能:关联关系。

练习题 ​

  1. 不用翻书,把上面 20.1 的速查表默写一遍(或整理到你的笔记中)。
  2. 回答:查一条记录用 First 还是 Find?查不到时行为有何不同?
  3. 回答:Where(struct) 和 Where(map) 有什么区别?为什么?
  4. 写一个综合查询:按状态筛选、按年龄排序、分页展示、返回总数。
  5. (思考题)GORM 的链式调用和 SQL 的语法有哪些相似之处?把 GORM 的链式 API 翻译成 SQL 的思维过程是怎样的?

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