Django: Kelompokkan menurut tanggal (hari, bulan, tahun)

94

Saya punya Model sederhana seperti ini:

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

Dan saya ingin menampilkan perincian bulan demi bulan dari:

  • Berapa banyak penjualan dalam sebulan ( COUNT)
  • Nilai gabungan ( SUM)

Saya tidak yakin apa cara terbaik untuk menyerang ini. Saya telah melihat beberapa pertanyaan ekstra-pilih yang tampak cukup menakutkan tetapi pikiran saya yang sederhana memberi tahu saya bahwa saya mungkin lebih baik hanya mengulang angka, mulai dari tahun / bulan awal yang sewenang-wenang dan menghitung hingga saya mencapai bulan saat ini, membuang yang sederhana kueri pemfilteran untuk bulan itu. Lebih banyak pekerjaan database - lebih sedikit stres pengembang!

Apa yang paling masuk akal bagi Anda? Adakah cara yang bagus untuk menarik kembali tabel data cepat? Atau apakah metode kotor saya mungkin ide terbaik?

Saya menggunakan Django 1.3. Tidak yakin apakah mereka telah menambahkan cara yang lebih baik GROUP_BYbaru - baru ini.

Oli
sumber

Jawaban:

225

Django 1.10 dan yang lebih baru

Django daftar dokumentasi extrasebagai usang segera . (Terima kasih telah menunjukkannya @seddonym, @ Lucas03). Saya membuka tiket dan inilah solusi yang diberikan 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 

Versi yang lebih lama

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

Editan

  • Hitungan ditambahkan
  • Menambahkan informasi untuk django> = 1.10
tback
sumber
1
backend database apa yang Anda gunakan - berfungsi dengan baik di 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 Tetap (Terima kasih kepada jarshwah)
tback
1
Truncmonth tidak tersedia di Django 1.8
Sudhakaran Packianathan
2
terima kasih, bekerja dengan baik. kasus sudut untuk versi pra-1.10: jika seseorang bergabung / memfilter pada model lain yang mungkin memiliki bidang yang sama (misalnya: stempel waktu), maka seseorang harus sepenuhnya memenuhi syarat bidang tersebut -'{}.timestamp'.format(model._meta.db_table)
zsepi
1
Hanya catatan singkat bahwa jika USE_TZpengaturan Django adalah True, kedua versi tersebut tidak sama persis. Versi yang menggunakan TruncMonthakan mengonversi stempel waktu ke zona waktu yang ditentukan oleh TIME_ZONEsetelan sebelum date_trunc_sqldipotong , sedangkan versi yang menggunakan akan memotong stempel waktu UTC mentah di database.
Daniel Harding
36

Hanya tambahan kecil untuk @tback jawaban: Itu tidak bekerja untuk saya dengan Django 1.10.6 dan postgres. Saya menambahkan order_by () di bagian akhir untuk memperbaikinya.

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
sumber
1
yup: docs.djangoproject.com/en/1.11/topics/db/aggregation/… ... tidak terasa seperti desain yang bagus tapi mereka sangat pintar, jadi sebenarnya begitu.
Williams
TruncDatememungkinkan Anda untuk mengelompokkan berdasarkan tanggal (hari dalam sebulan)
Neil
11

Pendekatan lain adalah dengan menggunakan ExtractMonth. Saya mengalami masalah saat menggunakan TruncMonth karena hanya satu nilai tahun waktu yang dikembalikan. Misalnya, hanya bulan-bulan tahun 2009 yang dikembalikan. ExtractMonth memperbaiki masalah ini dengan sempurna dan dapat digunakan seperti di bawah ini:

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

Ini querysetadalah daftar pesanan, satu baris per bulan, menggabungkan jumlah penjualan:sales_sum

@Djogja 2.1.7

CK
sumber
1

Inilah metode kotor saya. Ini kotor.

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

Mungkin ada cara yang lebih baik untuk mengulang tahun / bulan tetapi bukan itu yang saya pedulikan :)

Oli
sumber
BTW Ini akan bekerja dengan baik tetapi Anda tahu loop selama berbulan-bulan juga bukan ide yang bagus. Bagaimana jika seseorang ingin membuatnya di Hari dalam sebulan maka loop ini akan berulang selama 30-31 hari. jika tidak bekerja dengan baik
Mayank Pratap Singh
ini terlalu lambat jika Anda memiliki jutaan rekaman
berbeda dengan
@jifferent Benar-benar! Saya menambahkannya untuk menunjukkan apa solusi saya pada saat memposting pertanyaan. Jawaban lainnya jauh lebih baik.
Oli
0

Berikut adalah cara Anda dapat mengelompokkan data berdasarkan periode waktu yang berubah-ubah:

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
sumber
0

saya memiliki tabel pesanan di database saya. saya akan menghitung pesanan per bulan dalam 3 bulan terakhir

from itertools import groupby
from dateutil.relativedelta import relativedelta

date_range = datetime.now()-relativedelta(months=3)
aggs =Orders.objects.filter(created_at=date_range)\
            .extra({'date_created':"date(created_at)"}).values('date_created')

for key , group in groupby(aggs):
     print(key,len(list(group)))

create_at adalah bidang tanggal waktu. Dengan fungsi tambahan yang dilakukan adalah mengambil tanggal dari nilai-nilai datetime. saat menggunakan datetime kita mungkin tidak mendapatkan hitungan yang benar karena objek dibuat pada waktu yang berbeda dalam satu hari.

Putaran for akan mencetak tanggal dan jumlah hitungan

Sarath Chandran K
sumber
-1

Menurut bulan:

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

Berdasarkan Tahun:

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

Pada siang hari:

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

Jangan lupa untuk mengimpor Hitungan

from django.db.models import Count

Untuk django <1.10

jatinkumar patel
sumber
3
Ya, praktik yang bagus, impor semua dari model
JC Rocamonde
Saya jelas sedang ironis. Ini adalah praktik yang mengerikan untuk melakukan itu. Anda tidak boleh melakukannya dan saya akan memberikan suara negatif hanya untuk itu (yang tidak saya lakukan)
JC Rocamonde