多关联模型场景下优化select_related查询的性能问题
我正在开发一款数据分散在多张不同表中的应用,近期安装了Django Debug Toolbar后,发现存在一些重复及相似的查询。功能虽正常,但对当前查询方式的性能存疑。
模型定义
import uuid from django.db import models from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager class Location(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True) city = models.CharField(max_length=40, blank=True) placeId = models.CharField(max_length=100) point = models.PointField(srid=4326, blank=True, null=True) class CustomAccountManager(BaseUserManager): # 假设此处有自定义用户管理器实现 pass class User(AbstractBaseUser, PermissionsMixin): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True) email = models.EmailField('email address', unique=True) first_name = models.CharField(max_length=150, blank=True) current_location = models.ForeignKey(Location, on_delete=models.SET_NULL, related_name="location", null=True) search_range = models.PositiveSmallIntegerField(default=10, blank=True, null=True) objects = CustomAccountManager() class Profile(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True) user = models.OneToOneField(User, on_delete=models.CASCADE) dummy_section = models.TextField(max_length=155, null=True, blank=True) class Blocked(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_thats_blocking") blocked_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="blocked_user") date_created = models.DateTimeField(auto_now_add=True) class Interactions(models.Model): IntChoices = ( ("yes", "yes"), ("no", "no"), ) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, db_index=True) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="current_user") interacted_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="interacted_user") interacted = models.CharField(max_length=20, default="yes", choices=(IntChoices), db_index=True)
当前查询逻辑
获取搜索范围内的所有潜在用户Profile,从Interactions模型中排除已交互用户,同时排除Blocked模型中的已屏蔽用户:
from django.contrib.gis.measure import Distance range_number = 10 allowed_users = Profile.objects.select_related('user').filter(user__current_location__point__distance_lte=(current_loc, Distance(km=int(range_number)))) blocked = Blocked.objects.select_related('user').filter(user__id=current_id).values('blocked_user') users_taken_inter = Interactions.objects.select_related('user').filter(user__id=current_id).values('interacted_user') users_not_taken_action = allowed_users.exclude(user__in=users_taken_inter).exclude(user__in=blocked).exclude(user__id=current_id)
目前使用分页,平均产生35次查询,耗时35-40ms,希望去除重复查询并提升性能。
1. 用子查询合并查询,减少数据库往返
原来的blocked和users_taken_inter会生成独立查询,再在exclude时重复调用。改用Django的Subquery和OuterRef,把过滤逻辑整合到主查询中,将多次查询合并为1-2次主查询。
修改后的代码:
from django.db.models import Subquery, OuterRef from django.contrib.gis.measure import Distance range_number = 10 # 子查询:获取当前用户屏蔽的用户ID blocked_subquery = Subquery( Blocked.objects.filter(user_id=current_id).values('blocked_user_id') ) # 子查询:获取当前用户已交互的用户ID interacted_subquery = Subquery( Interactions.objects.filter(user_id=current_id).values('interacted_user_id') ) # 一次性构建完整查询,提前排除自己,合并子查询过滤 users_not_taken_action = ( Profile.objects .select_related('user__current_location') # 预加载关联的user和location,避免N+1查询 .filter( user__current_location__point__distance_lte=(current_loc, Distance(km=int(range_number))), user__id__ne=current_id # 提前排除当前用户,不用最后exclude ) .exclude(user__id__in=blocked_subquery) .exclude(user__id__in=interacted_subquery) )
2. 优化关联数据预加载,消除N+1查询
原来的select_related('user')只加载了User,但User关联了Location,当访问user.current_location时会触发额外查询。加上select_related('user__current_location')可以一次性加载所有关联数据,避免分页时每条记录都触发一次Location查询。
3. 添加组合索引,加速过滤查询
为Blocked和Interactions表添加组合索引,提升子查询的执行速度:
# 在Blocked模型中添加索引 class Blocked(models.Model): # ... 原有字段 ... class Meta: indexes = [ models.Index(fields=['user_id', 'blocked_user_id']), ] # 在Interactions模型中添加索引 class Interactions(models.Model): # ... 原有字段 ... class Meta: indexes = [ models.Index(fields=['user_id', 'interacted_user_id']), ]
同时,确保Location.point字段有空间索引(Django的PointField默认会创建,但可以确认数据库层面是否存在),加速地理距离过滤。
4. 避免查询集重复求值
查询集是惰性求值的,如果多次调用users_not_taken_action(比如分页时的计数和取数据)会触发多次查询。可以用cache()方法缓存结果,或者确保分页使用同一查询集的切片:
# 如果需要多次使用查询集,缓存结果 users_not_taken_action = users_not_taken_action.cache() # 分页示例(使用同一查询集) from django.core.paginator import Paginator paginator = Paginator(users_not_taken_action, 10) page_obj = paginator.get_page(request.GET.get('page'))
5. 简化地理查询(可选)
如果你的数据库支持(比如PostGIS),可以考虑使用distance_lte的优化版本,或者提前将距离计算转换为边界框过滤(先过滤大致范围再精确计算距离),减少地理计算的开销。
优化后查询次数会大幅降低(通常能降到3-5次以内),耗时也会明显减少。可以再次通过Django Debug Toolbar验证查询次数和耗时的变化。
内容的提问来源于stack exchange,提问作者rafaelHTML

