Django ORM - Условие или фильтр на LEFT JOIN
Я постараюсь быть максимально точным в этом вопросе. Представьте себе эти две модели. связь между которыми была установлена много лет назад:
class Event(models.Model):
instance_created_date = models.DateTimeField(auto_now_add=True)
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name="car_events")
...
a lot of normal text fields here, but they dont matter for this problem.
и
class Car(models.Model):
a lot of text fields here, but they dont matter for this problem.
hide_from_company_search = models.BooleanField(default=False)
images = models.ManyToManyField(Image, through=CarImage)
Допустим, я хочу запросить количество событий для данного автомобиля:
def get_car_events_qs() -> QuerySet:
six_days_ago = (timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=6))
cars = Car.objects.prefetch_related(
'car_events',
).filter(
some_conditions_on_fields=False,
).annotate(
num_car_events=Count(
'car_events',
filter=Q(car_events__instance_created_date__gt=six_days_ago), distinct=True)
)
return cars
По-настоящему сложной частью для этого является производительность запроса: Cars
имеет 450.000
записей, а Events
имеет 156.850.048
. Все поля, которые я использую для запроса, проиндексированы. Выполнение запроса занимает около 4 минут, в зависимости от загрузки базы данных. До добавления признаков прошло 18 минут
В результате вышеприведенного ORM-запроса получится следующий sql:
SELECT
"core_car"."id",
COUNT("analytics_carevent"."id") FILTER (WHERE ("analytics_carevent"."event" = 'view'
AND "analytics_carevent"."instance_created_date" >= '2022-05-10T07:45:16.672279+00:00'::timestamptz
AND "analytics_carevent"."instance_created_date" < '2022-05-11T07:45:16.672284+00:00'::timestamptz)) AS "num_cars_view",
LEFT OUTER JOIN "analytics_carevent" ON ("core_car"."id" = "analytics_carevent"."car_id")
WHERE
... some conditions that dont matter
GROUP BY
"core_car"."id"
Я почему-то подозреваю, что это FILTER
является проблемой.
Я пробовал с
.annotate(num_car_events=Count('car_events'))
и перемещение car_events__instance_created_date__gt=six_days_ago
в filter
:
.filter(some_conditions_on_fields=False, car_events__instance_created_date__gt=six_days_ago)
Но, конечно, это отфильтрует автомобили без событий, что не то, что нам нужно - но это супер быстро!
Я немного повозился с этим в raw sql и пришел к хорошему рабочему примеру, который я теперь хотел бы написать в ORM, поскольку мы не хотим использовать rawsql. Этот запрос занимает 2.2s
, что находится в нашей допустимой границе, но гораздо меньше, чем 18 минут.
SELECT
"core_car"."id",
COUNT(DISTINCT "analytics_carevent"."id") AS "num_cars_view",
FROM
"core_car"
LEFT JOIN "analytics_carevent" ON ("core_car"."id" = "analytics_carevent"."car_id" AND "analytics_carevent"."event" = 'view' AND "analytics_carevent"."instance_created_date" > '2022-05-14T00:00:00+02:00'::timestamptz
AND "analytics_carevent"."instance_created_date" <= '2022-05-15T00:00:00+02:00'::timestamptz)
WHERE (some conditions that dont matter)
GROUP BY "core_car"."id";
Мой вопрос теперь в следующем:
Как я могу сделать вышеприведенный запрос в ORM?
Мне нужно поместить "фильтр" или условия в left join
. Если я использую filter()
, то он просто поместит их в предложение where
, что неправильно.
Я пробовал:
two_days_ago = (timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=2))
cars = Car.objects.prefetch_related(
'car_events',
).filter(some_filters,)
cars = cars.annotate(events=FilteredRelation('car_events')).filter(car_events__car_id__in=cars.values_list("id", flat=True), car_events__instance_created_date__gt=six_days_ago)
Но я не думаю, что это правильно. Мне также нужен счетчик аннотаций.
Используется Django 4 и последний релиз python на момент написания статьи. :)
Большое спасибо!
TLDR: Наложение фильтра или условия на LEFT JOIN
в django, вместо queryset.filter()