Django Admin中select_related预查询仍产生额外SQL查询的问题解决
问题分析与解决方案
你的核心问题在于:资产ModelAdmin中定义的select_related只作用于资产列表页的查询,但Django Admin的Product列表过滤器生成选项时,会单独查询Product模型,完全不会复用资产的queryset,所以你加的预加载根本覆盖不到过滤器里的Product实例,自然会触发大量N+1查询。
下面给你几个可行的解决办法:
方案一:自定义预加载关联对象的Product过滤器
直接继承Django的SimpleListFilter,在生成过滤器选项时提前预加载category和company,一次性把关联数据查出来:
from django.contrib.admin import SimpleListFilter from .models import Product class ProductFilter(SimpleListFilter): title = '产品' parameter_name = 'product' def lookups(self, request, model_admin): # 预加载category和company,避免生成选项时触发额外查询 product_list = Product.objects.select_related('category', 'company').all() return [(product.id, str(product)) for product in product_list] def queryset(self, request, queryset): if self.value(): return queryset.filter(product_id=self.value()) return queryset
然后在资产的ModelAdmin中替换默认过滤器:
@admin.register(Asset) class AssetAdmin(admin.ModelAdmin): # 把原来的'product'换成自定义过滤器 list_filter = (ProductFilter, ) # 其他配置...
方案二:重写默认关联过滤器,强制预加载
如果不想完全自定义过滤器,可以继承Django默认的RelatedFieldListFilter,重写它的查询逻辑,给Product的查询集加上预加载:
from django.contrib.admin.filters import RelatedFieldListFilter class PreloadedProductFilter(RelatedFieldListFilter): def get_queryset(self, request): qs = super().get_queryset(request) # 仅针对product字段的过滤器,预加载关联对象 if self.field.name == 'product': return qs.select_related('category', 'company') return qs
然后在资产ModelAdmin中指定使用这个过滤器:
@admin.register(Asset) class AssetAdmin(admin.ModelAdmin): list_filter = ( ('product', PreloadedProductFilter), # 其他过滤器... ) # 其他配置...
方案三:给Product的关联属性加缓存
如果不想改过滤器逻辑,可以用Django的cached_property缓存关联对象的属性访问,减少重复查询:
from django.utils.functional import cached_property from django.db import models class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE) company = models.ForeignKey(Company, on_delete=models.SET_NULL, null=True, blank=True) name = models.CharField(max_length=100) @cached_property def category_name(self): return self.category.name @cached_property def company_name(self): return self.company.name if self.company else '' def __str__(self): if self.company: return f"{self.category_name} | {self.company_name} | {self.name}" return f"{self.category_name} | {self.name}"
这个方法会缓存每个Product实例的关联属性值,第一次访问时查询一次,后续不再触发SQL,但相比前两种预加载的方案,在过滤器选项数量多的时候,效率还是略低。
内容的提问来源于stack exchange,提问作者nigel239
相关产品推荐
相关产品推荐

