Prefetch_related用于解决反向多对一、一对多及多对多关系的N+1查询问题,通过额外查询+Python层拼接预加载关联数据;正向外键则应使用select_related。

什么是Prefetch_related触发的N+1问题
当你在Django中遍历一个QuerySet并访问外键或反向关系字段时,比如author.posts.all(),默认会为每个对象单独发一次SQL查询——1次查主表,N次查关联表。这就是典型的N+1问题。它不报错,但性能陡降,尤其在列表页渲染几十条记录时,可能发起上百次数据库请求。
Prefetch_related适合什么场景
Prefetch_related专用于**多对一、一对多、多对多**这类“反向”或“非外键字段”的关系预加载,底层用LEFT OUTER JOIN或额外的IN子查询实现。它不能替代select_related(后者只适用于单值外键正向关系)。
- 适用:获取
Author对象后,要批量访问其post_set.all()或tags.all() - 不适用:想取
Post.author.name这种单值正向外键字段(该用select_related('author')) - 可嵌套:支持
Prefetch_related('posts__tags'),但注意会生成更复杂的JOIN或多次查询
怎么写才不踩坑
常见错误是以为加了Prefetch_related就万事大吉,结果发现SQL没变、甚至更慢。关键看三点:
- 必须在QuerySet上链式调用,不是在for循环里补——
Author.objects.prefetch_related('post_set'),不是for a in qs: a.post_set.all() - 关系名要写对:
ForeignKey反向默认是模型名小写_set(如post_set),自定义related_name则用那个名字(如related_name='articles'就得写'articles') - 避免和
distinct()混用:如果Prefetch后又调.distinct(),Django可能放弃优化、回退成N+1;需要去重时优先在Python层处理,或改用values_list(...).distinct() - 大数据量慎用嵌套Prefetch:比如
prefetch_related('posts__comments__author')可能触发笛卡尔积,查出远超预期的行数
简单示例:
立即学习“Python免费学习笔记(深入)”;
qs = Author.objects.prefetch_related('post_set')
for author in qs:
print(author.name)
for post in author.post_set.all(): # 这里不再发SQL
print(post.title)怎么验证是否生效
最直接的办法是打开Django Debug Toolbar,或者手动启用查询日志:
- 在settings.py中配置
LOGGING,把django.db.backends设为DEBUG - 运行代码,观察SQL输出:优化前有N+1条
SELECT ... FROM "post",优化后只有2条——1条Author,1条Post(带WHERE "post"."author_id" IN (...)) - 注意:如果看到
SELECT ... FROM "post" WHERE "post"."author_id" = ?重复出现,说明Prefetch_related根本没生效,回去检查调用位置和关系名
嵌套关系、自定义to_attr、配合Prefetch对象做筛选等进阶用法,很容易漏掉中间某一层的预加载,导致局部仍N+1——这点最容易被忽略。


















