Django annotation ve aggregation

Django’da annotate ve aggregate kullanarak, veritabani tablosundaki satırların toplamı, ortalaması gibi, birden çok satırdan veya ilgili diğer tablolardaki değerler uzerinden database seviyesinde hesaplama yapip tek sorguda sonucu alabiliriz.

Asagidaki gibi Flat ve FlatDebit adinda iki modelimiz oldugunu varsayalim. Yaziyi bu modeller uzerinden ornekler vererek anlaymaya calisacagim.

class Flat(models.Model):
    name = models.CharField(max_length=30)

class FlatDebit(models.Model):
    flat = models.ForeignKey('myapp.Flat', related_name='flat_debits')
    amount = models.DecimalField(decimal_places=2, max_digits=9)

Aggregation

Aggregate result olarak tek satir veri dondurur.

Aggragete kullanimina ornekler:

# Dairenin borclarinin toplami
result = FlatDebit.objects.filter(flat_id=1).aggregate(
    total_amount=Sum('amount')
)['total_amount']

# Dairenin kac adet borcu var?
result = Flat.objects.filter(flat_id=1).aggregate(
    debit_count=Count('flat_debits')
)['debit_count']

Dikkat ettiyseniz aggragation sorgusundan queryset yerine tek satir veri dict olarak donduruyor.

Sunuda unutmamak gerekir ki aggregate sorgusundan bir sonuc donmeyedebilir. Mesela yukaridaki sorgulardan birinde herhangi bir sonuc donmez ise dictteki elemana ulasmaya calistigimda hata verecektir. Sonuc donmemesi durumunda default bir deger dondurmek guzel bir yontemdir. Bu sayede ekstradan kontrol etmemize gerek kalmaz. SQL sorgusu sonucunda bir sonuc donmemesi durumunda default bir deger donmesi icin Coalesce kullanilir.

Annotation

Mesela dairelerin borclarinin listelendigi bir sayfamiz oldugunu varsayalim. Sayfada her dairenin ismini ve o dairenin nekadar borcu oldugunu yan yana gostermek istiyoruz diyelim.

Django annotation kullanmadan bunu yapmak istedigimizde once daireleri cekip daha sonra her dairenin nekadar borcu oldugunu cekeriz:

flats = Flat.objects.all()

for flat in flats:
    total_debit = flat.flat_debits.aggregate(
        total=Sum('amount')
    )['total']
    print(flat.name, total_debit)

Bu sekilde kullandigimizda ilk basta butun daireleri cekmek icin daha sonra ise her daireye ait borclarin toplamini icin database’e istek yapiyoruz. Sistemde toplamda 100 adet daire varsa 101 defa database’e istek yapicaktir. Bu islem cok maliyetli bir islem. Bunu tek sorguda yapmak icin annotation kullanilabilir.

flats = Flat.objects.all().annotate(total_debit=Sum('flat_debits__amount'))
for flat in flats:
    print(flat.name, flat.total_debit)

Burda daireleri getirirken her dairenin uzerinde o daireye ait borclarin toplamini getirmis oluyoruz, sadece 1 query.

Annotate ile hesaplanan fieldlari kullanmak

Dairelerin borclarinin listelendigi sayfaya dairelerin borc tutarina gore filtreleme eklemek istedigimizi varsayalim. Kullanici minimum 1000TL borcu olan daireleri listelemek istiyor diyelim.

Bunun icin once her dairenin borclarinin toplamini alip daha sonra dairenin borcu kullanicinin girdigi tutardan buyukse onlari listeleyelim.


flats = Flat.objects.annotate(total_debit=Sum('flat_debits__amount')).filter(total_debit__gte=1000
)

Burda once daireleri cekerken borclarinin toplamini total_debit adinda bir fielda atiyoruz. Daha sonra da bu fielda gore filtreleme yapiyoruz.

Annotate ile hesapladigimiz fieldi filter, annotate veya aggragate icerisinde kullanabilecegimizi unutmamak gerekir. Mesela her flatin kendi uzerinde borclarini annotate ile hesapliyoruz. Raporun altinda toplam borcu gostermek icin ise annotate ile hesapladigimiz fieldi aggregatee veriyoruz.

flats = Flat.objects.all().annotate(
    total_debit=Sum('flat_debits__amount')
)

Yukaridaki annotation icerisinnde zaten her flat icin yaptigimiz hesaplanan degerleri toplayarak toplam borcu alabiliyoruz.

flats.aggregate(
    total=Coalesce(Sum('total_debit'), 0)
)['total']

Hatali sonuca sebep olabilecek annotation/aggragation kullanimi

Bir queryset uzerinde birden fazla tablo ile iliskili annotation oldugu durumda hatali sonuc donebiliyor. Mesela borclari toplam tutarlari ve faiz tutarlarini biryerde gostermek istiyoruz. Bir borca birden fazla faiz islemis olabilir. Asagidaki queryseti incelersek ann_debit_amount fieldini hesaplamak icin accrual relation’ina gidiyoruz. Daha sonra ayni queryset icerisinde ann_interest_amount fieldi icin flat_interests relationina gidiyoruz. Eger ilgili objenin birtane flat_interest‘i varsa sikinti cikmaz ama birden fazla varsa ayni flat_interest iki defa gelicektir. Yani 4TL+6TL olmak uzere 10TLlik faiz varken faizi 20TL olarak hesaplayacaktir.

# Borc hesaplamalari baslangic
flat_debits = flat_debits.annotate(
    ann_debit_amount=Coalesce(Sum(
        Case(When(accrual__due_date__lte=due_date, then='amount'))
    ), 0),
    ann_interest_amount=Coalesce(Sum('flat_interests__amount'), 0),
).annotate(
    ann_total_amount=F('ann_debit_amount') + F('ann_interest_amount'),
)

Bunu cozmek icin faizler icin ekstra bir query atarak farkli querysetleri in_bulk ile birlestirerek cozebiliyoruz.

flat_debits = flat_debits.annotate(
    ann_debit_amount=Coalesce(Sum(
        Case(When(due_date__lte=due_date, then='amount'))
    ), 0),
)

flat_interests = flat_debits.annotate(
    ann_interest_amount=Coalesce(Sum('flat_interests__amount'), 0),
).in_bulk()

for debit in flat_debits:
    debit.dyn_interest_amount = flat_interests[debit.pk].ann_interest_amount
Load comments