Django annotation ve aggregation

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

Aşağıdaki gibi Flat ve FlatDebit adında iki modelimiz olduğunu varsayalım. Yazıyı bu modeller üzerinden örnekler vererek anlatmaya çalışacağım.

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 satır veri döndürür.

Aggragete kullanımına örnekler:

# Dairenin borçlarının toplamı
result = FlatDebit.objects.filter(flat_id=1).aggregate(
    total_amount=Sum('amount')
)['total_amount']

# Dairenin kaç 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 satır veri dict olarak donduruyor.

Sunuda unutmamak gerekir ki aggregate sorgusundan bir sonuç dönmeyedebilir. Mesela yukarıdaki sorgulardan birinde herhangi bir sonuç dönmez ise dictteki elemana ulaşmaya çalıştığımda hata verecektir. Sonuç dönmemesi durumunda default bir değer döndürmek güzel bir yöntemdir. Bu sayede ekstradan kontrol etmemize gerek kalmaz. SQL sorgusu sonucunda bir sonuç dönmemesi durumunda default bir değer dönmesi için Coalesce kullanılır.

Annotation

Mesela dairelerin borçlarının listelendiği bir sayfamız olduğunu varsayalım. Sayfada her dairenin ismini ve o dairenin nekadar borcu olduğunu yan yana göstermek istiyoruz diyelim.

Django annotation kullanmadan bunu yapmak istediğimizde önce daireleri çekip daha sonra her dairenin nekadar borcu olduğunu çekeriz:

flats = Flat.objects.all()

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

Bu şekilde kullandığımızda ilk başta bütün daireleri çekmek için daha sonra ise her daireye ait borçların toplamını için database’e istek yapıyoruz. Sistemde toplamda 100 adet daire varsa 101 defa database’e istek yapıcaktır. Bu işlem çok maliyetli bir işlem. Bunu tek sorguda yapmak için annotation kullanılabilir.

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 üzerinde o daireye ait borçların toplamını getirmiş oluyoruz, sadece 1 query.

Annotate ile hesaplanan fieldlari kullanmak

Dairelerin borçlarının listelendiği sayfaya dairelerin borç tutarına göre filtreleme eklemek istediğimizi varsayalım. Kullanıcı minimum 1000TL borcu olan daireleri listelemek istiyor diyelim.

Bunun için önce her dairenin borçlarının toplamını alıp daha sonra dairenin borcu kullanıcının girdiği tutardan büyükse onları 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 üzerinde borçlarını annotate ile hesaplıyoruz. Raporun altında toplam borcu göstermek için ise annotate ile hesapladığımız fieldi aggregatee veriyoruz.

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

Yukarıdaki annotation içerisinnde zaten her flat için yaptığımız hesaplanan değerleri toplayarak toplam borcu alabiliyoruz.

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

Hatalı sonuca sebep olabilecek annotation/aggragation kullanımı

Bir queryset üzerinde birden fazla tablo ile ilişkili annotation olduğu durumda hatalı sonuç dönebiliyor. Mesela borçları toplam tutarları ve faiz tutarlarını biryerde göstermek istiyoruz. Bir borca birden fazla faiz işlemiş olabilir. Aşağıdaki queryseti incelersek ann_debit_amount fieldini hesaplamak için accrual relation’ına gidiyoruz. Daha sonra aynı queryset içerisinde ann_interest_amount fieldi için flat_interests relationina gidiyoruz. Eğer ilgili objenin birtane flat_interest‘i varsa sıkıntı çıkmaz ama birden fazla varsa aynı flat_interest iki defa gelicektir. Yani 4TL+6TL olmak üzere 10TLlik faiz varken faizi 20TL olarak hesaplayacaktır.

# Borç hesaplamaları başlangıç
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 çözmek için faizler için ekstra bir query atarak farklı querysetleri in_bulk ile birleştirerek çözebiliyoruz.

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