تجمیع (Aggregation) و Annotation در Django ORM: محاسبه میانگین، شمارش، بیشینه، کمینه و رفتار چندتجمیعی

این مقاله نحوهٔ انجام عملیات تجمیع در Django ORM را توضیح می‌دهد: استفاده از aggregate() برای محاسبهٔ مقادیر کلی مانند میانگین و بیشینه، استفاده از annotate() برای افزودن مقادیر تجمیعی به هر آبجکت، مشکلات رایج هنگام ترکیب چند تجمیع، استفاده از distinct=True برای جلوگیری از نتایج اشتباه، و نحوهٔ بررسی SQL تولیدشده برای درک بهتر رفتار Query.

aggregate، annotate، Count، Avg، Min، Maxdistinct، تجمیع QuerySetDjango ORM

~6 دقیقه مطالعه • بروزرسانی ۱۹ اسفند ۱۴۰۴

۱. مقدمه‌ای بر Aggregation

ORM جنگو به شما اجازه می‌دهد آبجکت‌ها را ایجاد، بازیابی، ویرایش و حذف کنید. اما در بسیاری از کاربردهای واقعی، نیاز دارید مقادیر خلاصه‌شده مثل مجموع، میانگین یا تعداد را محاسبه کنید. Django دو ابزار اصلی برای این کار دارد: aggregate() و annotate().

در این مقاله از مدل‌های زیر استفاده می‌کنیم:


Author(name, age)
Publisher(name)
Book(name, pages, price, rating, authors, publisher, pubdate)
Store(name, books)

۲. خلاصهٔ سریع Aggregation

تعداد کل کتاب‌ها:


Book.objects.count()

تعداد کتاب‌های یک ناشر خاص:


Book.objects.filter(publisher__name="BaloneyPress").count()

میانگین قیمت (با مقدار پیش‌فرض):


Book.objects.aggregate(Avg("price", default=0))

بیشترین قیمت:


Book.objects.aggregate(Max("price", default=0))

اختلاف بین بیشترین قیمت و میانگین قیمت:


Book.objects.aggregate(
    price_diff=Max("price", output_field=FloatField()) - Avg("price")
)

تعداد کتاب‌های هر ناشر:


Publisher.objects.annotate(num_books=Count("book"))

شمارش شرطی با Q objects:


above_5 = Count("book", filter=Q(book__rating__gt=5))
below_5 = Count("book", filter=Q(book__rating__lte=5))
Publisher.objects.annotate(above_5=above_5, below_5=below_5)

۵ ناشر برتر بر اساس تعداد کتاب:


Publisher.objects.annotate(num_books=Count("book"))
         .order_by("-num_books")[:5]

۳. تولید Aggregation روی کل QuerySet

aggregate() مقادیر خلاصه‌شده را برای کل QuerySet محاسبه می‌کند و یک دیکشنری برمی‌گرداند.

مثال: میانگین قیمت کتاب‌ها


Book.objects.aggregate(Avg("price"))

خروجی:


{'price__avg': 34.35}

نام‌گذاری سفارشی:


Book.objects.aggregate(average_price=Avg("price"))

چند تجمیع همزمان:


Book.objects.aggregate(Avg("price"), Max("price"), Min("price"))

۴. تجمیع برای هر آبجکت با annotate()

annotate() مقدارهای محاسبه‌شده را به هر آبجکت QuerySet اضافه می‌کند.

مثال: تعداد نویسندگان هر کتاب


q = Book.objects.annotate(Count("authors"))
q[0].authors__count

نام‌گذاری سفارشی:


q = Book.objects.annotate(num_authors=Count("authors"))

برخلاف aggregate، خروجی annotate یک QuerySet است و می‌تواند با filter، order_by یا annotateهای دیگر ترکیب شود.

۵. ترکیب چند تجمیع و مشکل Join

وقتی چندین annotate را با هم ترکیب می‌کنید، Django joinهای مختلف را ترکیب می‌کند و ممکن است نتایج اشتباه تولید شود.

مثال رفتار اشتباه:


q = Book.objects.annotate(Count("authors"), Count("store"))
q[0].authors__count  # اشتباه
q[0].store__count    # اشتباه

دلیل: join بین Book–Author و Book–Store باعث ضرب شدن ردیف‌ها می‌شود.

راه‌حل: استفاده از distinct=True


q = Book.objects.annotate(
    Count("authors", distinct=True),
    Count("store", distinct=True)
)

بررسی SQL برای اطمینان:


print(q.query)

جمع‌بندی

ابزارهای aggregation در Django—یعنی aggregate() و annotate()—راهی قدرتمند برای محاسبهٔ مقادیر خلاصه‌شده روی QuerySet یا برای هر آبجکت فراهم می‌کنند. درک رفتار joinها هنگام ترکیب چند تجمیع و استفاده از distinct=True برای جلوگیری از نتایج اشتباه ضروری است. همیشه می‌توانید SQL تولیدشده را بررسی کنید تا دقیقاً ببینید Django چه Queryای ساخته است.

۱. Joins و Aggregation روی فیلدهای مرتبط

تا اینجا تجمیع‌ها روی فیلدهای خود مدل انجام می‌شدند. اما Django اجازه می‌دهد روی فیلدهای مدل‌های مرتبط نیز تجمیع انجام دهید.

برای این کار از همان سینتکس __ استفاده می‌شود و Django به‌طور خودکار joinهای لازم را انجام می‌دهد.

مثال: کمترین و بیشترین قیمت کتاب‌های هر فروشگاه


Store.objects.annotate(
    min_price=Min("books__price"),
    max_price=Max("books__price")
)

تجمیع روی کل QuerySet:


Store.objects.aggregate(
    min_price=Min("books__price"),
    max_price=Max("books__price")
)

مثال join چندلایه:

کمترین سن نویسندهٔ هر کتاب موجود در فروشگاه‌ها:


Store.objects.aggregate(youngest_age=Min("books__authors__age"))

۲. عبور از روابط معکوس در Aggregation

همانند lookupهای معکوس، می‌توانید در aggregation نیز از روابط backward استفاده کنید.

مثال: تعداد کتاب‌های هر ناشر


Publisher.objects.annotate(Count("book"))

قدیمی‌ترین تاریخ انتشار کتاب‌های هر ناشر:


Publisher.objects.aggregate(oldest_pubdate=Min("book__pubdate"))

تجمیع روی many-to-many معکوس:

مجموع صفحات کتاب‌های هر نویسنده:


Author.objects.annotate(total_pages=Sum("book__pages"))

میانگین امتیاز کتاب‌های هر نویسنده:


Author.objects.aggregate(average_rating=Avg("book__rating"))

۳. تعامل Aggregation با filter و exclude

فیلتر قبل از annotate:

فیلتر تعیین می‌کند annotation روی چه آبجکت‌هایی محاسبه شود.


Book.objects.filter(name__startswith="Django")
            .annotate(num_authors=Count("authors"))

فیلتر قبل از aggregate:

فقط روی آبجکت‌های فیلترشده تجمیع انجام می‌شود.


Book.objects.filter(name__startswith="Django").aggregate(Avg("price"))

۴. فیلتر کردن روی Annotationها

می‌توانید از نام annotation در filter استفاده کنید.

کتاب‌هایی با بیش از یک نویسنده:


Book.objects.annotate(num_authors=Count("authors"))
            .filter(num_authors__gt=1)

دو annotation با فیلترهای متفاوت:


highly_rated = Count("book", filter=Q(book__rating__gte=7))
Author.objects.annotate(
    num_books=Count("book"),
    highly_rated_books=highly_rated
)

۵. انتخاب بین filter و filter argument در Aggregation

اگر فقط یک تجمیع دارید، بهتر است از QuerySet.filter() استفاده کنید.

پارامتر filter= در aggregation زمانی مفید است که چند تجمیع با شرط‌های متفاوت دارید.

۶. ترتیب annotate و filter

ترتیب اجرای annotate و filter بسیار مهم است و نتایج متفاوتی تولید می‌کند.

مثال Count:

ابتدا annotate سپس filter:


Publisher.objects.annotate(num_books=Count("book", distinct=True))
                 .filter(book__rating__gt=3)

فیلتر روی annotation تأثیر ندارد.

ابتدا filter سپس annotate:


Publisher.objects.filter(book__rating__gt=3)
                 .annotate(num_books=Count("book"))

فقط کتاب‌های با امتیاز بالای ۳ شمرده می‌شوند.

مثال Avg:

در حالت اول میانگین همهٔ کتاب‌ها محاسبه می‌شود؛ در حالت دوم فقط کتاب‌های فیلترشده.

۷. استفاده از order_by با Annotation

می‌توانید بر اساس annotation مرتب‌سازی کنید.

مثال:


Book.objects.annotate(num_authors=Count("authors"))
            .order_by("num_authors")

جمع‌بندی

Django ORM امکان انجام تجمیع‌های پیچیده روی روابط مستقیم و معکوس را فراهم می‌کند. ترتیب annotate و filter اهمیت زیادی دارد و می‌تواند نتایج متفاوتی ایجاد کند. همچنین می‌توانید روی annotationها فیلتر و مرتب‌سازی انجام دهید. برای درک بهتر رفتار Query، همیشه می‌توانید SQL تولیدشده را با str(queryset.query) بررسی کنید.

۱. تأثیر values() بر رفتار Aggregation

به‌طور معمول annotate() برای هر آبجکت یک مقدار تجمیعی تولید می‌کند. اما وقتی از values() استفاده می‌کنید، Django نتایج را بر اساس فیلدهای داخل values() گروه‌بندی می‌کند و سپس برای هر گروه یک annotation محاسبه می‌شود.

مثال: میانگین امتیاز کتاب‌های هر نویسنده


Author.objects.annotate(average_rating=Avg("book__rating"))

این Query برای هر نویسنده یک نتیجه برمی‌گرداند.

اما با values():


Author.objects.values("name").annotate(average_rating=Avg("book__rating"))

در این حالت نویسندگان با نام یکسان در یک گروه قرار می‌گیرند و میانگین مشترک محاسبه می‌شود.

۲. ترتیب annotate و values

ترتیب این دو بسیار مهم است.

حالت ۱: values() → annotate()

ابتدا گروه‌بندی انجام می‌شود، سپس annotation برای هر گروه محاسبه می‌شود.

حالت ۲: annotate() → values()

ابتدا annotation برای هر آبجکت محاسبه می‌شود، سپس values فقط تعیین می‌کند چه فیلدهایی در خروجی باشند.


Author.objects.annotate(average_rating=Avg("book__rating"))
      .values("name", "average_rating")

در این حالت باید average_rating را صریحاً در values() بیاورید.

۳. تعامل order_by با values()

فیلدهایی که در order_by() استفاده می‌شوند، حتی اگر در values() نباشند، در گروه‌بندی دخالت می‌کنند.

مثال:


items = Item.objects.order_by("name")
items.values("data").annotate(Count("id"))

این Query به‌جای گروه‌بندی بر اساس data، بر اساس (data, name) گروه‌بندی می‌کند.

راه‌حل:


items.values("data").annotate(Count("id")).order_by()

با پاک کردن order_by، گروه‌بندی درست انجام می‌شود.

نکته: Django هرگز order_by صریح را حذف نمی‌کند.

۴. تجمیع روی Annotationها

می‌توانید روی فیلدهای annotation شده نیز aggregate انجام دهید.

مثال: میانگین تعداد نویسندگان هر کتاب


Book.objects.annotate(num_authors=Count("authors"))
            .aggregate(Avg("num_authors"))

۵. Aggregation روی QuerySetهای خالی

اگر QuerySet خالی باشد، مقدار تجمیع معمولاً None برمی‌گردد.

مثال:


Book.objects.filter(name__contains="web").aggregate(Sum("price"))
# {'price__sum': None}

استفاده از default:


Book.objects.filter(name__contains="web")
    .aggregate(Sum("price", default=0))

Count همیشه ۰ برمی‌گرداند.

۶. استفاده از AnyValue در MySQL با ONLY_FULL_GROUP_BY

در MySQL، اگر در یک Query ترکیبی از عبارت‌های تجمیعی و غیرتجمیعی وجود داشته باشد، ممکن است خطای GROUP BY رخ دهد.

مثال خطا:

استفاده از Greatest و Count ممکن است باعث خطا شود:


OperationalError: Expression ... is not in GROUP BY

راه‌حل: استفاده از AnyValue


Book.objects.values(
    greatest_pages=Greatest("pages", 600),
).annotate(
    num_authors=Count("authors"),
    pages_per_author=AnyValue(F("greatest_pages")) / F("num_authors"),
).aggregate(Avg("pages_per_author"))

در دیتابیس‌های دیگر این مشکل وجود ندارد.

جمع‌بندی

values() باعث گروه‌بندی نتایج می‌شود و رفتار annotate را تغییر می‌دهد. ترتیب annotate و values اهمیت زیادی دارد و order_by می‌تواند گروه‌بندی را تغییر دهد. Django امکان تجمیع روی annotationها را فراهم می‌کند و برای Queryهای خالی مقدار پیش‌فرض قابل تنظیم است. در MySQL نیز AnyValue برای جلوگیری از خطاهای ONLY_FULL_GROUP_BY ضروری است.

نوشته و پژوهش شده توسط دکتر شاهین صیامی