如何通过链式prefetch_related()减少Django的SQL查询次数?
问题核心
你在使用prefetch_related关联多表时,发现仍生成多条SQL,甚至直接单独查询各模型的SQL次数更少,需要在保留prefetch_related的前提下减少查询次数。
原因分析
- 先全局prefetch再获取单个对象:当你先执行
Item.objects.prefetch_related(...).get(pk=pk),或者先获取全量QuerySet再get(pk=4),Django会先查询所有Item,再单独查询目标Item及其关联数据,属于重复查询。 .values()绕过预取缓存:item.item_photo.values()这类写法会忽略prefetch缓存,触发新的SQL查询——因为.values()会直接构造新的查询集,不会使用已预取的对象。- 关联关系类型不匹配:
ItemStock如果实际是和Item一对一(你用了get(item_id=pk)),却用ForeignKey+prefetch_related组合,应该改用OneToOneField+select_related,后者通过SQL JOIN一次性获取主表和关联表数据。
解决方案
- 先过滤目标Item再执行prefetch_related
针对单个Item的场景,先通过filter(pk=pk)定位目标,再预取关联数据,这样prefetch只会针对该Item的关联表查询,避免全局查询后再单独获取:
pk = 4 # 先过滤再prefetch,仅查询目标Item及其关联数据 item = Item.objects.filter(pk=pk).prefetch_related( 'item_photo', 'item_stock', 'item_review' ).first() # 直接使用预取的关联对象,避免用.values() context = { 'form': form, 'item': item, 'photos': item.item_photo.all(), # 使用预取的QuerySet 'stock': item.item_stock.first(), # 若为一对一关系,建议后续调整模型 'review': item.item_review.all(), }
这样只会生成4条SQL:1条查询Item,3条分别查询三个关联表(IN条件仅包含当前pk),和你单独查询的次数一致,但保留了ORM的关联对象操作便捷性。
- 避免使用.values(),手动提取所需数据
.values()会创建新查询集,不使用prefetch_related缓存的对象。如果需要字典格式数据,可以手动从预取对象中提取:
# 替代item.item_photo.values()的写法 photos = [{'id': p.id, 'photo': p.photo.url, 'priority': p.priority} for p in item.item_photo.all()]
- 调整一对一关联的模型设计(针对ItemStock)
如果每个Item对应唯一的ItemStock,将ItemStock的ForeignKey改为OneToOneField,然后用select_related代替prefetch_related,通过SQL JOIN一次性获取数据:
# 修改models.py中的ItemStock类 class ItemStock(models.Model): item = models.OneToOneField(Item, on_delete=models.SET_NULL, null=True, related_name="item_stock", verbose_name="商品番号") stock = models.IntegerField("在庫数", blank=False, default=0) # 查询时组合使用select_related和prefetch_related item = Item.objects.filter(pk=pk).select_related('item_stock').prefetch_related( 'item_photo', 'item_review' ).first()
这样SQL次数减少到3条:1条Join查询Item和ItemStock,2条分别查询item_photo和item_review。
- 正确结合过滤使用Prefetch类
如果需要对关联表做自定义过滤,使用Prefetch类时也要先过滤主表,避免全局预取:
from django.db.models import Prefetch pk = 4 item = Item.objects.filter(pk=pk).prefetch_related( Prefetch('item_photo', queryset=ItemPhoto.objects.order_by('priority'), to_attr='ipr'), Prefetch('item_stock', queryset=ItemStock.objects.all(), to_attr='isr'), Prefetch('item_review', queryset=ItemReview.objects.all(), to_attr='irr'), ).first() # 使用预取的to_attr属性 context = { 'form': form, 'item': item, 'photos': item.ipr, 'stock': item.isr[0] if item.isr else None, 'review': item.irr, }
验证效果
调整后,针对单个Item的查询会生成:
- 1条查询Item的SQL
- 3条查询关联表的SQL(若用select_related处理ItemStock,则是2条)
和你单独查询的次数一致,同时保留了ORM的关联对象操作,避免手动维护外键关联逻辑。
内容的提问来源于stack exchange,提问作者K-Yuuma
相关产品推荐
相关产品推荐

