Джанго условный агрегат подзапроса - PullRequest
0 голосов
/ 25 июня 2018

Упрощенный пример структуры моей модели:

class Corporation(models.Model):
    ...

class Division(models.Model):
    corporation = models.ForeignKey(Corporation)

class Department(models.Model):
    division = models.ForeignKey(Division)
    type = models.IntegerField()

Теперь я хочу отобразить таблицу, в которой отображаются корпорации, в столбце которых будет указано количество отделов определенного типа, например, type=10.В настоящее время это реализовано с помощью помощника в модели Corporation, который извлекает их, например,

class Corporation(models.Model):
    ...
    def get_departments_type_10(self):
        return (
            Department.objects
            .filter(division__corporation=self, type=10)
            .count()
        )

. Проблема в том, что это абсолютно снижает производительность из-за проблемы N + 1.

Я пытался решить эту проблему с помощью select_related, prefetch_related, annotate и subquery, но я не смог получить нужные мне результаты.

В идеале каждый Corporation в наборе запросов должен быть аннотирован целым числом type_10_count, которое отражает количество отделов этого типа.

Я уверен, что мог бы что-то сделать с raw sql в .extra(), но документы объявляютчто это будет устаревшим (я на Django 1.11)

РЕДАКТИРОВАТЬ: Пример необработанного решения SQL

corps = Corporation.objects.raw("""
SELECT
*,
(
    SELECT COUNT(*)
    FROM foo_division div ON div.corporation_id = c.id
    JOIN foo_department dept ON dept.division_id = div.id
    WHERE dept.type = 10
) as type_10_count
FROM foo_corporation c
""")

Ответы [ 2 ]

0 голосов
/ 03 июля 2018

Я думаю, с помощью Subquery мы можем получить SQL, аналогичный предоставленному вами, с этим кодом

# Get amount of departments with GROUP BY division__corporation [1]
# .order_by() will remove any ordering so we won't get additional GROUP BY columns [2]
departments = Department.objects.filter(type=10).values(
    'division__corporation'
).annotate(count=Count('id')).order_by()

# Attach departments as Subquery to Corporation by Corporation.id.
# Departments are already grouped by division__corporation
# so .values('count') will always return single row with single column - count [3]
departments_subquery = departments.filter(division__corporation=OuterRef('id'))
corporations = Corporation.objects.annotate(
    departments_of_type_10=Subquery(
        departments_subquery.values('count'), output_field=IntegerField()
    )
)

Сгенерированный SQL равен

SELECT "corporation"."id", ... (other fields) ...,
  (
    SELECT COUNT("division"."id") AS "count"
    FROM "department"
    INNER JOIN "division" ON ("department"."division_id" = "division"."id") 
    WHERE (
      "department"."type" = 10 AND
      "division"."corporation_id" = ("corporation"."id")
    ) GROUP BY "division"."corporation_id"
  ) AS "departments_of_type_10"
FROM "corporation"

Некоторые проблемы здесь заключаются в том, чтоПодзапрос может быть медленным с большими таблицами.Тем не менее, оптимизаторы запросов к базе данных могут быть достаточно умными для продвижения подзапроса в OUTER JOIN, по крайней мере, я слышал, что PostgreSQL делает это.

1.GROUP BY с использованием .values ​​и .annotate

2.проблемы order_by ()

3.Subquery

0 голосов
/ 28 июня 2018

Вы должны быть в состоянии сделать это с выражением Case(), чтобы запросить количество отделов, которые имеют тип, который вы ищете:

from django.db.models import Case, IntegerField, Sum, When, Value

Corporation.objects.annotate(
    type_10_count=Sum(
        Case(
            When(division__department__type=10, then=Value(1)),
            default=Value(0),
            output_field=IntegerField()
        )
    )
)
...