بهینه‌سازی دسترسی به دیتابیس در Django: راهکارهای عملی برای افزایش سرعت و کاهش هزینهٔ کوئری‌ها

این مقاله مجموعه‌ای از تکنیک‌ها و توصیه‌های مهم برای بهینه‌سازی دسترسی به دیتابیس در Django را ارائه می‌دهد. از پروفایل‌کردن کوئری‌ها و استفاده از ابزارهایی مانند explain() و django-debug-toolbar، تا درک رفتار QuerySet، استفاده از iterator()، انجام پردازش در دیتابیس به‌جای Python، و بهره‌گیری از RawSQL یا SQL خام. این راهنما به شما کمک می‌کند کوئری‌های سریع‌تر، کم‌هزینه‌تر و بهینه‌تر بنویسید.

بهینه‌سازی دیتابیس، QuerySetexplain، iterator، RawSQL، ایندکسDjango ORM، پروفایلینگ

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

۱. ابتدا پروفایل کنید

اولین قدم در هر نوع بهینه‌سازی، اندازه‌گیری است. باید بدانید چه کوئری‌هایی اجرا می‌شوند و هزینهٔ هرکدام چقدر است.

ابزارهای پیشنهادی:

  • QuerySet.explain() برای تحلیل نحوهٔ اجرای کوئری
  • پکیج django-debug-toolbar
  • ابزارهای مانیتورینگ دیتابیس (مثل pgAdmin، MySQL Workbench)

به یاد داشته باشید که ممکن است برای سرعت یا حافظه یا هر دو بهینه‌سازی کنید. همیشه بعد از هر تغییر، دوباره پروفایل کنید.

۲. استفاده از تکنیک‌های استاندارد بهینه‌سازی دیتابیس

۲.۱ ایندکس‌ها

ایندکس‌ها مهم‌ترین ابزار برای افزایش سرعت کوئری‌ها هستند. پس از پروفایل‌کردن، روی فیلدهایی که زیاد در filter()، exclude()، order_by() و ... استفاده می‌شوند ایندکس بگذارید.

روش‌ها:

  • db_index=True روی فیلد
  • Meta.indexes برای ایندکس‌های پیچیده‌تر

۲.۲ انتخاب نوع فیلد مناسب

نوع فیلد اشتباه می‌تواند سرعت کوئری‌ها را کاهش دهد. مثلاً ذخیره‌کردن عدد در CharField اشتباه است.

۳. درک رفتار QuerySet

QuerySetها lazy هستند؛ یعنی تا زمانی که واقعاً به داده نیاز نداشته باشید، کوئری اجرا نمی‌شود.

۳.۱ زمان اجرای QuerySet

کوئری‌ها در زمان‌هایی مثل موارد زیر اجرا می‌شوند:

  • تکرار روی QuerySet
  • تبدیل به list
  • دسترسی به اولین یا آخرین عنصر
  • ارزیابی در قالب template

۳.۲ کش شدن نتایج

نتایج QuerySet پس از اولین اجرا کش می‌شوند.

مثال:


entry = Entry.objects.get(id=1)
entry.blog      # اولین بار → کوئری
entry.blog      # بار دوم → کش

اما متدهای قابل فراخوانی (callable) هر بار کوئری جدید اجرا می‌کنند:


entry.authors.all()   # کوئری
entry.authors.all()   # دوباره کوئری

۳.۳ مراقب template باشید

templateها پرانتز نمی‌پذیرند، اما callables را خودکار اجرا می‌کنند. این می‌تواند باعث اجرای ناخواستهٔ کوئری شود.

۳.۴ استفاده از cached_property

برای propertyهای سفارشی، خودتان باید کش‌کردن را مدیریت کنید.

۴. استفاده از with در template

برای جلوگیری از اجرای چندبارهٔ QuerySet در template، از {% with %} استفاده کنید.

۵. استفاده از iterator() برای داده‌های حجیم

اگر QuerySet بسیار بزرگ باشد، کش‌کردن کل نتایج باعث مصرف زیاد حافظه می‌شود. در این حالت از iterator() استفاده کنید:


for obj in MyModel.objects.all().iterator():
    ...

۶. استفاده از explain()

QuerySet.explain() نحوهٔ اجرای کوئری را نشان می‌دهد:

  • ایندکس‌ها
  • joinها
  • نوع اسکن (Index Scan، Seq Scan و ...)

این اطلاعات برای یافتن کوئری‌های کند بسیار ارزشمند است.

۷. انجام کار در دیتابیس، نه در Python

همیشه سعی کنید پردازش را در دیتابیس انجام دهید، نه در Python.

مثال‌ها:

  • استفاده از filter() و exclude() به‌جای فیلترکردن در Python
  • استفاده از F() برای مقایسهٔ فیلدها
  • استفاده از annotate() برای aggregation

۸. استفاده از RawSQL

اگر ORM نتواند کوئری موردنظر شما را بسازد، از RawSQL استفاده کنید:


from django.db.models.expressions import RawSQL
MyModel.objects.annotate(
    custom=RawSQL("SELECT ...", [])
)

۹. استفاده از SQL خام

در نهایت، اگر هیچ‌کدام کافی نبود، می‌توانید SQL خام بنویسید:


from django.db import connection

with connection.cursor() as cursor:
    cursor.execute("SELECT ...")

برای دیدن کوئری‌هایی که Django تولید می‌کند:


from django.db import connection
print(connection.queries)

جمع‌بندی

بهینه‌سازی دیتابیس در Django ترکیبی از پروفایلینگ، درک رفتار QuerySet، استفادهٔ صحیح از ایندکس‌ها، و انجام پردازش در دیتابیس است. با رعایت این اصول، می‌توانید عملکرد برنامه را به‌طور چشمگیری بهبود دهید.

۱. بازیابی آبجکت‌ها با ستون‌های یکتا و ایندکس‌شده

برای استفاده از get()، بهترین کار این است که از ستون‌هایی استفاده کنید که:

  • ایندکس دارند
  • unique هستند

مثال:


entry = Entry.objects.get(id=10)

این بسیار سریع‌تر از:


entry = Entry.objects.get(headline="News Item Title")

و بسیار سریع‌تر از:


entry = Entry.objects.get(headline__startswith="News")

چون:

  • headline ایندکس ندارد
  • startswith ممکن است هزاران رکورد برگرداند
  • شبکه (در صورت دیتابیس جدا) هزینه را چند برابر می‌کند

۲. همه‌چیز را یک‌جا بگیرید اگر قرار است استفاده کنید

اگر قرار است مجموعه‌ای از داده‌ها را کامل استفاده کنید، بهتر است یک‌بار کوئری بزنید، نه چند بار.

۳. استفاده از select_related() و prefetch_related()

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

  • select_related() → join در دیتابیس (برای ForeignKey و OneToOne)
  • prefetch_related() → دو کوئری جدا + merge در Python (برای ManyToMany و reverse FK)

از آن‌ها استفاده کنید:

  • در Managerها
  • در viewها
  • با prefetch_related_objects() در صورت نیاز

۴. چیزهایی را که لازم ندارید، نگیرید

۴.۱ استفاده از values() و values_list()

اگر فقط مقدار می‌خواهید، نه آبجکت ORM:


Entry.objects.values("id", "headline")

۴.۲ استفاده از defer() و only()

برای جلوگیری از بارگذاری ستون‌های سنگین:


Entry.objects.only("id", "headline")

هشدار: اگر بعداً به فیلدهای defer شده دسترسی پیدا کنید، کوئری اضافه اجرا می‌شود.

۵. استفاده از contains()، count() و exists()

۵.۱ contains()

برای بررسی وجود یک آبجکت در QuerySet:


qs.contains(obj)

۵.۲ count()

برای شمارش بدون بارگذاری داده:


qs.count()

۵.۳ exists()

برای بررسی وجود حداقل یک نتیجه:


qs.exists()

۵.۴ اما زیاده‌روی نکنید

اگر قرار است داده را استفاده کنید، بهتر است QuerySet را یک‌بار evaluate کنید و از cache استفاده کنید.

مثال بهینه:


members = group.members.all()

if display_group_members:
    if members:
        if current_user in members:
            print("You and", len(members) - 1, "other users are members.")
        else:
            print("There are", len(members), "members.")

        for member in members:
            print(member.username)
    else:
        print("There are no members.")

این کد فقط یک کوئری اجرا می‌کند.

۶. استفاده از update() و delete() برای عملیات bulk

به‌جای اینکه آبجکت‌ها را یکی‌یکی بگیرید و save کنید:


Entry.objects.filter(...).update(status="published")

یا:


Entry.objects.filter(...).delete()

نکته: این روش‌ها سیگنال‌ها و save()/delete() سفارشی را اجرا نمی‌کنند.

۷. استفاده مستقیم از مقدار کلید خارجی

اگر فقط id لازم دارید:


entry.blog_id

نه:


entry.blog.id

دومی یک کوئری اضافه اجرا می‌کند.

۸. اگر ترتیب مهم نیست، order_by را حذف کنید

مرتب‌سازی هزینه دارد. اگر لازم نیست:


Entry.objects.order_by()

این default ordering را حذف می‌کند.

۹. از متدهای bulk استفاده کنید

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

  • bulk_create()
  • bulk_update()
  • update()
  • delete()

جمع‌بندی

بهینه‌سازی QuerySet در Django ترکیبی از انتخاب ستون‌های مناسب، کاهش تعداد کوئری‌ها، استفاده از ابزارهای ORM مانند select_related و prefetch_related، و انجام عملیات bulk است. با رعایت این اصول، می‌توانید عملکرد دیتابیس را به‌طور چشمگیری بهبود دهید.

۱. ایجاد گروهی آبجکت‌ها (bulk_create)

برای ایجاد چندین آبجکت، استفاده از bulk_create() بسیار سریع‌تر از create() تکی است.

نسخهٔ بهینه:


Entry.objects.bulk_create(
    [
        Entry(headline="This is a test"),
        Entry(headline="This is only a test"),
    ]
)

نسخهٔ کند و غیربهینه:


Entry.objects.create(headline="This is a test")
Entry.objects.create(headline="This is only a test")

نکته: bulk_create سیگنال‌ها و save() سفارشی را اجرا نمی‌کند.

۲. به‌روزرسانی گروهی آبجکت‌ها (bulk_update)

برای به‌روزرسانی چندین آبجکت، bulk_update() تعداد کوئری‌ها را به حداقل می‌رساند.

نسخهٔ بهینه:


entries = Entry.objects.bulk_create(
    [
        Entry(headline="This is a test"),
        Entry(headline="This is only a test"),
    ]
)

entries[0].headline = "This is not a test"
entries[1].headline = "This is no longer a test"

Entry.objects.bulk_update(entries, ["headline"])

نسخهٔ کند:


entries[0].headline = "This is not a test"
entries[0].save()

entries[1].headline = "This is no longer a test"
entries[1].save()

نکته: bulk_update نیز سیگنال‌ها و save() سفارشی را اجرا نمی‌کند.

۳. افزودن گروهی روابط ManyToMany

۳.۱ استفاده از add() با چند آبجکت

برای افزودن چند عضو به یک رابطهٔ ManyToMany:


my_band.members.add(me, my_friend)

به‌جای:


my_band.members.add(me)
my_band.members.add(my_friend)

۳.۲ استفاده از bulk_create روی مدل through

برای افزودن چندین جفت رابطه یا زمانی که مدل through سفارشی دارید:


PizzaToppingRelationship = Pizza.toppings.through

PizzaToppingRelationship.objects.bulk_create(
    [
        PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
        PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
        PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
    ],
    ignore_conflicts=True,
)

به‌جای:


my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)

۴. حذف گروهی روابط ManyToMany

۴.۱ استفاده از remove() با چند آبجکت


my_band.members.remove(me, my_friend)

به‌جای:


my_band.members.remove(me)
my_band.members.remove(my_friend)

۴.۲ استفاده از delete() روی مدل through با Q expression


from django.db.models import Q

PizzaToppingRelationship = Pizza.toppings.through

PizzaToppingRelationship.objects.filter(
    Q(pizza=my_pizza, topping=pepperoni)
    | Q(pizza=your_pizza, topping=pepperoni)
    | Q(pizza=your_pizza, topping=mushroom)
).delete()

به‌جای:


my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)

جمع‌بندی

استفاده از عملیات bulk در Django—چه برای ایجاد، چه به‌روزرسانی، چه افزودن و حذف روابط—تعداد کوئری‌ها را به‌شدت کاهش می‌دهد و عملکرد برنامه را بهبود می‌بخشد. البته باید توجه داشت که این روش‌ها سیگنال‌ها و منطق سفارشی مدل‌ها را اجرا نمی‌کنند، پس باید با دقت استفاده شوند.

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