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

Django中满足多条件的车辆查询性能优化咨询

优化多关联条件下的Car查询性能问题

问题背景

现有模型结构:

class Car(models.Model):
    uuid = models.UUIDField()
    is_red = models.BooleanField()

class Owner(models.Model):
    car = models.ForeignKey(Car, to_field="uuid", on_delete=models.CASCADE)

class Driver(models.Model):
    cars = models.ManyToManyField(Car, related_name="drivers")  # 注:原代码中`ProtectedArea`应为笔误,修正为Car

业务需求是筛选满足以下任一条件的Car:

  • 车辆是红色(is_red=True)
  • 至少有一位Owner
  • 至少有一位Driver

当前使用自定义管理器实现,但查询(尤其是计数查询)速度极慢,本地测试环境50k条Car、20k条Owner、1.2k条Driver数据下,计数查询耗时超2秒,且尝试改用反向NOT EXISTS的写法后性能更差。另外Car模型包含大体积多边形数据,可能加剧性能损耗。

优化方案

1. 重构查询逻辑,用正向EXISTS替代IN子查询

原查询使用uuid__in子查询,会先查询所有关联的uuid再去重,效率较低。改用Django的反向关联自动生成EXISTS查询,更符合数据库优化器的处理逻辑:

class ActiveCarManager(models.Manager):
    def get_queryset(self):
        # 利用反向关联,自动生成EXISTS检查是否存在关联的Owner/Driver
        return (
            super()
            .get_queryset()
            .filter(
                Q(is_red=True)
                | Q(owner__isnull=False)
                | Q(drivers__isnull=False)
            )
            .distinct()  # 避免多关联导致的重复行
        )

该写法生成的SQL会直接检查每条Car是否存在关联的Owner或Driver,无需提前查询所有关联uuid,减少子查询的开销。

2. 补充关键索引

从查询计划的Seq Scan可以看出,缺少必要索引导致全表扫描,需添加以下索引:

  • 给Owner.car添加索引:修改模型时设置db_index=True,或手动执行SQL:
    CREATE INDEX idx_owner_car_uuid ON app_owner(car);
    
  • 给Car.is_red添加索引:models.BooleanField(db_index=True),加速布尔值过滤
  • 确保Car.uuid是唯一索引:设置models.UUIDField(unique=True),自动生成唯一索引,提升关联查询速度
  • 确认多对多中间表app_driver_cars的car_id和driver_id已加索引(Django默认会为多对多外键生成索引,可通过数据库工具验证)

3. 避免加载大体积字段

Car模型中的多边形数据属于大字段,查询时如果不需要使用这些数据,通过only()或defer()排除,减少数据传输和对象实例化的开销:

# 在视图或管理器中指定只加载必要字段
Car.objects.only('uuid', 'is_red').filter(...)
# 或排除大字段
Car.objects.defer('polygon_field').filter(...)

这也是Django Debug Toolbar显示耗时比数据库查询计划长的核心原因之一——大字段的传输和处理会增加额外耗时。

4. 新增is_active字段并通过信号维护

如果上述优化仍无法满足性能要求,可考虑新增一个预计算的is_active字段,将三个条件的判断结果提前存储:

class Car(models.Model):
    uuid = models.UUIDField(unique=True)
    is_red = models.BooleanField(db_index=True)
    is_active = models.BooleanField(default=False, db_index=True)
    # 其他字段...

然后通过Django信号自动维护该字段的状态:

from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver

@receiver(post_save, sender=Car)
def update_car_active_on_color_change(sender, instance, **kwargs):
    # 当is_red修改时,更新is_active
    instance.is_active = instance.is_red or instance.owner_set.exists() or instance.drivers.exists()
    instance.save(update_fields=['is_active'])

@receiver(post_save, sender=Owner)
@receiver(post_delete, sender=Owner)
def update_car_active_on_owner_change(sender, instance, **kwargs):
    car = instance.car
    car.is_active = car.is_red or car.owner_set.exists() or car.drivers.exists()
    car.save(update_fields=['is_active'])

@receiver(m2m_changed, sender=Driver.cars.through)
def update_car_active_on_driver_change(sender, instance, action, pk_set, **kwargs):
    if action in ['post_add', 'post_remove', 'post_clear']:
        # 批量更新受影响的Car
        cars = Car.objects.filter(id__in=pk_set)
        for car in cars:
            car.is_active = car.is_red or car.owner_set.exists() or car.drivers.exists()
            car.save(update_fields=['is_active'])

这种方式的优势是查询时直接过滤is_active=True,性能接近单字段索引查询,但需要注意维护数据一致性,确保所有修改场景都被信号覆盖。

5. 优化计数查询

如果使用FilterView的分页功能,默认的计数查询会遍历所有符合条件的行。可根据场景优化:

  • 若无需分页,关闭FilterView的分页设置
  • 若允许近似计数(如后台统计场景),可使用PostgreSQL的近似计数方法:
    SELECT reltuples::BIGINT FROM pg_class WHERE relname='app_car';
    
  • 对于精确计数,可考虑使用第三方库提供的高效计数方法。

内容的提问来源于stack exchange,提问作者tommueller

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 12:08:09