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_relatedprefetch_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 查询。


参见

查询操作完全指南
信号机制
初始化与配置

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog