You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多关联模型场景下优化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 16:52:57