Можно ли с помощью django ORM сделать GROUP BY по нескольким столбцам отдельно и агрегировать каждый из них по другому столбцу?

Я знаю, как GROUP BY и агрегировать:

>>> from expenses.models import Expense
>>> from django.db.models import Sum
>>> qs = Expense.objects.order_by().values("is_fixed").annotate(is_fixed_total=Sum("price"))
>>> qs
<ExpenseQueryset [{'is_fixed': False, 'is_fixed_total': Decimal('1121.74000000000')}, {'is_fixed': True, 'is_fixed_total': Decimal('813.880000000000')}]>

Однако, если я хочу сделать то же самое для двух других столбцов, он возвращает только последний:

>>> qs = (
...     Expense.objects.order_by()
...     .values("is_fixed")
...     .annotate(is_fixed_total=Sum("price"))
...     .values("source")
...     .annotate(source_total=Sum("price"))
...     .values("category")
...     .annotate(category_total=Sum("price"))
... )
>>> qs
<ExpenseQueryset [{'category': 'FOOD', 'category_total': Decimal('33.9000000000000')}, {'category': 'GIFT', 'category_total': Decimal('628')}, {'category': 'HOUSE', 'category_total': Decimal('813.880000000000')}, {'category': 'OTHER', 'category_total': Decimal('307')}, {'category': 'RECREATION', 'category_total': Decimal('100')}, {'category': 'SUPERMARKET', 'category_total': Decimal('52.8400000000000')}]>

Возможно ли достичь желаемого с помощью одного запроса вместо трех?

Ответ - НЕТ, потому что это невозможно с SQL

Но вы можете использовать следующий подход в сочетании с кодированием на python:

Я не думаю, что это возможно даже в сыром SQL, потому что в каждом запросе вы можете группировать по одному или нескольким полям вместе, но не с раздельными результатами для каждого. Но это возможно сделать с помощью одного запроса и использовать небольшие коды python для объединения результатов в нужном вам формате. Ниже я описал, как вы можете использовать это шаг за шагом. и в следующем разделе написал метод python, который вы можете динамически использовать для любого дальнейшего применения.

Как это работает

Единственное простое решение, которое я могу упомянуть, это то, что вы группируете по тем 3 полям, которые вам нужны, и делаете простое программирование на python для суммирования результатов по каждому полю. В этом методе у вас будет только один запрос, но отдельные результаты для каждого поля group-by.

from expenses.models import Expense
from django.db.models import Sum

qs = Expense.objects.order_by().values("is_fixed", "source", "category").annotate(total=Sum("price"))

Теперь результат будет примерно таким, как показано ниже:

<ExpenseQueryset [{'category': 'FOOD', 'is_fixed': False, 'source': 'MONEY', 'total': Decimal('33.9000000000000')}, { ...}, 

Теперь мы можем просто агрегировать результат каждого поля путем итерации по этому результату

category_keys = []
for q in qs:
    if not q['category'] in category_keys:
        category_keys.append(q['category'])

# Now we have proper values of category in category_keys
category_result = []
for c in category_keys:
    value = sum(item['total'] for item in qs if item['category'] == c)
    category_result.append({'category': c, 'total': value)

И результат для поля category будет примерно таким:

[{'category': 'FOOD', 'total': 33.3}, {... other category results ...}

Теперь мы можем продолжить и сделать результат для других групп по полям is_fixed и source, как показано ниже:

source_keys = []
for q in qs:
    if not q['source'] in source_keys:
        source_keys.append(q['source'])
source_result = []
for c in source_keys:
    value = sum(item['total'] for item in qs if item['source'] == c)
    source_result.append({'source': c, 'total': value)

is_fixed_keys = []
for q in qs:
    if not q['is_fixed'] in is_fixed_keys:
        source_keys.append(q['is_fixed'])
is_fixed_result = []
for c in is_fixed_keys:
    value = sum(item['total'] for item in qs if item['is_fixed'] == c)
    is_fixed_result.append({'is_fixed': c, 'total': value)

Глобальное решение

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

def find_group_by_separated_by_keys(key_list):
    """ key_list in this example will be:
        key_list = ['category', 'source', 'is_fixed']
    """
    qs = Expense.objects.order_by().values(*tuple(key_list)).annotate(total=Sum("price"))
    qs = list(qs)
    result = []
    for key in key_list:
        key_values = []
        for item in qs:
            if not item[key] in key_values:
                key_values.append(item[key])
        
        key_result = []
        for v in key_values:
            value = sum(item['total'] for item in qs if item[key] == v)
            key_result.append({key: v, 'total': value})

        result.extend(key_result)
    return result

Теперь просто используйте его как показано ниже в вашем коде:

find_group_by_separated_by_keys(['category', 'source', 'is_fixed')

И он выдаст список значений в правильном формате, как вы хотели

Вернуться на верх