صفحهای که با ۲۰۱ کوئری باز میشود
مهمترین مشکل کارایی در پروژههای جنگو N+1 است: یک کوئری برای فهرست و سپس برای هر ردیف یک کوئری دیگر برای رابطهاش. این کد بیگناه به نظر میرسد:
{% for order in orders %}
<tr>
<td>{{ order.tracking_code }}</td>
<td>{{ order.customer.full_name }}</td> {# هر بار یک کوئری #}
<td>{{ order.items.count }}</td> {# باز یک کوئری #}
</tr>
{% endfor %}
با ۱۰۰ سفارش: ۱ + ۱۰۰ + ۱۰۰ = ۲۰۱ کوئری. روی لوکال با SQLite سریع به نظر میرسد؛ روی سرور با پایگاه دادهی شبکهای، صفحه چند ثانیه طول میکشد.
راهحلها
| ابزار | برای | روش |
|---|---|---|
select_related | ForeignKey و OneToOne (رو به جلو) | JOIN در همان کوئری |
prefetch_related | ManyToMany و رابطهی معکوس | یک کوئری جدا با IN و اتصال در پایتون |
annotate(Count) | وقتی فقط تعداد یا جمع لازم است | GROUP BY در همان کوئری |
from django.db.models import Count, Prefetch
orders = (Order.objects
.select_related("customer", "customer__user")
.annotate(item_count=Count("items"))
.prefetch_related(
Prefetch("items",
queryset=OrderItem.objects.select_related("carpet").order_by("id"),
to_attr="item_list"))
)[:50]
# در قالب: order.customer.full_name، order.item_count، و حلقه روی order.item_list
# مجموع: ۲ کوئری، مستقل از تعداد سفارشها
django-debug-toolbar: اول ببینید، بعد بهینه کنید
pip install django-debug-toolbar
# settings (فقط توسعه)
if DEBUG:
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ["127.0.0.1"]
# config/urls.py
from django.conf import settings
if settings.DEBUG:
urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))]
پنل SQL نوار ابزار تعداد کوئریها، زمان هر کدام و مهمتر از همه «similar» و «duplicate» را نشان میدهد؛ ده کوئری مشابه یعنی یک N+1.
نکتههایی که کمتر کسی میداند
- اگر روی رابطهی prefetchشده دوباره
.filter()یا.order_by()بزنید (order.items.filter(...))، کش prefetch دور ریخته میشود و کوئری تازه میرود؛ فیلتر را داخلPrefetch(queryset=...)بگذارید. - وقتی در
Prefetchازto_attrاستفاده میکنید، نتیجه فقط در همان ویژگی (item_list) است؛order.items.allوorder.items.countدر قالب دوباره برای هر سفارش کوئری میزنند. در قالب فقط ازitem_listاستفاده کنید. - در تستها با
self.assertNumQueries(2)تعداد کوئری را قفل کنید تا کسی بعداً بیصدا N+1 را برنگرداند. - debug-toolbar روی پاسخهای JSON و API ظاهر نمیشود؛ برای آنها
connection.queries(فقط با DEBUG=True) یا لاگرdjango.db.backendsرا در سطح DEBUG روشن کنید. .iterator()از Django 4.1 با prefetch_related کار میکند به شرطی کهchunk_sizeبدهید؛ برای خروجی اکسل از صدها هزار ردیف بدون پر شدن حافظه.