> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# select_related 与 prefetch_related 完全指南
- URL: https://blog.vercanti.com/select_related-yu-prefetch_related-wan-quan-zhi-nan/
- Published: 2026-08-28T14:34:51.000Z
- Updated: 2026-08-28T14:57:30.000Z
- Description: 一句话： 不使用预加载时，循环访问关联对象会触发 N+1 查询（每次访问都发一条 SQL）： 正确做法： 适用场景： FK / O2O 正向关联（"多"的一方查询"一"的那个对象） 等价 SQL（近似）： 适用场景： 反向关联（一对多）、M2M（多对多） 等价 SQL（近似，一对多）： 多对多等价 SQL（近似，M2M）： Prefetch 允许对预加载的关联集合添加额外的过滤、排序等： Prefetch 参数： 根据关联字段数量选择加载策略：select_related 适合 ForeignKey / OneToOne（JOIN 一张表），prefet
- Author: yellowdog
- Tags: Tortoise-orm

> 官方文档：<https://tortoise.github.io/query.html>  
> 适用版本：tortoise-orm 0.21+（2026-05-08 核实）  
> 最后更新：2026-03-05

---

## 1\. 本质区别

| 对比项  | select\_related()           | prefetch\_related()                               |
| ---- | --------------------------- | ------------------------------------------------- |
| 核心手段 | **单条 SQL，用 JOIN** 把关联表一起拉出来 | **多条 SQL**（通常 2 条起）：先查主表，再查关联表（IN / 中间表），最后在内存里组装 |
| 适用关系 | **外键 FK / OneToOne（正向）**    | **反向关系（One-to-Many 反向）/ ManyToMany / 一对多集合**      |
| 结果形态 | 每行主表记录 + 关联对象字段（重复出现）       | 主表记录不重复；关联集合在内存里挂上去                               |
| 性能特点 | JOIN 可能导致行膨胀（尤其对"多"的那侧不适用）  | 查询次数增加，但避免巨大 JOIN、避免行膨胀，适合集合关系                    |

一句话：

- **`select_related` \= JOIN（把"1"的对象带出来）**
- **`prefetch_related` \= 先主表、再批量查关联（把"多"的集合带出来）**

---

## 2\. N+1 问题

不使用预加载时，循环访问关联对象会触发 N+1 查询（每次访问都发一条 SQL）：

```python
# 反例：N+1 查询
students = await Student.all()   # 1 条 SQL
for s in students:
    clazz = await s.clazz   # N 条 SQL！每个学生各一条

```

**正确做法：**

```python
# select_related：1 条 SQL 搞定
students = await Student.all().select_related('clazz')
for s in students:
    print(s.clazz.name)   # 无额外 SQL

```

---

## 3\. select\_related 详解

### `QuerySet.select_related(*fields) -> QuerySet`

| 参数       | 说明                                |
| -------- | --------------------------------- |
| \*fields | 关联字段名；支持双下划线跨层（'clazz\_\_school'） |

**适用场景：** FK / O2O 正向关联（"多"的一方查询"一"的那个对象）

```python
# 单层
students = await Student.all().select_related('clazz')
for s in students:
    print(s.name, s.clazz.name)

# 多层（学生 → 班级 → 学校）
students = await Student.all().select_related('clazz', 'clazz__school')
for s in students:
    print(s.name, s.clazz.name, s.clazz.school.name)

# 多个 FK 字段
orders = await Order.all().select_related('user', 'product', 'user__address')

```

**等价 SQL（近似）：**

```sql
SELECT
    s.id, s.name, s.clazz_id,
    c.id AS c_id, c.name AS c_name
FROM student s
LEFT JOIN class c ON c.id = s.clazz_id;

```

---

## 4\. prefetch\_related 详解

### `QuerySet.prefetch_related(*fields) -> QuerySet`

| 参数       | 说明                      |
| -------- | ----------------------- |
| \*fields | 关联字段名（字符串）或 Prefetch 对象 |

**适用场景：** 反向关联（一对多）、M2M（多对多）

```python
# 一对多：班级 → 学生
classes = await Class.all().prefetch_related('students')
for c in classes:
    async for s in c.students:   # 不再发 SQL
        print(c.name, s.name)

# 多对多：学生 → 课程
students = await Student.all().prefetch_related('courses')
for s in students:
    courses = await s.courses.all()   # 已预加载，无额外 SQL

```

**等价 SQL（近似，一对多）：**

```sql
-- 第1条：主表
SELECT c.id, c.name FROM class c;

-- 第2条：批量查关联（IN 查询）
SELECT s.id, s.name, s.clazz_id
FROM student s
WHERE s.clazz_id IN (1, 2, 3, ...);

```

**多对多等价 SQL（近似，M2M）：**

```sql
-- 第1条：学生
SELECT id, name FROM student;

-- 第2条：中间表
SELECT student_id, course_id
FROM student_course
WHERE student_id IN (...);

-- 第3条：课程
SELECT id, title FROM course
WHERE id IN (...);

```

---

## 5\. Prefetch 对象（高级用法）

`Prefetch` 允许对预加载的关联集合添加额外的过滤、排序等：

```python
from tortoise.query_utils import Prefetch

# 带过滤：只预加载活跃学生
classes = await Class.all().prefetch_related(
    Prefetch('students', queryset=Student.filter(is_active=True))
)

# 带排序
classes = await Class.all().prefetch_related(
    Prefetch('students', queryset=Student.all().order_by('name'))
)

# 自定义挂载属性名
classes = await Class.all().prefetch_related(
    Prefetch(
        'students',
        queryset=Student.filter(is_active=True).order_by('-score'),
        to_attr='top_students'   # 挂载到 c.top_students 而非 c.students
    )
)
for c in classes:
    for s in c.top_students:   # 注意：to_attr 是普通列表，不是 RelationManager
        print(s.name)

```

**`Prefetch` 参数：**

| 参数       | 类型       | 说明                                        |
| -------- | -------- | ----------------------------------------- |
| relation | str      | 关联字段名（位置参数）                               |
| queryset | QuerySet | 自定义预加载查询                                  |
| to\_attr | str      | 结果挂载到的属性名；若指定，访问方式为普通列表而非 RelationManager |

---

## 6\. 混合使用

```python
# 同时用两种
students = await Student.all() \
    .select_related('clazz') \          # FK: 学生 → 班级
    .prefetch_related('courses')         # M2M: 学生 → 课程

# 多层预加载
classes = await Class.all() \
    .select_related('school') \          # 班级 → 学校（FK）
    .prefetch_related(
        Prefetch('students', queryset=Student.filter(is_active=True).select_related('guardian'))
    )

```

---

## 7\. 选型规则

| 情况               | 用哪个                                            |
| ---------------- | ---------------------------------------------- |
| 外键字段（FK），取"一"的一侧 | select\_related("fk\_field")                   |
| OneToOne 正向访问    | select\_related("o2o\_field")                  |
| 反向一对多（取关联集合）     | prefetch\_related("reverse\_set")              |
| 多对多（取集合）         | prefetch\_related("m2m\_field")                |
| 带过滤/排序的关联集合      | prefetch\_related(Prefetch(..., queryset=...)) |
| 深层 FK 链（3+ 层）    | select\_related("a\_\_b\_\_c")                 |

---

## 8\. 常见踩坑

### 1\. `select_related` 用于"多"的关系 → 行膨胀

```python
# 错误：班级有100个学生，班级行会重复100次
classes = await Class.all().select_related('students')  # 不要这样！
# 正确
classes = await Class.all().prefetch_related('students')

```

### 2\. 忘记 await 关联字段

```python
# 未预加载时，直接访问会报错（而不是隐式查询）
student = await Student.get(id=1)
print(student.clazz.name)   # AttributeError 或 OperationalError！

# 正确方式一：预加载
student = await Student.get(id=1).select_related('clazz')
print(student.clazz.name)   # OK

# 正确方式二：手动 await（会单独发 SQL）
clazz = await student.clazz
print(clazz.name)

```

### 3\. `prefetch_related` 后访问 M2M 集合

```python
students = await Student.all().prefetch_related('courses')
for s in students:
    # M2M 反向关系仍需 await，但不会发额外 SQL（已预加载）
    courses = await s.courses.all()
    for c in courses:
        print(c.title)

```

### 4\. `to_attr` 改变访问方式

```python
# 不用 to_attr：通过 RelationManager 访问
async for s in clazz.students:   # 或 await clazz.students.all()

# 用 to_attr：直接是列表
for s in clazz.active_students:  # 普通列表，不需要 await

```

### 5\. 深层预加载性能

```python
# 深层 prefetch 可能发很多条 SQL，注意权衡
# 通常不超过 3 层
await A.all().prefetch_related('b__c__d')   # 谨慎使用

```

---

## 最佳实践

**根据关联字段数量选择加载策略**：`select_related` 适合 ForeignKey / OneToOne（JOIN 一张表），`prefetch_related` 适合 ManyToManyField 或反向关联（多条独立 SQL）。混用两者时先跑 SQL 日志确认查询数量。

**prefetch\_related 配合 `Prefetch` 对象过滤关联**：只需关联对象的子集时，传入 `Prefetch('orders', queryset=Order.filter(status='paid'))` 避免加载全部关联数据，减少内存和网络开销。

```python
from tortoise.query_utils import Prefetch

users = await User.all().prefetch_related(
    Prefetch('orders', queryset=Order.filter(status='paid'))
)

```

**避免在循环中访问未预加载的关联**：关联字段未预加载就访问会触发 N+1 查询。在业务逻辑入口处统一预加载所有本次请求需要的关联，而不是在下游函数中按需加载。

**用 SQL 日志验证预加载效果**：开发时配置 `tortoise.contrib.test.TORTOISE_TEST_DB` 并设置日志级别为 DEBUG，确认实际 SQL 数量与预期一致，发现意外的 N+1。

**深层嵌套不超过 2 层**：`prefetch_related('team__members__profile')` 会发 3 条 SQL，且内存中需要组装多层嵌套对象。超过 2 层时考虑改用原生 SQL 或分步查询并手动组装。

---

## 常见陷阱

### 陷阱：访问未预加载关联字段抛出 `NoValuesFetched`

**现象：** 代码中访问 `user.orders` 时抛出 `tortoise.exceptions.NoValuesFetched: orders`。  
**原因：** Tortoise ORM 的关联字段是懒加载的，未经 `select_related` 或 `prefetch_related` 预加载时不能直接访问。  
**解决：** 在查询时加上对应的预加载：`await User.get(id=uid).prefetch_related('orders')`，或使用 `await user.fetch_related('orders')` 单独加载。

### 陷阱：`select_related` 在 ManyToMany 关联上不可用

**现象：** 对 M2M 字段调用 `select_related` 后关联为空或报错。  
**原因：** `select_related` 只能处理单行关联（ForeignKey/OneToOne），M2M 关联需要多行，必须用 `prefetch_related`。  
**解决：** 将所有 M2M 和反向 FK 关联改用 `prefetch_related`；只有正向 FK/O2O 用 `select_related`。

### 陷阱：`Prefetch` 过滤后循环内再次查询产生 N+1

**现象：** 虽然用了 `Prefetch`，但在模板或服务层循环中仍出现大量 SQL。  
**原因：** `Prefetch` 只缓存了符合过滤条件的关联对象；若代码中又通过 `await obj.relation.filter(...)` 重新查询，仍会发 SQL。  
**解决：** 在 Python 层对已预加载的列表做过滤（`[o for o in user.orders if o.status == 'paid']`），不要再发 SQL 查询。

---

## 参见

[查询操作完全指南](https://blog.vercanti.com/tortoise-orm-cha-xun-cao-zuo-wan-quan-zhi-nan/)  
[信号机制](https://blog.vercanti.com/xin-hao-ji-zhi/)  
[初始化与配置](https://blog.vercanti.com/tortoise-orm-chu-shi-hua-yu-pei-zhi/)