web-dev-qa-db-fra.com

Django: groupe de requêtes par mois

Comment calculer le total par mois sans utiliser de supplément?

J'utilise actuellement:

  • Django 1.8
  • postgre 9.3.13
  • Python 2.7

Exemple.

enter image description here

Ce que j'ai essayé jusqu'à présent.

#Doesn't work for me but I don't mind because I don't want to use extra
truncate_month = connection.ops.date_trunc_sql('month','day')
invoices = Invoice.objects.filter(is_deleted = False,company = company).extra({'month': truncate_month}).values('month').annotate(Sum('total'))

----
#It works but I think that it's too slow if I query a big set of data
for current_month in range(1,13):
    Invoice.objects.filter(date__month = current__month).annotate(total = Sum("total"))

et aussi celui-ci, la réponse semble excellente mais je ne peux pas importer le module TruncMonth.

Django: Grouper par date (jour, mois, année)


P.S. Je sais que cette question est déjà posée plusieurs fois mais je ne vois aucune réponse.

Merci!


[~ # ~] solution [~ # ~] :

Merci à la réponse de @ Vin-G.

enter image description here

17
aldesabido

Tout d'abord, vous devez créer une fonction qui peut extraire le mois pour vous:

from Django.db import models
from Django.db.models import Func

class Month(Func):
    function = 'EXTRACT'
    template = '%(function)s(MONTH from %(expressions)s)'
    output_field = models.IntegerField()

Après cela, tout ce que vous devez faire est

  1. annoter chaque ligne avec le mois
  2. regrouper les résultats par mois annoté à l'aide de values()
  3. annoter chaque résultat avec la somme agrégée des totaux à l'aide de Sum()

Important : si votre classe de modèle a un ordre par défaut spécifié dans les méta-options, alors vous devrez ajouter une clause order_by() vide . C'est à cause de https://docs.djangoproject.com/en/1.9/topics/db/aggregation/#interaction-with-default-ordering-or-order-by

Les champs qui sont mentionnés dans la partie order_by() d'un ensemble de requêtes (ou qui sont utilisés dans l'ordre par défaut sur un modèle) sont utilisés lors de la sélection des données de sortie, même s'ils ne sont pas autrement spécifiés dans la fonction values() appel. Ces champs supplémentaires sont utilisés pour regrouper les résultats "similaires" et ils peuvent faire apparaître des lignes de résultats autrement identiques comme étant distinctes.

Si vous n'êtes pas sûr, vous pouvez simplement ajouter la clause order_by() vide de toute façon sans aucun effet négatif.

c'est à dire.

from Django.db.models import Sum

summary = (Invoice.objects
              .annotate(m=Month('date'))
              .values('m')
              .annotate(total=Sum('total'))
              .order_by())

Voir l'intégralité de Gist ici: https://Gist.github.com/alvingonzales/ff9333e39d221981e5fc4cd6cdafdd17

Si vous avez besoin de plus d'informations:

Détails sur la création de vos propres classes Func: https://docs.djangoproject.com/en/1.8/ref/models/expressions/#func-expressions

Détails sur la clause values ​​(), (faites attention à la façon dont elle interagit avec annotate () par rapport à l'ordre des clauses): https://docs.djangoproject.com/en/1.9/topics/db/ agrégation/# valeurs

l'ordre dans lequel les clauses annotate () et values ​​() sont appliquées à une requête est significatif. Si la clause values ​​() précède l'annotate (), l'annotation sera calculée en utilisant le regroupement décrit par la clause values ​​().

27
Vin-G

itertools.groupby est l'option performante de Python et peut être utilisée avec une seule requête db:

from itertools import groupby

invoices = Invoice.objects.only('date', 'total').order_by('date')
month_totals = {
    k: sum(x.total for x in g) 
    for k, g in groupby(invoices, key=lambda i: i.date.month)
}
month_totals
# {1: 100, 3: 100, 4: 500, 7: 500}

Je ne connais pas de pure solution Django ORM. Le date__month le filtre est très limité et ne peut pas être utilisé dans values, order_by, etc.

4
schwobaseggl

Je ne sais pas si ma solution est plus rapide que la vôtre. Vous devez le profiler. Néanmoins, je ne demande la base de données qu'une seule fois au lieu de 12 fois.

#utils.py
from Django.db.models import Count, Sum


def get_total_per_month_value():
    """
    Return the total of sales per month

    ReturnType: [Dict]
    {'December': 3400, 'February': 224, 'January': 792}
    """
    result= {}
    db_result = Sale.objects.values('price','created')
    for i in db_result:
        month = str(i.get('created').strftime("%B"))
        if month in result.keys():
            result[month] = result[month] + i.get('price')
        else:
            result[month] = i.get('price')
    return result

#models.py
class Sale(models.Model):
    price = models.PositiveSmallIntegerField()
    created = models.DateTimeField(_(u'Published'), default="2001-02-24")

#views.py
from .utils import get_total_per_month_value
# ...
result = get_total_per_month_value()

test.py

  #
    import pytest
    from mixer.backend.Django import mixer
    #Don't try to write in the database
    pytestmark = pytest.mark.Django_db
    def test_get_total_per_month():
        from .utils import get_total_per_month_value
        selected_date = ['01','02','03','01','01']
        #2016-01-12 == YYYY-MM-DD
        for i in selected_date:
            mixer.blend('myapp.Sale', created="2016-"+i+"-12")
        values = get_total_per_month_value() #return a dict
        months = values.keys()
        assert 'January' in months, 'Should include January'
        assert 'February' in months, 'Should include February'
        assert len(months) == 3, 'Should aggregate the months'
1
result = (
    invoices.objects
        .all()
        .values_list('created_at__year', 'created_at__month')
        .annotate(Sum('total'))
        .order_by('created_at__year', 'created_at__month')
)
1
thosimo

N'oubliez pas que Django fournissent un gestionnaire natif datetimes , qui vous permet de retirer facilement tous les jours/semaines/mois/années) de tout ensemble de requêtes pour les modèles avec un champ datetime. Donc, si le modèle Invoice ci-dessus a un champ datetime created, et que vous voulez des totaux pour chaque mois dans votre ensemble de requêtes, vous pouvez simplement faire:

    invoices = Invoice.objects.all()
    months = invoices.datetimes("created", kind="month")
    for month in months:
        month_invs = invoices.filter(created__month=month.month)
        month_total = month_invs.aggregate(total=Sum("otherfield")).get("total")
        print(f"Month: {month}, Total: {month_total}")

Pas de fonctions externes ou deps nécessaires.

1
shacker