Django中满足多条件的车辆查询性能优化咨询
问题背景
现有模型结构:
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

