如何优化Django Profile列表接口的Avatar查询性能?
Django Profile头像查询优化方案
问题背景
现有Django项目中,Profile模型与Media模型为一对多关系(一个Profile对应多个Media,其中media_type=0代表头像)。当前的ProfileSerializer通过SerializerMethodField获取头像,但在列表接口中每个Profile都会触发额外的数据库查询,引发性能问题。
模型定义(models.py)
class Profile(models.Model): """ 用户资料模型 该模型存储用户的资料信息,包括头像、位置、城市、地址以及地理坐标(纬度和经度)。 字段说明: - user (OneToOneField): 关联的用户实例。 - avatar (ImageField): 可选的用户头像(个人照片)。 - location_picture (ImageField): 可选的用户位置照片。 - city (CharField): 可选的用户所在城市。 - address (CharField): 可选的用户详细地址。 - latitude (DecimalField): 可选的用户位置纬度,有效范围:-90.0 至 90.0。 - longitude (DecimalField): 可选的用户位置经度,有效范围:-180.0 至 180.0。 """ user = models.OneToOneField( settings.AUTH_USER_MODEL, verbose_name=_("用户"), help_text=_("关联的用户实例"), db_comment=_("关联的用户实例"), on_delete=models.CASCADE ) city = models.CharField( verbose_name=_("城市"), help_text=_("可选字段,用户所在城市"), db_comment=_("可选字段,用户所在城市"), max_length=255, null=True, blank=True ) address = models.CharField( verbose_name=_("详细地址"), help_text=_("可选字段,用户的详细地址"), db_comment=_("可选字段,用户的详细地址"), max_length=255, null=True, blank=True ) shair_location = models.BooleanField( verbose_name=_("共享位置"), default=False, help_text=_("可选字段,是否共享位置"), db_comment=_("可选字段,是否共享位置") ) latitude = models.DecimalField( verbose_name=_("纬度"), help_text=_("可选字段,用户位置的纬度"), db_comment=_("可选字段,用户位置的纬度"), max_digits=9, decimal_places=6, null=True, blank=True, validators=[ MinValueValidator(-90.0), MaxValueValidator(90.0) ]) longitude = models.DecimalField( verbose_name=_("经度"), help_text=_("可选字段,用户位置的经度"), db_comment=_("可选字段,用户位置的经度"), max_digits=9, decimal_places=6, null=True, blank=True, validators=[ MinValueValidator(-180.0), MaxValueValidator(180.0) ]) def __str__(self) -> str: return f"用户:{self.user}|城市:{self.city}" class Media(models.Model): profile = models.ForeignKey( Profile, verbose_name=_("用户资料"), help_text=_("用户资料可关联多个媒体文件(头像、封面、简历等)"), db_comment=_("用户资料可关联多个媒体文件(头像、封面、简历等)"), on_delete=models.CASCADE, related_name="medias" ) media_type = models.IntegerField( verbose_name=_("媒体类型"), choices=MediaType.choices, help_text=_("媒体类型:头像(0)、简历(1)、封面(2)、相册(3)"), db_comment=_("媒体类型:头像(0)、简历(1)、封面(2)、相册(3)"), validators=[ MinValueValidator(0), MaxValueValidator(3) ] ) image = models.ImageField( verbose_name=_("图片"), help_text=_("图片字段"), db_comment=_("图片字段"), upload_to="images/%Y/%m/%d/", validators=[ validate_image_size, ], null=True, blank=True ) video = models.FileField( verbose_name=_("视频"), help_text=_("视频字段"), db_comment=_("视频字段"), upload_to="videos/%Y/%m/%d", validators=[ validate_video_size, ], null=True, blank=True ) created_at = models.DateTimeField( verbose_name=_("创建时间"), help_text=_("媒体文件的创建时间"), db_comment=_("媒体文件的创建时间"), auto_now_add=True )
当前序列化器(serializer.py)
class ProfileSerializer(serializers.ModelSerializer): user = UserSerializer() avatar = serializers.SerializerMethodField(method_name="get_avatar_url") class Meta: model = Profile fields = ["id", "avatar", "user", "city", "address", "latitude", "longitude"] def get_avatar_url(self, profile): avatar_media = profile.medias.filter(media_type=0).first() if avatar_media: if avatar_media.image: return avatar_media.image else: return None else: return None
优化方案
方案1:使用预查询(Prefetch)过滤头像媒体
通过Prefetch对象在查询Profile时提前过滤并获取头像类型的Media,彻底避免N+1查询。
实现步骤:
- 在视图中修改查询集,添加自定义
Prefetch:
from django.db.models import Prefetch # 获取Profile列表时的查询集 profiles = Profile.objects.prefetch_related( Prefetch( 'medias', # 过滤出头像类型的媒体,按创建时间倒序取最新的 queryset=Media.objects.filter(media_type=0).order_by('-created_at'), # 把结果存到自定义属性avatar_media中 to_attr='avatar_media' ) ).select_related('user').all()
- 修改序列化器的
get_avatar_url方法,直接使用预取的属性:
def get_avatar_url(self, profile): # 从预取的集合中取第一个元素 avatar_media = profile.avatar_media[0] if profile.avatar_media else None if avatar_media and avatar_media.image: return avatar_media.image return None
方案2:为Profile添加头像缓存字段
如果头像更新频率低,可以在Profile模型中直接存储最新头像的关联,避免每次查询都去关联表查找。
实现步骤:
- 修改Profile模型,添加
avatar字段:
class Profile(models.Model): # ... 其他原有字段 avatar = models.ForeignKey( Media, verbose_name=_("头像"), help_text=_("用户的最新头像"), on_delete=models.SET_NULL, null=True, blank=True, related_name="avatar_profile" )
- 编写信号,当头像类型的Media创建/更新时,自动同步到Profile的
avatar字段:
from django.db.models.signals import post_save from django.dispatch import receiver @receiver(post_save, sender=Media) def update_profile_avatar(sender, instance, created, **kwargs): # 仅处理头像类型的媒体 if instance.media_type == 0: instance.profile.avatar = instance # 只更新avatar字段,提升性能 instance.profile.save(update_fields=['avatar'])
- 修改序列化器,直接使用缓存的头像字段:
class ProfileSerializer(serializers.ModelSerializer): user = UserSerializer() # 直接从关联的Media对象中获取图片 avatar = serializers.ImageField(source='avatar.image', read_only=True, allow_null=True) class Meta: model = Profile fields = ["id", "avatar", "user", "city", "address", "latitude", "longitude"]
查询时只需添加select_related避免关联查询:
profiles = Profile.objects.select_related('avatar', 'user').all()
方案3:使用子查询(Subquery)获取头像URL
通过Django的子查询功能,在Profile的主查询中直接获取对应的头像URL,将N+1查询转化为单个查询。
实现步骤:
- 在视图的查询集中添加子查询注解:
from django.db.models import Subquery, OuterRef # 子查询:获取每个Profile最新的有效头像URL latest_avatar = Media.objects.filter( profile=OuterRef('pk'), media_type=0, image__isnull=False ).order_by('-created_at').values('image')[:1] profiles = Profile.objects.annotate( avatar_url=Subquery(latest_avatar) ).select_related('user').all()
- 修改序列化器,直接使用注解的字段:
class ProfileSerializer(serializers.ModelSerializer): user = UserSerializer() avatar = serializers.CharField(source='avatar_url', read_only=True, allow_null=True) class Meta: model = Profile fields = ["id", "avatar", "user", "city", "address", "latitude", "longitude"]
方案对比
- 预查询(Prefetch):适合头像更新频繁的场景,无需修改模型,实现简单,性能提升明显。
- 缓存字段:适合头像更新不频繁的场景,查询性能最优,但需要维护信号逻辑,增加了模型复杂度。
- 子查询(Subquery):无需修改模型,一次查询完成所有数据获取,适合需要灵活过滤的场景。
内容的提问来源于stack exchange,提问作者ARYAN-NIKNEZHAD
相关产品推荐
相关产品推荐

