Лучший способ выбрать самый последний элемент для каждой группы в Django / Postgres - PullRequest
0 голосов
/ 23 января 2020

У меня есть проблема с производительностью запросов, которую я пытаюсь решить в Django.

Среда:

  • Django 2.2
  • Python 3.6
  • Postgresql 11

Примеры моделей:

class Location(models.Model):
    name = models.CharField(max_length=256)
    # ...

class VendingMachine(models.Model):
    location = models.ForeignKey("MyApp.Location", on_delete=models.CASCADE)
    name = models.CharField(max_length=8)
    # ...

class Vend(models.Model):
    vending_machine = models.ForeignKey("MyApp.VendingMachine", on_delete=models.PROTECT)
    vend_start_time = models.DateTimeField(db_index=True)
    # ...

Я пытаюсь получить список самых последних Vends на VendingMachine.

Есть несколько подходов, которые я выбрал, но они либо не совсем работают в моих настройках и требованиях, либо слишком долго исполняются. .

Версия 1:

Vend.objects.filter(pk__in=Subquery(Vend.objects.order_by().values('vendingmachine__location__id', 'vendingmachine__id').annotate(max_id=Max('id')).values('max_id')))

Эта версия очень быстрая. Тем не менее, это работает только в том случае, если идентификаторы Vend в хронологическом порядке. Данные вставляются в базу данных в случайном порядке, поэтому это не работает.

Версия 2:

Vend.objects.all().order_by('vendingmachine_id', '-vend_start_time').distinct('vendingmachine_id')

Эта версия занимает 12-15 секунд выполнить, и так как он выполняется через paginator, запрос выполняется дважды (один раз для подсчета, второй раз для получения объектов и нарезки), поэтому загрузка страницы занимает около 30 секунд, что слишком долго .
Другая проблема с этой версией заключается в том, что результаты не могут быть отсортированы (кроме как в Python) после того, как они возвращены, так как он полагается на order_by для сортировки vend_start_time для выбора последней.

Версия 3:

vend_sub_qs = Vend.objects.filter(vendingmachine_id=OuterRef("vendingmachine_id")).order_by("-vend_start_time").values_list("id", flat=True)[:1]
vend_qs = Vend.objects.filter(pk__in=Subquery(vend_sub_qs)).order_by("-vend_start_time")
vending_machines = VendingMachine.objects.prefetch_related(Prefetch("vend_set", queryset=vend_qs))

Я попробовал другой подход, в результате чего появился список торговых автоматов с их последними предварительно выбранными Vends. Это не очень хорошо работает, потому что мне действительно нужно завершить QuerySet of Vends.
Это также было чрезвычайно медленно и занимало около 45 секунд.

Резюме:

Важно, чтобы я завершил QuerySet объекта Vend и чтобы он мог быть отсортирован по различным полям в Vend.

Было бы идеально, если бы он мог выполняться за 5 секунд или меньше.

Можно использовать Django функции, которые Postgres специфицированы c.
Raw SQL также является опцией, если QuerySet все еще можно получить в конце.

Ответы [ 2 ]

0 голосов
/ 28 января 2020

Обратный способ получения результатов. означает, вместо того, чтобы запрашивать Vend go и извлекать VendingMachines и упорядочивать связанные торговые по времени

class VendingMachine(models.Model):
    location = models.ForeignKey("MyApp.Location", on_delete=models.CASCADE)
    name = models.CharField(max_length=8)
    # ...

# Note I added 'related_name' in here
class Vend(models.Model):
    vending_machine = models.ForeignKey("MyApp.VendingMachine", on_delete=models.PROTECT, related_name='vends')
    vend_start_time = models.DateTimeField(db_index=True)
    # ...

Тогда ваш запрос будет выглядеть следующим образом:

vending_machines = VendingMachine.objects.prefetch_related('vends').all()

Тогда примените предпочитаемый порядок и фильтры на VendingMachine или Vend с для каждого автомата.

Например, если вы перебираете торговые автоматы, вы делаете что-то вроде

for machine in vending_machines:
    most_recent_vends = machine.vends.order_by('-vend_start_time')
0 голосов
/ 28 января 2020

Я смог решить эту проблему, используя пользовательские SQL.
https://docs.djangoproject.com/en/dev/topics/db/sql/#executing -custom- sql -прямо

Raw SQL:

vends = Vend.objects.raw('SELECT * FROM "myapp_vend" WHERE (vendingmachine_id, vend_start_time) IN (SELECT vendingmachine_id, max(vend_start_time) FROM "myapp_vend" GROUP BY vendingmachine_id)')    

Это было выполнено за 2 секунды и правильно дало мне QuerySet объектов Vend.
Однако это был RawQuerySet, который не поддерживает вызовы order_by и annotation. Поскольку я передаю QuerySet в библиотеку, которая применяет упорядочение и аннотации для отображения таблицы, мне потребовался обычный QuerySet.

Custom SQL:

with connection.cursor() as cursor:
    cursor.execute('SELECT id FROM "myapp_vend" WHERE (vendingmachine_id, vend_start_time) IN (SELECT vendingmachine_id, max(vend_start_time) FROM "myapp_vend" GROUP BY vendingmachine_id)') 
    ids = [x[0] for x in cursor.fetchall()]
vends = Vend.objects.filter(id__in=ids)   

Только выбор идентификатора позволил мне затем выполнить обычный оператор фильтра Django, выбирающий идентификаторы, возвращаемые пользовательским SQL. Это дало мне обычный QuerySet, который можно было передать в библиотеку, в которой были добавлены order_by и аннотации, но для этого нужно выполнить два запроса.

...