prefetch_related必须用于反向ForeignKey和多对多关系的N+1查询优化,如查用户所有订单及每个订单的商品时用prefetch_related('orders__items'),避免101次SQL查询。

prefetch_related 什么时候必须用
查一个用户的所有订单,再查每个订单的全部商品——这种「外键嵌套查」不加 prefetch_related,Django 就会为每个订单单独发一次 SQL 查商品,100 个订单就是 101 次查询。这不是慢,是崩。
它专治两类关系的 N+1:多对多(ManyToManyField)和反向 ForeignKey(比如 Order.items.all(),其中 Item.order 是正向外键,Order.items 就是反向关系)。
- 正向 ForeignKey(如
item.order)用select_related,走 JOIN,不能用prefetch_related -
prefetch_related内部发额外的 SELECT + IN 查询,适合「一对多」「多对多」这类无法高效 JOIN 的场景 - 如果模型有自定义
related_name(比如ForeignKey(User, related_name='posts')),就得用那个名字,不是字段名
怎么写才不白写
常见错误是只写一层、漏掉深层嵌套,或者用了 prefetch_related 却还在循环里调用未预取的属性。
- 要查用户 + 所有订单 + 每个订单的商品:用
prefetch_related('orders__items'),双下划线表示跨关系穿透 - 如果
Item还关联了Category,且你想一并查出:写成prefetch_related('orders__items__category'),但注意这会触发三次查询(orders、items、category 各一次) - 别在
for order in qs:循环里再写order.items.all()—— 那样还是触发新查询,得直接用order.items.all()(此时已缓存) - 如果只想要商品名列表,别用
.values_list('name', flat=True)在 prefetch 后链式调用,它会绕过预取结果,重查数据库
容易被忽略的性能陷阱
prefetch_related 不是银弹,用错反而更慢。
- 大量数据时,IN 查询可能超长(比如 5000 个 order_id),MySQL 默认
max_allowed_packet会截断,报错MySQLdb._exceptions.OperationalError: (1301, "Result of <code>subqueryis larger thanmax_allowed_packet") - 对空集合或极小结果集,额外查询反而增加开销;可先判断
if qs.exists()再决定是否 prefetch - 不能和
distinct()混用在含 JOIN 的查询后(虽然prefetch_related本身不 JOIN,但若前面用了select_related或annotate,就可能冲突) - 使用
Prefetch对象可精细控制:比如只查特定字段Prefetch('items', queryset=Item.objects.only('id', 'name'))
调试时怎么看它有没有生效
光看代码没用,得确认 SQL 真的变少了。
- 打开 Django Debug Toolbar,点 SQL 标签页,对比加/不加
prefetch_related的查询次数和语句内容 - 手动打印:在视图里加
print(connection.queries)(记得from django.db import connection),注意它只记录当前请求生命周期内的查询 - 留意日志里有没有重复出现类似
SELECT ... FROM "myapp_item" WHERE "myapp_item"."order_id" IN (1,2,3,...)的语句——这就是prefetch_related在工作 - 如果看到
SELECT ... FROM "myapp_order" INNER JOIN "myapp_item",说明你误用了select_related或 ORM 自动 JOIN 了,不是prefetch_related的行为
最常被跳过的其实是反向关系名拼错,或者忘了 related_name 被自定义过。查不到数据不一定是逻辑错,可能是预取根本没执行。










