Django: Agrupar por fecha (día, mes, año)

89

Tengo un modelo simple como este:

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

Y quiero generar un desglose mes a mes de:

  • Cuantas ventas hubo en un mes ( COUNT)
  • El valor combinado ( SUM)

No estoy seguro de cuál es la mejor manera de atacar esto. He visto algunas consultas de selección extra que parecen bastante aterradoras, pero mi mente simple me dice que podría estar mejor simplemente iterando números, comenzando desde un año / mes de inicio arbitrario y contando hasta llegar al mes actual, descartando simples filtrado de consultas para ese mes. Más trabajo de base de datos, ¡menos estrés del desarrollador!

¿Qué tiene más sentido para ti? ¿Existe una buena forma de recuperar una tabla rápida de datos? ¿O mi método sucio es probablemente la mejor idea?

Estoy usando Django 1.3. No estoy seguro de si han agregado una forma más agradable GROUP_BYrecientemente.

Oli
fuente

Respuestas:

219

Django 1.10 y superior

Listas de documentación de Django extracomo obsoleta en poco tiempo . (Gracias por señalar eso @seddonym, @ Lucas03). Abrí un boleto y esta es la solución que proporcionó 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 

versiones mas antiguas

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')

Ediciones

  • Cuenta agregada
  • Información agregada para django> = 1.10
tback
fuente
1
¿Qué backend de base de datos está utilizando? Funciona bien en postgres>>> qs.extra({'month':td}).values('month').annotate(Sum('total')) [{'total__sum': Decimal('1234.56'), 'month': datetime.datetime(2011, 12, 1, 0, 0)}]
tback
1
@seddonym Fixed (Gracias a jarshwah)
tback
1
Truncmonth no está disponible en Django 1.8
Sudhakaran Packianathan
2
gracias, funciona muy bien. caso de esquina para la versión anterior a 1.10: si uno se une / filtra en otros modelos que podrían tener el mismo campo (por ejemplo: marca de tiempo), entonces uno debe calificar completamente el campo -'{}.timestamp'.format(model._meta.db_table)
zsepi
1
Solo una nota rápida de que si la USE_TZconfiguración de Django es True, las dos versiones no son exactamente equivalentes. La versión que usa TruncMonthconvertirá la marca de tiempo a la zona horaria especificada por la TIME_ZONEconfiguración antes de truncar, mientras que la versión que usa date_trunc_sqltruncará la marca de tiempo UTC sin procesar en la base de datos.
Daniel Harding
32

Solo una pequeña adición a la respuesta de @tback: no funcionó para mí con Django 1.10.6 y postgres. Agregué order_by () al final para arreglarlo.

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
fuente
1
sí: docs.djangoproject.com/en/1.11/topics/db/aggregation/… ... no se siente como un buen diseño pero son muy, muy inteligentes esos tipos de django, así que en realidad lo es.
Williams
TruncDatele permite agrupar por fecha (día del mes)
Neil
10

Otro enfoque es utilizar ExtractMonth. Tuve problemas al usar TruncMonth debido a que solo se devolvió un valor de año de fecha y hora. Por ejemplo, solo se devolvieron los meses de 2009. ExtractMonth solucionó este problema perfectamente y se puede usar como se muestra a continuación:

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

El querysetes una lista de Orden, una línea por cada mes, la combinación de la suma de las ventas:sales_sum

@Django 2.1.7

CK
fuente
1

Este es mi método sucio. Está sucio.

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

Bien puede haber una mejor manera de recorrer años / meses, pero eso no es lo que realmente me importa :)

Oli
fuente
Por cierto, funcionará bien, pero sabes que el ciclo durante meses tampoco es una gran idea. ¿Qué pasa si alguien quiere hacerlo en el día de un mes, entonces este ciclo se repetirá de 30 a 31 días? de lo contrario, funciona bien
Mayank Pratap Singh
esto es demasiado lento si tiene millones de registros
diferente
@jifferent ¡Absolutamente! Lo agregué para mostrar cuál era mi solución al momento de publicar la pregunta. Las otras respuestas son mucho mejores.
Oli
0

Así es como puede agrupar datos por períodos de tiempo arbitrarios:

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
fuente
-1

Por mes:

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

Por año:

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

Por día:

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

No olvide importar Count

from django.db.models import Count

Para django <1.10

jatin
fuente
3
Sí, gran práctica, importar todo desde modelos
JC Rocamonde
Claramente estaba siendo irónico. Es una práctica horrible hacer eso. No deberías hacerlo y habría votado en contra solo por eso (que no hice)
JC Rocamonde