← 返回题目列表

Django 中 select_related 和 prefetch_related 有什么区别?

高频 中等 第 6 / 27 题 更新于 2026/07/27
DjangoORMN+1性能优化

简化版

select_related 用 SQL JOIN 一次查出外键或一对一关联对象,适合 ForeignKey / OneToOneFieldprefetch_related 会额外发查询,再由 Django 在 Python 层组装结果,适合 ManyToMany、反向外键和多值关系。

详细版

两者都是为了解决 N+1 查询问题,但适用关系不同:

方法查询方式适合关系特点
select_relatedSQL 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)

它通常会执行两类查询:

  1. 查询作者列表;
  2. 查询这些作者对应的文章;
  3. 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.nametagscomments.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_relatedSQL 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