Можно ли с помощью 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')
И он выдаст список значений в правильном формате, как вы хотели