Django 中 select_related 和 prefetch_related 有什么区别?
简化版
select_related 用 SQL JOIN 一次查出外键或一对一关联对象,适合 ForeignKey / OneToOneField。prefetch_related 会额外发查询,再由 Django 在 Python 层组装结果,适合 ManyToMany、反向外键和多值关系。
详细版
两者都是为了解决 N+1 查询问题,但适用关系不同:
| 方法 | 查询方式 | 适合关系 | 特点 |
|---|---|---|---|
select_related | SQL JOIN | 外键、一对一 | 一条 SQL 查主表和关联表,适合单值关系 |
prefetch_related | 多条 SQL + Python 组装 | 多对多、反向外键,也可用于外键 | 避免 JOIN 导致行数膨胀,适合多值关系 |
例子:
# Article -> Author 是 ForeignKey
articles = Article.objects.select_related("author")
# Author -> Article 是反向一对多
authors = Author.objects.prefetch_related("article_set")
如果列表页展示 20 篇文章和作者名,不优化时可能是 1 次查文章 + 20 次查作者;用 select_related("author") 后通常一条 JOIN 查询就够。
完整版教学
一、先理解 N+1 查询
N+1 查询是 ORM 里最常见的性能坑。比如:
articles = Article.objects.all()
for article in articles:
print(article.author.name)
第一行查询文章列表,是 1 次查询。循环里每访问一次 article.author,如果 author 没有提前加载,就可能再查一次数据库。假设有 100 篇文章,就可能变成 101 次查询。
N+1 的问题不是 SQL 复杂,而是数据库往返次数太多。每次查询都有网络、连接、解析、执行开销,高并发下很容易放大。
二、select_related:用 JOIN 解决单值关系
select_related 适合外键和一对一,因为每条主表记录最多对应一条关联记录。JOIN 后结果行数不会爆炸。
articles = Article.objects.select_related("author").all()
for article in articles:
print(article.author.name) # 不再逐条查询 author
它生成的 SQL 大致是:
SELECT article.*, author.*
FROM article
LEFT OUTER JOIN author ON article.author_id = author.id;
适合场景:
- 文章列表展示作者信息;
- 订单详情展示用户信息;
- 用户资料展示一对一扩展表。
不适合场景:一篇文章有多个评论,如果用 JOIN 拉评论,会让文章行被重复多次,数据量膨胀。
三、prefetch_related:用多次查询解决多值关系
多对多、一对多这种关系,每个对象可能对应多条记录。Django 更适合先查主对象,再查关联对象,然后在 Python 层把它们组装起来。
authors = Author.objects.prefetch_related("articles")
for author in authors:
for article in author.articles.all():
print(article.title)
它通常会执行两类查询:
- 查询作者列表;
- 查询这些作者对应的文章;
- Django 根据关联键把文章挂回作者对象。
这样比给每个作者单独查文章高效得多。
四、Prefetch 可以定制预加载
复杂场景可以使用 Prefetch:
from django.db.models import Prefetch
authors = Author.objects.prefetch_related(
Prefetch(
"articles",
queryset=Article.objects.filter(status="published").order_by("-created_at"),
to_attr="published_articles",
)
)
这样可以控制关联查询条件,还可以把结果放到自定义属性里,避免和默认 manager 行为混淆。
五、选择标准
判断时抓住一句:单值关系用 JOIN,多值关系用预取。
- ForeignKey 正向访问:优先
select_related - OneToOne:优先
select_related - ManyToMany:用
prefetch_related - 反向 ForeignKey:用
prefetch_related - 数据量很大时:关注字段选择、分页和实际 SQL,不要盲目预加载全部数据
六、常见误区
- 以为二者都只发一条 SQL。
prefetch_related通常是多条 SQL。 - 以为
select_related可以优化多对多。多对多更适合prefetch_related。 - 预加载后又调用不同过滤条件,例如
author.articles.filter(status="draft"),这可能绕过预取缓存重新查询。 - 在后台列表页、API 列表页忽略 N+1,数据少时没感觉,数据一多就拖垮接口。
七、如何判断应该优化哪条关系
面试官经常会给一个代码片段,让你判断会不会出现 N+1。判断方法是:先看主查询拿到的是哪类对象,再看循环里访问了哪些关联字段。如果循环里访问的是外键或一对一对象,比如 book.author.name,通常考虑 select_related("author");如果访问的是反向外键或多对多,比如 author.books.all(),通常考虑 prefetch_related("books")。
select_related 的本质是 SQL JOIN,把关联对象的字段一起查回来,适合一对一、外键这种单值关系。如果用在多值关系上,JOIN 会造成主对象重复行,Django 不适合用这种方式还原集合关系。prefetch_related 则是先查主对象,再用额外 SQL 查关联对象,然后在 Python 层组装关系,适合多对多和反向外键。
实际项目里,不是所有关联都提前加载越多越好。加载过多字段或过深关系,会造成 SQL 复杂、内存上升、响应变慢。优化目标应该是当前页面或接口真正需要的数据。列表页和详情页的查询策略往往不同,不能一套 select_related/prefetch_related 到处复用。
八、DRF 场景里的常见坑
在 DRF 中,N+1 经常不是出现在视图代码,而是藏在 Serializer 里。例如序列化文章列表时,字段里访问 author.name、tags 或 comments.count(),都可能触发额外查询。比较好的做法是在 ViewSet 的 get_queryset() 里根据 serializer 需要提前声明关联优化。
如果需要对预取集合加过滤或排序,可以使用 Prefetch 对象:
from django.db.models import Prefetch
authors = Author.objects.prefetch_related(
Prefetch("books", queryset=Book.objects.filter(published=True))
)
这样能避免在循环里对每个 author 再执行一次过滤查询。面试时能说到 Prefetch,说明你不只是知道两个 API 名字,还知道复杂场景下怎么落地。
九、常见误区与追问
| 关系类型 | 推荐 API | 查询特点 |
|---|---|---|
| 外键 / 一对一 | select_related | SQL JOIN,一次带出单个关联对象 |
| 多对多 / 反向外键 | prefetch_related | 多次查询,Python 层组装集合 |
| 需要过滤预取集合 | Prefetch | 自定义预取 queryset 和缓存目标 |
- 误区:select_related 和 prefetch_related 都只发一条 SQL。
select_related通常通过 JOIN 形成一条查询,prefetch_related通常至少两条查询。 - 误区:多对多关系也应该用 select_related。 多值关系会造成 JOIN 后主对象重复,更适合用
prefetch_related预取再组装。 - 误区:预加载越多越好。 过深、过宽的预加载会增加 SQL 复杂度和内存占用,应只加载当前页面或接口确实需要的关系。
- 追问:为什么 prefetch 后再 filter 可能仍然查库? 预取缓存对应的是指定关系的已取集合,后续新的过滤条件可能生成新查询,绕过原缓存。
- 追问:DRF 里 N+1 常藏在哪里? 常藏在 serializer 字段、
SerializerMethodField、反向关系和.count()调用中,应在get_queryset()集中优化。 - 追问:怎么判断该用哪个 API? 先看关联结果是单个对象还是对象集合;单个对象偏
select_related,集合关系偏prefetch_related。
记忆钩子:单值关系用 JOIN,集合关系用预取。看到循环里访问外键想
select_related,访问多对多或反向集合想prefetch_related。
十、加强记忆
select_related 靠 JOIN,一次带出外键或一对一对象;prefetch_related 靠额外查询和 Python 组装,适合多对多和反向一对多。判断标准很朴素:关联结果是单个对象,多半用 select_related;关联结果是一组对象,多半用 prefetch_related。