Django: Grupo por data (dia, mês, ano)

89

Eu tenho um modelo simples como este:

class Order(models.Model):
    created = model.DateTimeField(auto_now_add=True)
    total = models.IntegerField() # monetary value

E eu quero produzir um detalhamento mês a mês de:

  • Quantas vendas houve em um mês ( COUNT)
  • O valor combinado ( SUM)

Não tenho certeza de qual é a melhor maneira de atacar isso. Já vi algumas consultas extra-selecionadas de aparência bastante assustadora, mas minha mente simples está me dizendo que seria melhor apenas iterar números, começando de um ano / mês inicial arbitrário e contando até chegar ao mês atual, descartando simples filtragem de consultas para aquele mês. Mais trabalho de banco de dados - menos estresse do desenvolvedor!

O que faz mais sentido para você? Existe uma maneira legal de obter uma tabela de dados rápida? Ou meu método sujo é provavelmente a melhor ideia?

Estou usando o Django 1.3. Não tenho certeza se eles adicionaram uma maneira mais agradável GROUP_BYrecentemente.

Oli
fonte

Respostas:

219

Django 1.10 e superior

A documentação do Django lista extracomo obsoleta em breve . (Obrigado por apontar isso @seddonym, @ Lucas03). Abri um tíquete e esta é a solução fornecida pelo jarshwah.

from django.db.models.functions import TruncMonth
from django.db.models import Count

Sales.objects
    .annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
    .values('month')                          # Group By month
    .annotate(c=Count('id'))                  # Select the count of the grouping
    .values('month', 'c')                     # (might be redundant, haven't tested) select month and count 

versões mais antigas

from django.db import connection
from django.db.models import Sum, Count

truncate_date = connection.ops.date_trunc_sql('month', 'created')
qs = Order.objects.extra({'month':truncate_date})
report = qs.values('month').annotate(Sum('total'), Count('pk')).order_by('month')

Editar% s

  • Contagem adicionada
  • Adicionadas informações para django> = 1,10
tback
fonte
1
qual banco de dados você está usando - funciona bem no postgres>>> qs.extra({'month':td}).values('month').annotate(Sum('total')) [{'total__sum': Decimal('1234.56'), 'month': datetime.datetime(2011, 12, 1, 0, 0)}]
tback de
1
@seddonym corrigido (graças a jarshwah)
tback de
1
Truncmonth não está disponível no Django 1.8
Sudhakaran Packianathan
2
obrigado, funciona muito bem. Caso de canto para a versão pré-1.10: se alguém se junta / filtra em outros modelos que podem ter o mesmo campo (por exemplo: carimbo de data / hora), deve-se qualificar totalmente o campo -'{}.timestamp'.format(model._meta.db_table)
zsepi
1
Apenas uma nota rápida que se a USE_TZconfiguração do Django for True, as duas versões não são exatamente equivalentes. A versão usando TruncMonthirá converter o carimbo de data / hora para o fuso horário especificado pela TIME_ZONEconfiguração antes de truncar, enquanto a versão usando date_trunc_sqlirá truncar o carimbo de data / hora UTC bruto no banco de dados.
Daniel Harding de
32

Apenas uma pequena adição à resposta @tback: Não funcionou para mim com Django 1.10.6 e postgres. Eu adicionei order_by () no final para consertar.

from django.db.models.functions import TruncMonth
Sales.objects
    .annotate(month=TruncMonth('timestamp'))  # Truncate to month and add to select list
    .values('month')                          # Group By month
    .annotate(c=Count('id'))                  # Select the count of the grouping
    .order_by()
Rani
fonte
1
sim: docs.djangoproject.com/en/1.11/topics/db/aggregation/… ... não parece um bom design, mas eles são muito, muito inteligentes aqueles caras django, então realmente é.
Williams
TruncDatepermite que você agrupe por data (dia do mês)
Neil
10

Outra abordagem é usar ExtractMonth. Tive problemas ao usar TruncMonth devido ao retorno de apenas um valor de ano datetime. Por exemplo, apenas os meses de 2009 foram devolvidos. ExtractMonth corrigiu esse problema perfeitamente e pode ser usado como a seguir:

from django.db.models.functions import ExtractMonth
Sales.objects
    .annotate(month=ExtractMonth('timestamp')) 
    .values('month')                          
    .annotate(count=Count('id'))                  
    .values('month', 'count')  
Tartaruga
fonte
2
    metrics = {
        'sales_sum': Sum('total'),
    }
    queryset = Order.objects.values('created__month')
                               .annotate(**metrics)
                               .order_by('created__month')

A queryseté uma lista de Ordem, uma linha por mês, combinando a soma das vendas:sales_sum

@Django 2.1.7

CK
fonte
1

Aqui está meu método sujo. Está sujo.

import datetime, decimal
from django.db.models import Count, Sum
from account.models import Order
d = []

# arbitrary starting dates
year = 2011
month = 12

cyear = datetime.date.today().year
cmonth = datetime.date.today().month

while year <= cyear:
    while (year < cyear and month <= 12) or (year == cyear and month <= cmonth):
        sales = Order.objects.filter(created__year=year, created__month=month).aggregate(Count('total'), Sum('total'))
        d.append({
            'year': year,
            'month': month,
            'sales': sales['total__count'] or 0,
            'value': decimal.Decimal(sales['total__sum'] or 0),
        })
        month += 1
    month = 1
    year += 1

Pode haver uma maneira melhor de repetir anos / meses, mas não é exatamente isso que me interessa :)

Oli
fonte
Aliás, vai funcionar bem, mas você sabe que o loop ao longo dos meses também não é uma boa ideia. E se alguém quiser fazer isso no dia de um mês, esse loop irá iterar por 30-31 dias. caso contrário, está funcionando bem
Mayank Pratap Singh
isso é muito lento se você tiver milhões de registros
dia
@jifferent Absolutamente! Eu o adicionei para mostrar qual era minha solução no momento de postar a pergunta. As outras respostas são muito melhores.
Oli
0

Aqui está como você pode agrupar dados por períodos arbitrários de tempo:

from django.db.models import F, Sum
from django.db.models.functions import Extract, Cast
period_length = 60*15 # 15 minutes

# Annotate each order with a "period"
qs = Order.objects.annotate(
    timestamp=Cast(Extract('date', 'epoch'), models.IntegerField()),
    period=(F('timestamp') / period_length) * period_length,
)

# Group orders by period & calculate sum of totals for each period
qs.values('period').annotate(total=Sum(field))
Max Malysh
fonte
-1

Por mês:

 Order.objects.filter().extra({'month':"Extract(month from created)"}).values_list('month').annotate(Count('id'))

Por ano:

 Order.objects.filter().extra({'year':"Extract(year from created)"}).values_list('year').annotate(Count('id'))

Por dia:

 Order.objects.filter().extra({'day':"Extract(day from created)"}).values_list('day').annotate(Count('id'))

Não se esqueça de importar Count

from django.db.models import Count

Para django <1,10

jatin
fonte
3
Sim, ótima prática, importe todos dos modelos
JC Rocamonde
Eu estava claramente sendo irônico. É uma prática horrível fazer isso. Você não deveria fazer isso e eu teria votado contra apenas por isso (o que não fiz)
JC Rocamonde