Повторное использование подзапросов для заказа в Django ORM - PullRequest
2 голосов
/ 27 июня 2019

Я управляю собачьим салоном, где собаки подстригаются нечасто.Чтобы поощрить владельцев, я хотел бы разослать ваучеры на их следующий визит.Ваучер будет основан на том, была ли у собаки стрижка в течение последних 2 месяцев или 2 лет.Спустя 2 года мы можем предположить, что клиент потерян, и менее 2 месяцев назад слишком близко к своей предыдущей стрижке.Сначала мы нацелимся на владельцев, которые недавно посетили.

Моя базовая база данных - PostgreSQL.

from datetime import timedelta
from django.db import models
from django.db.models import Max, OuterRef, Subquery
from django.utils import timezone


# Dogs have one owner, owners can have many dogs, dogs can have many haircuts

class Owner(models.model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)


class Dog(models.model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name="dogs")
    name = models.CharField(max_length=255)


class Haircut(models.model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    dog = models.ForeignKey(Dog, on_delete=models.CASCADE, related_name="haircuts")
    at = models.DateField()


today = timezone.now().date()
start = today - timedelta(years=2)
end = today - timedelta(months=2)

Мне кажется, что проблему можно разбить на два запроса.Во-первых, это то, что объединяет собак владельца, которые в последний раз резали за последние 2 месяца до 2 лет.

dog_aggregate = Haircut.objects.annotate(Max("at")).filter(at__range=(start, end))

И затем объединяет результат этого с таблицей владельцев.

owners_by_shaggiest_dog_1 = Owner.objects # what's the rest of this?

В результате SQL похож на:

select
  owner.id,
  owner.name
from
  (
    select
      dog.owner_id,
      max(haircut.at) last_haircut
    from haircut
      left join dog on haircut.dog_id = dog.id
    where
      haircut.at
        between current_date - interval '2' year
            and current_date - interval '2' month
    group by
      dog.owner_id
  ) dog_aggregate
  left join owner on dog_aggregate.owner_id = owner.id
order by
  dog_aggregate.last_haircut asc,
  owner.name;

Благодаря некоторой тренировке мне удалось получить правильный результат с помощью:

haircut_annotation = Subquery(
    Haircut.objects
    .filter(dog__owner=OuterRef("pk"), at__range=(start, end))
    .order_by("-at")
    .values("at")[:1]
)

owners_by_shaggiest_dog_2 = (
    Owner.objects
    .annotate(last_haircut=haircut_annotation)
    .order_by("-last_haircut", "name")
)

Однако полученный SQL кажется неэффективным как новыйзапрос выполняется для каждой строки:

select
  owner.id,
  owner.name,
  (
    select
    from haircut
      inner join dog on haircut.dog_id = dog.id
    where haircut.at
            between current_date - interval '2' year
                and current_date - interval '2' month
      and dog.owner_id = (owner.id)
    order by
      haircut.at asc
    limit 1
  ) last_haircut
from
  owner
order by
  last_haircut asc,
  owner.name;

PS На самом деле я не управляю салоном для собак, поэтому не могу дать вам ваучер.Извините!

1 Ответ

1 голос
/ 27 июня 2019

Если я правильно понял, вы можете сделать запрос, например:

from django.db.models import <b>Max</b>

Owners.objects.filter(
    dogs__haircuts__at__range=(start, end)
).annotate(
    last_haircut=Max('dogs__haircuts__at')
).order_by('last_haircut', 'name')

Последняя стрижка должна быть здесь Max, так как время проходит, отметка времени становится больше.

Обратите внимание, что ваш запрос и этот запрос не исключают владельцев собак, которые были вымыты совсем недавно.Мы просто не принимаем это во внимание, когда вычисляем last_haircut.

. Если вы хотите исключить таких владельцев, вы должны создать запрос вроде:

from django.db.models import Max

Owners.objects.exclude(
    <b>dogs__haircuts__at__gt=end</b>
).filter(
    dogs__haircuts__at__range=(start, end)
).annotate(
    last_haircut=Max('dogs__haircuts__at')
).order_by('last_haircut', 'name')
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...