select_related 与 prefetch_related 完全指南
一句话: 不使用预加载时,循环访问关联对象会触发 N+1 查询(每次访问都发一条 SQL): 正确做法: 适用场景: FK / O2O 正向关联("多"的一方查询"一"的那个对象) 等价 SQL(近似): 适用场景: 反向关联(一对多)、M2M(多对多) 等价 SQL(近似,一对多): 多对多等价 SQL(近似,M2M): Prefetch 允许对预加载的关联集合添加额外的过滤、排序等: Prefetch 参数: 根据关联字段数量选择加载策略:select_related 适合 ForeignKey / OneToOne(JOIN 一张表),prefet
官方文档: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):
# 反例:N+1 查询
students = await Student.all() # 1 条 SQL
for s in students:
clazz = await s.clazz # N 条 SQL!每个学生各一条
正确做法:
# 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 正向关联("多"的一方查询"一"的那个对象)
# 单层
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(近似):
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(多对多)
# 一对多:班级 → 学生
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(近似,一对多):
-- 第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):
-- 第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 允许对预加载的关联集合添加额外的过滤、排序等:
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. 混合使用
# 同时用两种
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 用于"多"的关系 → 行膨胀
# 错误:班级有100个学生,班级行会重复100次
classes = await Class.all().select_related('students') # 不要这样!
# 正确
classes = await Class.all().prefetch_related('students')
2. 忘记 await 关联字段
# 未预加载时,直接访问会报错(而不是隐式查询)
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 集合
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 改变访问方式
# 不用 to_attr:通过 RelationManager 访问
async for s in clazz.students: # 或 await clazz.students.all()
# 用 to_attr:直接是列表
for s in clazz.active_students: # 普通列表,不需要 await
5. 深层预加载性能
# 深层 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')) 避免加载全部关联数据,减少内存和网络开销。
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 查询。