Django Aggregate & Annotate Notları
- Bu yazımızda Django'da
aggregateveannotatekullanı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.
Veritabanı Modeli

models.py
xfrom django.db import modelsclass 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)python manage.py shell diyerek shell ortamında pratiklere başlıyoruz
xxxxxxxxxxIn [1]: import datetimeIn [2]: from decimal import DecimalIn [3]: from django.db.models import *In [4]: from storebook.models import Book, Store, Author, PublisherIn [5]: Book.objects.count() # Kac adet kitap var ?Out[5]: 10In [6]: Book.objects.filter(publisher__name="İz Yayıncılık") # iz yayincilik'a ait kitaplari getirOut[6]: <QuerySet [<Book: Book object (10)>]>In [7]: Book.objects.filter(publisher__name__istartswith="Polen") # polen yayincilik'a ait kitaplari getirOut[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 sayisiOut[8]: 2In [9]: # Butun kitaplarin ortalama fiyatiIn [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 gosterOut[11]: {'ortalama_fiyat': Decimal('21.5380000000000')}In [12]: Book.objects.aggregate(en_pahali=Max('price')) # en pahali kitapOut[12]: {'en_pahali': Decimal('32.2900000000000')}In [14]: Book.objects.aggregate(en_ucuz=Min('price')) # en ucuz kitap fiyatiOut[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.538Out[17]: 10.751999999999999In [18]: # Her bir yayin evine ait kitap sayisini bulmak icin annotate kullanalimIn [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ıkKitap 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ıkKitap 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 alalimIn [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ı 2In [26]: print(publishers[1].name, publishers[1].kitap_sayisi)Polen Yayınları 2In [27]: print(publishers[2].name, publishers[2].kitap_sayisi)Diriliş Yayınları 2In [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 aldikAggregate
- 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.
xxxxxxxxxx>>> 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_sayisi2# queryset'teki ikinci obje icin yazar sayisini sorgulama>>> qs[1]<Book: Kitap Deneme 3>>>> qs[1].yazar_sayisi1annotate()bir QuerySet return eder. Return edilen bu queryset başka bir queryset ilefilter, order_by()gibi yapılar kullanılarak sorgu düzenlenebilir.
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.
xxxxxxxxxxIn [36]: book1 = Book.objects.first()In [37]: book1.authors.count()Out[37]: 1In [38]: book1.store_set.count()Out[38]: 2In [39]: q = Book.objects.annotate(Count('authors'),Count('store'))In [40]: q[0].authors__countOut[40]: 2In [41]: q[0].store__countOut[41]: 2Bir çok aggregate için bu problemi gidermenin yolu yoktur, ancak Count aggregate'e ait distinct parametresi yardımcı olabilir.
xxxxxxxxxxIn [45]: q = Book.objects.annotate(Count('authors', distinct=True),Count('store',distinct=True))In [46]: q[0].authors__countOut[46]: 1In [47]: q[0].store__countOut[47]: 2
Join ve Aggregate
Şimdiye kadar yapılan örneklerde Model'e ait field'lar üzerinden aggregation yaptık. Ancak bazen ilişkili model'e ait field üzerinde de aggretaion yapma ihtiyacı duyarız. Django'daki double underscore
__notasyonu ile ilişkili modeldeki alanları aggregation'da kullanabiliriz.xxxxxxxxxx# Her bir yayinevine ait butun kitaplarin icindeki max ve min fiyatini gorelimqs = 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 Yurdu15.8700000000000 30.2300000000000 Idefix13.75 32.2900000000000 D&RJoin zincirleri double underscore ile istenildigi kadar kullanılabilir.
Örneğin; her bir yayın evindeki en genc yazar bilgisi
xxxxxxxxxxIn [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 Yurdu77 Idefix58 D&R# aggregate ile en genc yazarIn [92]: qs = Store.objects.aggregate(en_genc_yazar=Min('books__authors__age'))In [93]: qsOut[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;
xxxxxxxxxxIn [102]: qs = Publisher.objects.annotate(Count('book'))In [103]: qs[0].book__countOut[103]: 1In [104]: qs[1].book__countOut[104]: 1In [105]: qs[2].book__countOut[105]: 2Ya da her bir yayınevindeki toplam kitap sayfa sayısını bulmak için
xxxxxxxxxxIn [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ık339 İ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ık208 Beyan YayınlarıBütün yayınevlerindeki en eski ve en yeni yayımlanan kitaba ait tarih bilgisini aşağıdaki gibi alabiliriz
xxxxxxxxxxIn [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)}Yazarların kitaplarının ortalama oranlarını aşağıdaki gibi alabiliriz
xxxxxxxxxx# her bir yazarin spesifik ortalamasiIn [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ürek4.9 Rasim Özdenören4.95 Sezai Karakoç4.7 Cemil Meriç5.0 İbn Kayyım El Cevziyye4.9 İhsan Süreyya SırmaOut[127]: [None, None, None, None, None, None]# butun yazarlarin ortalamasiIn [128]: Author.objects.aggregate(ortalama_oran=Avg('book__rating'))Out[128]: {'ortalama_oran': 4.88}Yazarların kitap sayilarini almak icin
xxxxxxxxxxIn [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ürek1 Rasim Özdenören2 Sezai Karakoç1 Cemil Meriç2 İbn Kayyım El Cevziyye2 İhsan Süreyya SırmaOut[132]: [None, None, None, None, None, None]In [133]: qaggr = Author.objects.aggregate(kitapsayisi=Count('book'))In [134]: qaggrOut[134]: {'kitapsayisi': 10}
Aggregation ve diğer QuerySet ibareleri
Aggregate ile
filter()veyaexlude()ibaresini kullanabilirsiniz.Örneğin
Çile başlayan kitapların ortalama fiyatını aşağıdaki şekilde alabilirizxxxxxxxxxxIn [138]: Book.objects.filter(name__startswith='Ç').aggregate(Avg('price'))Out[138]: {'price__avg': Decimal('28.7900000000000')}Y ile başlayan her bir kitabın bulunduğu pazar yeri sayisi
xxxxxxxxxxIn [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Ç ile başlayan her kitabın bulunduğu pazar yeri sayısı
xxxxxxxxxxIn [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 Nur3 Çile
Annotation'ların filtrelenmesi
filter()veyaexclude()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
xxxxxxxxxxIn [172]: qa = Store.objects.annotate(num_book=Count('books'))In [173]: [print(q.num_book, q.name) for q in qa]8 Kitap Yurdu4 Idefix5 D&RIn [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 Yurdu5 D&REğer 2 tane annotation'ı birleştirerek filtreleme yapacaksanız filter parametresini ve aşağıdaki yapıyı kullanabilirsiniz. Örneğin her bir yazarın kitap sayisini ve kitaplarinin oranlarindan 4.8'den buyuk ve esit olanlari almak icin;
xxxxxxxxxxIn [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 esit1 1 Rasim Özdenören2 2 Sezai Karakoç1 0 Cemil Meriç # 1 kitabi var hicbiri 4.8'den buyuk ve esit DEGIL!2 2 İbn Kayyım El Cevziyye2 2 İhsan Süreyya Sırmafilter parametresini tekil annotation ve aggregation'da kullanmak önerilmez. Bunun yerine Queryset.filter( ) yapısı kullanılmalıdır.
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.
xxxxxxxxxx#once ortalama alinir daha sonra en az 1 tane kitabin rate'i 4.'den buyuk olan(lar) gelirIn [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ık4.1 Beyan Yayınları# burada once kitap rate'i 4.0'dan buyuk olanlar alinir daha sonra ortalamalar alinirIn [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ık4.1 Beyan Yayınlarıorder_by ile annotate kullanımı
xxxxxxxxxxIn [9]: qa = Store.objects.annotate(kitapsayisi=Count('books')).order_by('kitapsayisi')In [10]: [print(q.kitapsayisi, q.name) for q in qa ] 4 Idefix5 D&R8 Kitap YurduIn [11]: qa = Store.objects.annotate(kitapsayisi=Count('books')).order_by('-kitapsayisi')In [12]: [print(q.kitapsayisi, q.name) for q in qa ]8 Kitap Yurdu5 D&R4 Idefixvalues() ile annotate kullanımı
xxxxxxxxxxIn [17]: qa = Author.objects.values('name').annotate(ort_rate=Avg('book__rating'))In [18]: qaOut[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ürek4.9 Rasim Özdenören3.95 Sezai Karakoç5.0 İbn Kayyım El Cevziyye4.0 İhsan Süreyya SırmaEğer
values()ibaresiannotate()ibaresinden önce gelirse, annotation'lar otomatik olarak sonuç data setine eklenecektir. Ancakvalues(),annotate()'den sonra gelirse annotation field'ları values içerisine eklemeniz gerekmektedir.xxxxxxxxxxIn [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 eklendiOut[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.
xxxxxxxxxxIn [10]: Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))Out[10]: {'num_authors__avg': 1.0}# baska bir ornekIn [14]: Store.objects.annotate(num_books=Count('books')).aggregate(Avg('num_books'))Out[14]: {'num_books__avg': 4.25}
Kaynak
Yorumlar