优化Django反向查询:prefetch_related仍触发额外查询的解决方案
问题原因分析
你遇到的情况是:虽然使用了prefetch_related('eir')预取关联数据,但调用chassis.first().eir.all()时依然触发了新的数据库查询。核心原因是:
- 执行
chassis = Chassis.objects.prefetch_related('eir')后,输入chassis会触发QuerySet求值,此时Django查询前21条Chassis并批量预取对应Eir数据(WHERE chassis_id IN (1292,104))。 - 但默认调用
instance.eir.all()时,会生成新的QuerySet实例,在部分场景下会绕过预取缓存,重新发起单条Chassis的Eir查询。而你在DRF的to_representation中使用instance.eir.all().last().id,会导致每条Chassis都触发一次额外查询,形成N+1问题。
解决方案
方案1:用Prefetch预取排序后的关联数据
通过Prefetch指定预取时的排序规则,让预取的Eir按需求排列,后续直接从缓存取结果,避免额外查询:
from django.db.models import Prefetch # 查询时预取每个Chassis的Eir,并按id降序排列(方便快速取最新一条) chassis_qs = Chassis.objects.prefetch_related( Prefetch('eir', queryset=Eir.objects.order_by('-id')) )
在DRF的to_representation中直接使用缓存数据:
def to_representation(self, instance): ret = super().to_representation(instance) eirs = instance.eir.all() ret['eir'] = eirs[0].id if eirs else None return ret
此时instance.eir.all()会直接调用预取的缓存,不会触发新的数据库查询。
方案2:用Subquery注解最新Eir的ID(更高效)
如果仅需获取每个Chassis对应的最新Eir的ID,不需要预取所有Eir记录,推荐用Subquery直接注解字段,一次查询搞定所有数据:
from django.db.models import Subquery, OuterRef # 子查询:获取每个Chassis对应的最新Eir的ID latest_eir_subquery = Eir.objects.filter( chassis=OuterRef('pk') ).order_by('-id').values('id')[:1] # 给每个Chassis添加latest_eir_id注解字段 chassis_qs = Chassis.objects.annotate( latest_eir_id=Subquery(latest_eir_subquery) )
在DRF的to_representation中直接使用注解字段:
def to_representation(self, instance): ret = super().to_representation(instance) ret['eir'] = instance.latest_eir_id return ret
这种方式仅需两次查询(一次查Chassis,一次查所有最新Eir的ID),比预取所有Eir数据更高效,尤其适合Chassis关联大量Eir记录的场景。
验证测试
按方案1修改查询后,在Django CLI重新测试:
>>> chassis = Chassis.objects.prefetch_related(Prefetch('eir', queryset=Eir.objects.order_by('-id'))) >>> chassis # 触发两次查询:Chassis列表和批量预取Eir >>> chassis.first().eir.all() # 直接从缓存读取,无新查询
此时数据库日志不会出现第三次查询,符合预期。
内容的提问来源于stack exchange,提问作者Dean Christian Armada
相关产品推荐
相关产品推荐

