Какой самый быстрый способ запросить элементы с существующим внешним ключом и записью "многие-ко-многим" в 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(ProtectedArea, related_name="cars")
Сейчас большая часть логики моего приложения опирается на автомобили, для которых верно хотя бы одно из трех условий: он красный, у него есть хотя бы один владелец, у него есть хотя бы один водитель. Возможно, это важная информация, что в действительности модель автомобиля содержит довольно большие полигональные данные, может быть, это имеет значение для производительности?
У меня есть пользовательский менеджер для этого, но теперь, независимо от того, как я построил запрос, он кажется чрезвычайно медленным. Время взято с моей локальной машины с ~50k машин, 20k владельцев, 1.2k водителей. Представление является стандартным FilterView
из django-filter
без каких-либо фильтров, которые на самом деле активны.
В настоящее время мой менеджер выглядит следующим образом:
class ActiveCarManager(models.Manager):
def get_queryset(self):
cars_with_owners = Owner.objects.values("car__uuid").distinct()
cars_with_drivers = Drivers.objects.values("cars__uuid").distinct()
return (
super()
.get_queryset()
.filter(
Q(uuid__in=cars_with_owners)
| Q(uuid__in=cars_with_drivers)
| Q(is_red=True)
)
)
Представление генерирует 2 запроса, один запрос на подсчет и один запрос на получение фактических элементов. Запрос, который выполняется так медленно, - это запрос подсчета. На нашем экземпляре staging он регулярно занимает несколько секунд. Вот сгенерированный SQL и план запроса:
SELECT COUNT(*) AS "__count"
FROM "app_car"
WHERE ("app_car"."uuid" IN (SELECT DISTINCT U0."car" FROM "app_owner" U0) OR "app_car"."uuid" IN (SELECT DISTINCT U2."uuid" FROM "app_driver" U0 LEFT OUTER JOIN "app_driver_cars" U1 ON (U0."id" = U1."driver_id") LEFT OUTER JOIN "app_car" U2 ON (U1."car_id" = U2."id")) OR "app_car"."is_red" = TRUE)
Aggregate (cost=32426.52..32426.53 rows=1 width=8) (actual time=951.281..951.289 rows=1 loops=1)
-> Seq Scan on app_car (cost=13300.79..32335.30 rows=36485 width=0) (actual time=20.850..950.695 rows=3687 loops=1)
Filter: ((hashed SubPlan 1) OR (hashed SubPlan 2) OR (is_red IS TRUE))
Rows Removed by Filter: 44147
SubPlan 1
-> HashAggregate (cost=556.68..580.34 rows=2366 width=16) (actual time=6.713..6.940 rows=2366 loops=1)
Group Key: u0.park
Batches: 1 Memory Usage: 241kB
-> Seq Scan on app_owner u0 (cost=0.00..508.57 rows=19245 width=16) (actual time=0.007..3.970 rows=19245 loops=1)
Filter: approved
Rows Removed by Filter: 12
SubPlan 2
-> HashAggregate (cost=12669.76..12705.58 rows=3582 width=16) (actual time=12.404..12.718 rows=3009 loops=1)
Group Key: u2.uuid
Batches: 1 Memory Usage: 369kB
-> Nested Loop Left Join (cost=0.86..12660.81 rows=3582 width=16) (actual time=0.090..10.961 rows=3585 loops=1)
-> Merge Left Join (cost=0.56..238.42 rows=3582 width=8) (actual time=0.065..1.403 rows=3585 loops=1)
Merge Cond: (u0_1.id = u1.driver_id)
-> Index Only Scan using driver_id_pkey on driver_id u0_1 (cost=0.28..70.02 rows=2249 width=8) (actual time=0.036..0.388 rows=2249 loops=1)
Heap Fetches: 0
-> Index Only Scan using driver_id_park_id_car_cb09e8dc_uniq on driver_id_carss u1 (cost=0.28..118.01 rows=3582 width=16) (actual time=0.026..0.435 rows=3582 loops=1)
Heap Fetches: 0
-> Memoize (cost=0.30..4.11 rows=1 width=24) (actual time=0.002..0.002 rows=1 loops=3585)
Cache Key: u1.car_id
Cache Mode: logical
Hits: 576 Misses: 3009 Evictions: 0 Overflows: 0 Memory Usage: 377kB
-> Index Scan using app_car_pkey on app_car u2 (cost=0.29..4.10 rows=1 width=24) (actual time=0.002..0.002 rows=1 loops=3009)
Index Cond: (id = u1.car_id)
Planning Time: 5.902 ms
Execution Time: 952.436 ms
Интересно, что в плане запросов указано, что это заняло 952 мс, однако Django Debug Toolbar сообщает о 2162 мс...
Есть ли у вас идеи, как я могу ускорить это? Или я должен использовать специальный столбец "is_active" и обновлять его с помощью сигналов?
Вы можете оптимизировать это до:
class ActiveCarManager(models.Manager):
def get_queryset(self):
return (
super()
.get_queryset()
.filter(~Q(owner=None) | ~Q(cars=None) | Q(is_red=True))
.distinct()
)
Параметр related_name=…
[Django-doc] однако является именем отношения в reverse, то есть от Car
модели к Driver
модели в данном случае. Поэтому (часто) не имеет особого смысла называть его так же, как и прямое отношение. Таким образом, вы можете рассмотреть возможность переименования отношения в cars
drivers
.
Если мы переименуем related_name=…
, то получится:
class ActiveCarManager(models.Manager):
def get_queryset(self):
return (
super()
.get_queryset()
.filter(~Q(owner=None) | ~Q(drivers=None) | Q(is_red=True))
.distinct()
)
Или мне следует использовать специальный столбец "is_active" и обновлять его с помощью сигналов?
I would strongly advise against that. Then you move the burden to the signals working properly. Signals however tend to be "very painful". Indeed, you will need to know if an Owner
is created, updated, removed, if a Driver
changes what Car
s it drives, is removed, etc. While that may look simple, the number of cases can grow very fast, and furthermore, signals don't run when you do bulk updates, so it is likely that eventually the signal will not be triggered. See this article I wrote about signals [django-antipatterns].