Django Annotate ve Aggregate Nedir?¶
Bu yazımızda Django'da aggregate ve annotate kullanımını öğreneceğiz.
Tanımlar¶
Aggregation, bir şeyleri gruplama, kümele, bir araya getirme sürecidir. Örneklerimizde veritabanı tablolarını bir araya getirmeyi django queryset'ler ile yapacağız. Annotation, bir yazıya ya da resme kısa bir açıklama ya da not eklemedir. Örneklerimizde django'daki model sınıf instance'a ekstra attribute ekleyerek kullanışlı bilgiler alacağız.
models.py¶
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
pubdate = models.DateField()
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
In [1]: import datetime
In [2]: from decimal import Decimal
In [3]: from django.db.models import *
In [4]: from storebook.models import Book, Store, Author, Publisher
In [5]: Book.objects.count() # Kac adet kitap var ?
Out[5]: 10
In [6]: Book.objects.filter(publisher__name="İz Yayıncılık") # iz yayincilik'a ait kitaplari getir
Out[6]: <QuerySet [<Book: Book object (10)>]>
In [7]: Book.objects.filter(publisher__name__istartswith="Polen") # polen yayincilik'a ait kitaplari getir
Out[7]: <QuerySet [<Book: Book object (9)>, <Book: Book object (16)>]>
In [8]: Book.objects.filter(publisher__name__istartswith="Polen").count() # polen yayincilik'a ait kitap sayisi
Out[8]: 2
In [9]: # Butun kitaplarin ortalama fiyati
In [10]: Book.objects.all().aggregate(Avg('price'))
Out[10]: {'price__avg': Decimal('21.5380000000000')}
In [11]: Book.objects.all().aggregate(ortalama_fiyat=Avg('price')) # ortalama_fiyat key'i olarak goster
Out[11]: {'ortalama_fiyat': Decimal('21.5380000000000')}
In [12]: Book.objects.aggregate(en_pahali=Max('price')) # en pahali kitap
Out[12]: {'en_pahali': Decimal('32.2900000000000')}
In [14]: Book.objects.aggregate(en_ucuz=Min('price')) # en ucuz kitap fiyati
Out[14]: {'en_ucuz': Decimal('13.75')}
In [15]: # En pahali kitap ortalama bir kitaptan ne kadar fazla ?
In [16]: Book.objects.aggregate(fiyat_farki=Max('price')-Avg('price'))
Out[16]: {'fiyat_farki': Decimal('10.7520000000000')}
In [17]: 32.29 - 21.538
Out[17]: 10.751999999999999
In [18]: # Her bir yayin evine ait kitap sayisini bulmak icin annotate kullanalim
In [19]: Publisher.objects.annotate(kitap_sayisi=Count('book'))
Out[19]: <QuerySet [<Publisher: Publisher object (1)>, <Publisher: Publisher object (2)>, <Publisher: Publisher object (3)>, <Publisher: Publisher object (4)>, <Publisher: Publisher object (5)>, <Publisher: Publisher object (6)>, <Publisher: Publisher object (7)>]>
In [20]: publishers = Publisher.objects.annotate(kitap_sayisi=Count('book'))
In [22]: [print("Kitap Adedi: ",p.kitap_sayisi," - Yayinevi", p.name) for p in publishers]
Kitap Adedi: 1 - Yayinevi Seha Yayıncılık
Kitap Adedi: 1 - Yayinevi İletişim Yayınları
Kitap Adedi: 2 - Yayinevi Büyük Doğu Yayınları
Kitap Adedi: 2 - Yayinevi Polen Yayınları
Kitap Adedi: 2 - Yayinevi Diriliş Yayınları
Kitap Adedi: 1 - Yayinevi İz Yayıncılık
Kitap Adedi: 1 - Yayinevi Beyan Yayınları
Out[22]: [None, None, None, None, None, None, None]
In [23]: # kitap sayisi en fazla olan 3 tane yayin evini kitap sayisina gore sirali sekilde alalim
In [24]: publishers = Publisher.objects.annotate(kitap_sayisi=Count('book')).order_by('-kitap_sayisi')[:3]
In [25]: print(publishers[0].name, publishers[0].kitap_sayisi)
Büyük Doğu Yayınları 2
In [26]: print(publishers[1].name, publishers[1].kitap_sayisi)
Polen Yayınları 2
In [27]: print(publishers[2].name, publishers[2].kitap_sayisi)
Diriliş Yayınları 2
In [28]: print(publishers[3].name, publishers[3].kitap_sayisi)
#---------------------------------------------------------------------------
# IndexError: list index out of range -> hatasi alinir cunku [:3] diyerek sadece 3 tane yayin evi aldik
Aggregate¶
Queryset sonucuna göre aggregation yapılabilir. Book.objects.all().aggregate(Avg('price')) gibi. Book.objects.aggregate(Avg('price')) şeklinde de aggregation yapılabilir. aggregate() key-value çiftine sahip bir dictionary return eder. Aggregate değeri için manuel isim vermek isterseniz Book.objects.aggregate(ortalama_fiyat=Avg('price'))şeklinde kullanabilirsiniz. Return edilen veri {'ortalama_fiyat': 34.35} gibi olacaktır. Birden fazla aggregate oluşturmak için Book.objects.aggregate(Avg('price'), Max('price'), Min('price')) yapısını kullanabilirsiniz. Return edilen veri: {'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')} gibi olacaktır.
Annotate¶
Bir queryset'te her bir obje için bağımsız özet bir veri üretmek için kullanılır. Örneğin; bir kitap listesinde her bir kitaba kaç tane yazarın katkıda bulunduğunu bilmek için annotate yapısı kullanılabilir.
>>> from django.db.models import Count
>>> qs = Book.objects.annotate(yazar_sayisi=Count('authors'))
# queryset'teki ilk obje icin yazar sayisini sorgulama
>>> qs[0]
<Book: Kitap Deneme 2>
>>> qs[0].yazar_sayisi
2
# queryset'teki ikinci obje icin yazar sayisini sorgulama
>>> qs[1]
<Book: Kitap Deneme 3>
>>> qs[1].yazar_sayisi
1
annotate ile birden fazla aggregation birleştirme¶
- Subquery'ler yerine join'ler kullanıldığı için birden fazla aggregation birleştirme hatalı sonuçlar çıkarmaktadır.
- Bir çok aggregate için bu problemi gidermenin yolu yoktur, ancak Count aggregate'e ait distinct parametresi yardımcı olabilir.
In [45]: q = Book.objects.annotate(Count('authors', distinct=True),Count('store',distinct=True))
In [46]: q[0].authors__count
Out[46]: 1
In [47]: q[0].store__count
Out[47]: 2
# Her bir yayinevine ait butun kitaplarin icindeki max ve min fiyatini gorelim
qs = Store.objects.annotate(minfiyat=Min('books__price'),maxfiyat=Max('books__price'))
>>> [print(q.minfiyat,q.maxfiyat,q.name) for q in qs]
13.75 32.2900000000000 Kitap Yurdu
15.8700000000000 30.2300000000000 Idefix
13.75 32.2900000000000 D&R
In [65]: qs = Store.objects.annotate(en_genc_yazar=Min('books__authors__age'))
In [66]: [print(q.en_genc_yazar, q.name) for q in qs]
71 Kitap Yurdu
77 Idefix
58 D&R
# aggregate ile en genc yazar
In [92]: qs = Store.objects.aggregate(en_genc_yazar=Min('books__authors__age'))
In [93]: qs
Out[93]: {'en_genc_yazar': 58}
Backward relationship aggregation¶
İlişkili modellerin lowercase sınıf adını ve double underscore notasyonunu kullanarak aggregation yapılabilir. Publisher -> Book arasındaki ilişkiyi yukarıda belirtmiştik.
Örneğin her bir yayınevine ait kitap sayısını öğrenmek için;
In [102]: qs = Publisher.objects.annotate(Count('book'))
In [103]: qs[0].book__count
Out[103]: 1
In [104]: qs[1].book__count
Out[104]: 1
In [105]: qs[2].book__count
Out[105]: 2
In [116]: qs_sum = Publisher.objects.annotate(Sum('book__pages'))
In [118]: [print(q.book__pages__sum, q.name) for q in qs_sum]
270 Seha Yayıncılık
339 İletişim Yayınları
1120 Büyük Doğu Yayınları
584 Polen Yayınları
276 Diriliş Yayınları
136 İz Yayıncılık
208 Beyan Yayınları
In [121]: Publisher.objects.aggregate(en_eski_yayin=(Min('book__pubdate')))
Out[121]: {'en_eski_yayin': datetime.date(1992, 1, 1)}
In [122]: Publisher.objects.aggregate(en_yeni_yayin=(Max('book__pubdate')))
Out[122]: {'en_yeni_yayin': datetime.date(2021, 2, 22)}
# her bir yazarin spesifik ortalamasi
In [126]: qa = Author.objects.annotate(ortalama_oran=Avg('book__rating'))
In [127]: [print(q.ortalama_oran, q.name) for q in qa]
4.75 Necip Fazıl Kısakürek
4.9 Rasim Özdenören
4.95 Sezai Karakoç
4.7 Cemil Meriç
5.0 İbn Kayyım El Cevziyye
4.9 İhsan Süreyya Sırma
Out[127]: [None, None, None, None, None, None]
# butun yazarlarin ortalamasi
In [128]: Author.objects.aggregate(ortalama_oran=Avg('book__rating'))
Out[128]: {'ortalama_oran': 4.88}
In [131]: qa = Author.objects.annotate(kitapsayisi=Count('book'))
In [132]: [print(q.kitapsayisi, q.name) for q in qa]
2 Necip Fazıl Kısakürek
1 Rasim Özdenören
2 Sezai Karakoç
1 Cemil Meriç
2 İbn Kayyım El Cevziyye
2 İhsan Süreyya Sırma
Out[132]: [None, None, None, None, None, None]
In [133]: qaggr = Author.objects.aggregate(kitapsayisi=Count('book'))
In [134]: qaggr
Out[134]: {'kitapsayisi': 10}
Aggregation ve diğer QuerySet ibareleri¶
Aggregate ile filter() veya exlude() ibaresini kullanabilirsiniz.
Örneğin Ç ile başlayan kitapların ortalama fiyatını aşağıdaki şekilde alabiliriz
In [138]: Book.objects.filter(name__startswith='Ç').aggregate(Avg('price'))
Out[138]: {'price__avg': Decimal('28.7900000000000')}
In [155]: qa = Book.objects.filter(name__startswith='Y').annotate(yayinci_sayisi=Count('store'))
In [156]: [print(q.yayinci_sayisi, q.name) for q in qa]
2 Yitik Cennet
In [159]: qa = Book.objects.filter(name__startswith='Ç').annotate(yayinci_sayisi=Count('store'))
In [160]: [print(q.yayinci_sayisi, q.name) for q in qa]
2 Çöle İnen Nur
3 Çile
Annotation'ların filtrelenmesi¶
filter() veya exclude() ibareleri annotate ile kullanılabilir.
Her bir kitap satış sitesindeki kitapların adedinin bilgisi ve adedi 5'ten büyük eşit olanların bilgisi
In [172]: qa = Store.objects.annotate(num_book=Count('books'))
In [173]: [print(q.num_book, q.name) for q in qa]
8 Kitap Yurdu
4 Idefix
5 D&R
In [174]: qa = Store.objects.annotate(num_book=Count('books')).filter(num_book__gte=5)
In [175]: [print(q.num_book, q.name) for q in qa]
8 Kitap Yurdu
5 D&R
In [179]: yuksek_puanli = Count('book',filter=Q(book__rating__gte=4.8))
In [180]: qa = Author.objects.annotate(num_books=Count('book'), yuksek_puanli_kitaplar=yuksek_puanli)
In [181]: [print(q.num_books, q.yuksek_puanli_kitaplar ,q.name) for q in qa]
2 1 Necip Fazıl Kısakürek # 2 kitabindan 1 tanesi 4.8 den buyuk ve esit
1 1 Rasim Özdenören
2 2 Sezai Karakoç
1 0 Cemil Meriç # 1 kitabi var hicbiri 4.8'den buyuk ve esit DEGIL!
2 2 İbn Kayyım El Cevziyye
2 2 İhsan Süreyya Sırma
filter parametresini 2 veya daha fazla aggregation ibarelerinde farklı condition'larda kullanım önerilir.
annotate() ve filter() ibarelerinin sırası¶
Kompleks querysetler geliştirirken annotate ve filter ibarelerinin sırasına dikkat etmek gereklidir.
#once ortalama alinir daha sonra en az 1 tane kitabin rate'i 4.'den buyuk olan(lar) gelir
In [200]: qa = Publisher.objects.annotate(ort_rate=Avg('book__rating')).filter(book__rating__gt=4.0)
In [201]: [print(q.ort_rate ,q.name) for q in qa]
4.7 İletişim Yayınları
4.75 Büyük Doğu Yayınları
5.0 Polen Yayınları
3.95 Diriliş Yayınları # (3.0+4.9)/2 !!!
4.9 İz Yayıncılık
4.1 Beyan Yayınları
# burada once kitap rate'i 4.0'dan buyuk olanlar alinir daha sonra ortalamalar alinir
In [202]: qa = Publisher.objects.filter(book__rating__gt=4.0).annotate(ort_rate=Avg('book__rating'))
In [203]: [print(q.ort_rate ,q.name) for q in qa]
4.7 İletişim Yayınları
4.75 Büyük Doğu Yayınları
5.0 Polen Yayınları
4.9 Diriliş Yayınları # 4.9/1 !!!
4.9 İz Yayıncılık
4.1 Beyan Yayınları
order_by ile annotate kullanımı¶
In [9]: qa = Store.objects.annotate(kitapsayisi=Count('books')).order_by('kitapsayisi')
In [10]: [print(q.kitapsayisi, q.name) for q in qa ]
4 Idefix
5 D&R
8 Kitap Yurdu
In [11]: qa = Store.objects.annotate(kitapsayisi=Count('books')).order_by('-kitapsayisi')
In [12]: [print(q.kitapsayisi, q.name) for q in qa ]
8 Kitap Yurdu
5 D&R
4 Idefix
In [17]: qa = Author.objects.values('name').annotate(ort_rate=Avg('book__rating'))
In [18]: qa
Out[18]: <QuerySet [{'name': 'Cemil Meriç', 'ort_rate': 4.7}, {'name': 'Necip Fazıl Kısakürek', 'ort_rate': 4.75}, {'name': 'Rasim Özdenören', 'ort_rate': 4.9}, {'name': 'Sezai Karakoç', 'ort_rate': 3.95}, {'name': 'İbn Kayyım El Cevziyye', 'ort_rate': 5.0}, {'name': 'İhsan Süreyya Sırma', 'ort_rate': 4.0}]>
In [21]: [print(q['ort_rate'], q['name']) for q in qa ]
4.7 Cemil Meriç
4.75 Necip Fazıl Kısakürek
4.9 Rasim Özdenören
3.95 Sezai Karakoç
5.0 İbn Kayyım El Cevziyye
4.0 İhsan Süreyya Sırma
In [25]: Author.objects.values('name').annotate(ort_rate=Avg('book__rating'))
Out[25]: <QuerySet [{'name': 'Cemil Meriç', 'ort_rate': 4.7}, {'name': 'Necip Fazıl Kısakürek', 'ort_rate': 4.75}, {'name': 'Rasim Özdenören', 'ort_rate': 4.9}, {'name': 'Sezai Karakoç', 'ort_rate': 3.95}, {'name': 'İbn Kayyım El Cevziyye', 'ort_rate': 5.0}, {'name': 'İhsan Süreyya Sırma', 'ort_rate': 4.0}]>
In [26]: Author.objects.annotate(ort_rate=Avg('book__rating')).values('name','ort_rate') # ort_rate eklendi
Out[26]: <QuerySet [{'name': 'Necip Fazıl Kısakürek', 'ort_rate': 4.75}, {'name': 'Rasim Özdenören', 'ort_rate': 4.9}, {'name': 'Sezai Karakoç', 'ort_rate': 3.95}, {'name': 'Cemil Meriç', 'ort_rate': 4.7}, {'name': 'İbn Kayyım El Cevziyye', 'ort_rate': 5.0}, {'name': 'İhsan Süreyya Sırma', 'ort_rate': 4.0}]>
Aggreating annotations¶
Annotate üzerine aggregation yapabilirsiniz.
In [10]: Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))
Out[10]: {'num_authors__avg': 1.0}
# baska bir ornek
In [14]: Store.objects.annotate(num_books=Count('books')).aggregate(Avg('num_books'))
Out[14]: {'num_books__avg': 4.25}