Django ORM کی سب سے بڑی طاقت ہی اس کا سب سے بڑا جال بھی ہے: order.customer.name دیکھنے میں ایک مفت attribute access لگتا ہے، مگر اس dot کے پیچھے یہ فیصلہ چھپا ہوتا ہے کہ آیا Django کے پاس یہ data پہلے سے memory میں موجود ہے یا وہ اسے لانے کے لیے ایک بالکل نئی SQL query چلانے والا ہے۔ زیادہ تر اوقات، template loop یا serializer میں، یہ query ہر row کے لیے ایک بار چلتی ہے — اور چونکہ ہر انفرادی query تیز ہوتی ہے، اس لیے کسی کو تب تک احساس نہیں ہوتا جب تک وہ page جو local پر 40ms لیتا تھا، production data کے ساتھ چار سیکنڈ نہ لینے لگے، جہاں دس کے بجائے ایک ہزار rows ہوں۔
یہی N+1 مسئلے کی مخصوص شکل ہے: فہرست لانے کے لیے ایک query، پھر N مزید queries — ہر item کے لیے ایک — تاکہ ہر row کے متعلقہ object کو fetch کیا جا سکے۔ یہ Django میں کوئی bug نہیں؛ یہ lazy relationship loading اور ایسے code کا فطری نتیجہ ہے جو کبھی واضح طور پر نہیں کہتا: “اور ساتھ ہی متعلقہ rows کو bulk میں بھی load کر لو۔” اس کا حل نہ اندازہ لگانا ہے نہ ہی بے تحاشا over-fetching — بلکہ یہ پہچاننا ہے کہ کون سا relationship بار بار queries trigger کر رہا ہے، اور پھر چند مخصوص tools میں سے وہی ایک چننا ہے جو اس relationship کی اصل ساخت سے میل کھاتا ہو۔
آپ یہ سیکھیں گے:
- حقیقی request trace میں N+1 queries کو کیسے دیکھا جائے، صرف ان کا شک نہ کیا جائے
- lazy relationship access ہی ابتدا میں N+1 کی وجہ کیوں بنتا ہے
- کب
select_relatedدرست حل ہے، اور یہ ہر relationship type کے لیے کیوں کام نہیں کرتا - کب
prefetch_relatedدرست حل ہے، اور اندرونی طور پر یہ مختلف کیا کرتا ہے Prefetchobjects filtered اور ordered related querysets کو کیسے handle کرتے ہیں- کب دونوں tools موزوں نہیں ہوتے اور raw aggregation بہتر جواب ہوتا ہے
- N+1 regressions کو production تک پہنچنے سے پہلے CI میں fail کیسے کروایا جائے
فہرستِ مضامین
- N+1 حقیقت میں کیسا دکھتا ہے
- اسے دیکھنا: اصل دنیا میں N+1 پکڑنے کے tools
- lazy loading اس کی وجہ کیوں بنتی ہے
- select_related: forward اور one-to-one relationships کے لیے
- prefetch_related: reverse اور many-to-many relationships کے لیے
- Prefetch objects: related data کی filtering اور ordering
- جب دونوں درست نہ ہوں: iteration کے بجائے aggregation
- CI میں regressions پکڑنا
- عام غلطیاں
N+1 حقیقت میں کیسا دکھتا ہے
ایک ایسا page لیں جو حالیہ orders کی فہرست دکھاتا ہے اور ہر customer کا نام بھی:
orders = Order.objects.filter(status="pending")[:50]
for order in orders:
print(order.customer.name)
یہ دیکھنے میں بے ضرر 51 سطور کا code لگتا ہے۔ مگر یہ 51 queries ہیں: ابتدائی orders queryset کے لیے ایک SELECT، پھر ہر ایک order.customer access کے لیے ایک اضافی SELECT ... WHERE id = ?، کیونکہ customer ایک foreign key ہے جسے Django پہلی access پر lazily load کرتا ہے، نہ کہ order fetch کرتے وقت eager طور پر۔ پچاس rows کا مطلب database تک پچاس اضافی round trips ہے، اور ہر ایک single-row lookup کے لیے connection اور network overhead کی پوری قیمت چکانی پڑتی ہے، حالانکہ یہ سب ایک JOIN میں ہو سکتا تھا۔
یہ بالکل وہی loop-and-lookup pattern ہے جو Postgres plan میں nested loop کے طور پر زیادہ iteration count کے ساتھ ظاہر ہوتا ہے — database کی طرف سے اسی مسئلے کو دیکھنے کے لیے EXPLAIN ANALYZE guide کے loops section کو دیکھیں، ORM کی طرف سے نہیں۔ دونوں دراصل ایک ہی bug ہیں، صرف لباس مختلف ہے: ایک بظاہر سستا operation جو پورے set کے لیے ایک بار batch ہونے کے بجائے ہر row کے لیے الگ الگ دہرایا جا رہا ہے۔
اسے دیکھنا: اصل دنیا میں N+1 پکڑنے کے tools
code review سے N+1 کا اندازہ لگانا اصل مسائل کو miss بھی کرتا ہے اور false positives بھی مسلسل پیدا کرتا ہے — انہیں تلاش کرنے کا واحد قابلِ اعتماد طریقہ یہ ہے کہ کسی حقیقی request کے لیے اصل query count دیکھا جائے۔
Django Debug Toolbar local طور پر سب سے تیز option ہے — یہ ہر request میں چلنے والی ہر query کو panel میں count کرتا ہے، duplicate queries کو ساتھ ساتھ دکھاتا ہے، اور جب ایک ہی SELECT ... WHERE id = ? کی ساخت response میں بار بار دہرائی جائے تو واضح طور پر “similar queries” flag کرتا ہے۔
django.test.utils.CaptureQueriesContext یہی کام programmatically کرتا ہے، جو اس لیے اہم ہے کہ اسے tests میں استعمال کیا جا سکتا ہے، صرف interactive browser session میں نہیں:
from django.test.utils import CaptureQueriesContext
from django.db import connection
with CaptureQueriesContext(connection) as ctx:
list(Order.objects.filter(status="pending")[:50])
for order in Order.objects.filter(status="pending")[:50]:
_ = order.customer.name
print(len(ctx.captured_queries)) # 51, not 2
django-silk اور APM tools جیسے Sentry کی performance monitoring یا Datadog کا APM production میں اس سے بھی آگے جاتے ہیں — یہ query counts اور مجموعی وقت کو مخصوص views اور حتیٰ کہ مخصوص lines سے منسوب کرتے ہیں، اور اسی طرح وہ N+1 patterns پکڑے جاتے ہیں جو صرف حقیقی data volume اور اصل relationship fan-out میں ظاہر ہوتے ہیں، نہ کہ local پر استعمال ہونے والے چھوٹے fixture data میں۔
ان تمام tools میں دیکھنے والا signal ایک ہی ہے: تقریباً ایک جیسی query جو صرف ایک WHERE-clause parameter میں مختلف ہو، ایک ہی request میں درجنوں یا سینکڑوں بار دہرائی جائے۔ اصل شناخت یہی repetition ہے، صرف total query count نہیں۔
lazy loading اس کی وجہ کیوں بنتی ہے
Django کا ORM design کے لحاظ سے lazy ہے — queryset اس وقت تک database کو hit نہیں کرتا جب تک اس پر iteration نہ ہو، اور related-object descriptor اس وقت تک database کو hit نہیں کرتا جب تک اس تک رسائی نہ ہو۔ یہ ایک سوچا سمجھا اور عمومی طور پر اچھا default ہے: اس کا مطلب یہ ہے کہ Order.objects.filter(...) کو کئی سطور میں build کیا جا سکتا ہے، آگے pass کیا جا سکتا ہے، اور query قبل از وقت چلائے بغیر مزید filter بھی کیا جا سکتا ہے۔
اس laziness کی قیمت یہ ہے کہ Django کے پاس اس مقام پر، جہاں آپ orders = Order.objects.filter(...) لکھتے ہیں، یہ جاننے کا کوئی طریقہ نہیں ہوتا کہ اگلی ہی سطر میں آپ ہر row پر .customer کو touch کرنے والے ہیں۔ Django کے نقطۂ نظر سے ہر order.customer access ایک بالکل آزاد فیصلہ ہے — اسے یہ یاد نہیں رہتا کہ وہ انچاس rows پہلے بالکل اسی قسم کے سوال کا جواب دے چکا ہے۔ select_related اور prefetch_related خاص طور پر اسی لیے موجود ہیں تاکہ Django کو پہلے سے بتایا جا سکے: “تمہیں پہلے ہی معلوم ہے کہ مجھے ہر row کے لیے یہ related data درکار ہوگا — اسے ابھی لے آؤ، ایک یا دو queries میں، بجائے اس کے کہ ہر row پر الگ پوچھنے کا انتظار کرو۔”
یہاں اس بات میں درستگی ضروری ہے کہ آخر cache کیا ہوتا ہے، کیونکہ یہی اکثر confusion کا ذریعہ بنتا ہے۔ جب ایک queryset evaluate ہو جاتا ہے (ایک بار iterate کیا جائے، slice کیا جائے، یا list() سے force کیا جائے)، تو اس کے results اسی queryset instance پر cache ہو جاتے ہیں — دوسری بار iterate کرنے سے query دوبارہ نہیں چلتی۔ لیکن یہ cache queryset object پر رہتی ہے، ان model instances پر نہیں جو اس نے پیدا کیے، اور نہ ہی یہ related-object lookups تک پھیلتی ہے۔ ایک ہی order object پر پہلی بار order.customer اور دوسری بار order.customer Django کے per-instance relation cache کو hit کرتا ہے، اس لیے صرف ایک بار query ہوتی ہے — مگر تھوڑی دیر بعد Order.objects.filter(...) کی نئی call صفر سے شروع ہوتی ہے، اسے اس بات کی کوئی یادداشت نہیں ہوتی کہ پچھلے queryset نے کیا fetch کیا تھا۔ یہی وجہ ہے کہ N+1 خاص طور پر loops میں ظاہر ہوتا ہے: ہر iteration ایک الگ model instance پیدا کرتی ہے جس کا اپنا خالی relation cache ہوتا ہے، اس لیے “یہ تو پہلے پوچھا جا چکا ہے” والی بچت rows کے درمیان کبھی جمع ہی نہیں ہوتی۔
Async Views اور Query Counting
Django کی async views (async def get(self, request), جو 4.1 سے دستیاب ہیں) اوپر بیان کیے گئے کسی بھی mechanics کو تبدیل نہیں کرتیں — select_related اور prefetch_related بالکل ویسے ہی کام کرتے ہیں، اور sync_to_async میں لپٹی ORM calls بھی اسی طرح batch ہوتی ہیں۔ جو چیز بدلتی ہے وہ یہ ہے کہ N+1 مسئلہ نظر سے اوجھل کرنا کتنا آسان ہو جاتا ہے: asyncio کے تحت concurrent requests ایک ہی worker پر interleave ہوتی ہیں، اس لیے ایک سست اور query-heavy view پورے process کو ویسے block نہیں کرتی جیسے synchronous view کرتی، اور query-count regression بظاہر مناسب wall-clock latency کے پیچھے چھپ سکتی ہے جب تک concurrency نہ بڑھ جائے۔ debugging کا طریقہ وہی رہتا ہے — CaptureQueriesContext اور assertNumQueries، async def test clients کے تحت بھی ویسے ہی کام کرتے ہیں — مگر async کو اس کی وجہ سمجھیں کہ query counts زیادہ دانستہ انداز میں test کیے جائیں، نہ کہ اس لیے کہ اب ان کی اہمیت کم ہو گئی ہے۔
select_related: forward اور one-to-one relationships کے لیے
select_related ایک SQL JOIN بنا کر کام کرتا ہے اور related row کے columns کو parent کے ساتھ اسی query میں لے آتا ہے — ایک round trip، ایک result set، اور related objects پہلے سے populated ہوتے ہیں جب آپ ان تک رسائی کرتے ہیں۔
orders = Order.objects.filter(status="pending").select_related("customer")
for order in orders:
print(order.customer.name) # no extra query — already joined
یہ صرف ان relationships کے لیے کام کرتا ہے جہاں ہر row کے مقابلے میں join ہونے کے لیے ٹھیک ایک related row ہو: ForeignKey اور OneToOneField، جب انہیں “forward” direction میں follow کیا جائے (Order سے اس کے Customer تک، اس کے برعکس نہیں)۔ آپ ایک ہی call میں کئی hops بھی chain کر سکتے ہیں:
Order.objects.select_related("customer__account__billing_address")
ہر اضافی hop نئی query کے بجائے اسی joined query میں مزید columns شامل کرتی ہے، اس لیے chain کتنی ہی گہری ہو، round trip ایک ہی رہتی ہے — اس کا tradeoff ہر row کے لیے چوڑا result set ہے، جو اس وقت اہم ہو جاتا ہے جب joined tables میں بہت سے ایسے columns ہوں جن کی آپ کو حقیقت میں ضرورت نہ ہو۔ select_related reverse foreign keys یا many-to-many relationships میں مدد نہیں کر سکتا، کیونکہ ایسا JOIN جو ہر parent row کے لیے کئی related rows واپس لا سکتا ہو، اس قدرتی one-row-per-parent structure کو توڑ دیتا ہے جو JOIN بناتا ہے — اور یہی وہ صورت ہے جس کے لیے prefetch_related موجود ہے۔
prefetch_related: reverse اور many-to-many relationships کے لیے
prefetch_related بالکل مختلف حکمتِ عملی اپناتا ہے: ایک JOINed query کے بجائے، یہ دوسری، الگ query چلاتا ہے جو پورے batch کے لیے تمام related rows ایک ساتھ fetch کرتی ہے، پھر Python میں انہیں درست parent objects کے ساتھ جوڑ دیتی ہے۔
customers = Customer.objects.filter(active=True).prefetch_related("orders")
for customer in customers:
for order in customer.orders.all(): # no extra query per customer
print(order.total)
یہ کُل دو queries بنتی ہیں، چاہے customers 10 ہوں یا 10,000: ایک SELECT * FROM customers WHERE active، اور ایک SELECT * FROM orders WHERE customer_id IN (...) جو پہلے result set کے ہر customer کو cover کرتی ہے۔ پھر Django Python میں دوسرے result set کو customer_id کے لحاظ سے group کرتا ہے اور ہر order کو اس کے متعلقہ customer کے ساتھ attach کر دیتا ہے، لہٰذا loop کے اندر customer.orders.all() دوبارہ database کو touch نہیں کرتی۔
یہ reverse foreign keys (customer.orders، یعنی “many” side سے “one” کی طرف)، many-to-many fields، اور ہر ایسے relationship کے لیے درست (اور واحد) tool ہے جہاں ایک parent row کے ساتھ کئی matching related rows ہو سکتی ہوں — JOIN parent rows کو duplicate کیے بغیر اسے ظاہر نہیں کر سکتا، اس لیے prefetch_related کا الگ-query-پلس-Python-merge طریقہ صرف متبادل انداز نہیں بلکہ ساختی ضرورت ہے۔ select_related اور prefetch_related کے درمیان انتخاب مکمل طور پر relationship cardinality پر منحصر ہے: اگر ہر parent کے لیے ایک related row ہے تو select_related؛ اگر ممکنہ طور پر کئی related rows ہیں تو prefetch_related۔
select_related | prefetch_related | |
|---|---|---|
| Relationship shape | ہر parent کے لیے ایک related row | ہر parent کے لیے صفر، ایک، یا کئی related rows |
| Applies to | ForeignKey, OneToOneField (forward) | reverse FK, ManyToManyField, GenericRelation |
| Mechanism | ایک query، SQL JOIN | دو (یا زیادہ) queries، Python میں merge |
| Query count | پوری chain کے لیے ہمیشہ 1 | ہر prefetched relationship کے لیے 1 + 1 |
| Can filter/order the related set | نہیں (join مکمل parent columns واپس لاتا ہے) | ہاں، Prefetch objects کے ذریعے |
آخری سطر اپنی جگہ الگ سے ذہن نشین کرنے کے قابل ہے: select_related یہ محدود نہیں کر سکتا کہ کون سی related row واپس آئے، کیونکہ JOIN یا match کرتا ہے یا نہیں کرتا — ایک plain join کے اندر “صرف سب سے حالیہ match دو” جیسا کوئی تصور نہیں ہوتا۔ جب بھی کسی related lookup کو اپنی filter یا ordering درکار ہو، آپ prefetch_related کے دائرے میں ہوتے ہیں، چاہے relationship بظاہر ہر parent کے لیے ایک row ہی کیوں نہ ہو، کیونکہ customized queryset قبول کرنے کا واحد mechanism Prefetch object ہے۔
Prefetch objects: related data کی filtering اور ordering
prefetch_related("orders") تمام related orders کو fetch کرتا ہے۔ اکثر آپ کو صرف filtered یا ordered subset چاہیے ہوتا ہے — مثلاً صرف pending orders، یا صرف پانچ سب سے حالیہ — اور plain string argument یہ اظہار نہیں کر سکتی۔ Prefetch objects یہ کر سکتے ہیں:
from django.db.models import Prefetch
recent_pending = Prefetch(
"orders",
queryset=Order.objects.filter(status="pending").order_by("-created_at"),
to_attr="recent_pending_orders",
)
customers = Customer.objects.prefetch_related(recent_pending)
for customer in customers:
for order in customer.recent_pending_orders: # already filtered, already ordered
print(order.total)
queryset argument آپ کو prefetch کے اندر ہی filter، order، یا مزید select_related تک کرنے دیتا ہے — ایک Prefetch اپنے اندر nested select_related رکھ سکتا ہے تاکہ many-to-many relationship سے لٹکی ہوئی one-to-one relationship کو بھی cover کیا جا سکے، اور پھر بھی total queries صرف دو ہی رہیں۔ to_attr argument filtered result کو default manager پر overwrite کرنے کے بجائے ایک نئے attribute name کے تحت محفوظ کرتا ہے، جو اس لیے اہم ہے کہ اگر request میں کہیں اور اسی customer object کو بغیر filtered .orders.all() call کے ساتھ دوبارہ استعمال کیا جائے تو بصورتِ دیگر خاموشی سے ایک نئی، unprefetched query trigger ہو جائے گی۔
جب دونوں درست نہ ہوں: iteration کے بجائے aggregation
بعض اوقات اصل مقصد یہ نہیں ہوتا کہ “ہر row کے لیے ہر related object دو” — بلکہ ہر row کے لیے ایک عدد یا چند اعداد درکار ہوتے ہیں، جیسے order count یا total۔ مکمل related objects کو prefetch_related سے fetch کر کے Python میں ان پر صرف len() یا sum() چلانا ضائع شدہ کام ہے؛ database یہ aggregate براہِ راست ایک ہی query میں نکال سکتا ہے، اور individual related rows کو Python objects میں materialize کرنے کی بھی ضرورت نہیں رہتی۔
from django.db.models import Count, Sum
customers = Customer.objects.annotate(
order_count=Count("orders"),
lifetime_total=Sum("orders__total"),
)
for customer in customers:
print(customer.order_count, customer.lifetime_total) # no related objects fetched at all
یہ پچھلے دو حلوں سے واقعی ایک مختلف حل ہے، اور جب بھی آخری مقصد related rows نہیں بلکہ کوئی عدد ہو، یہی درست راستہ ہے۔ annotate() aggregation کو database کے اندر push کرتا ہے — وہی جگہ جہاں GROUP BY اور COUNT() پہلے ہی یہ کام مؤثر طریقے سے کرتے ہیں — اس کے بجائے کہ ہر related row کو network پر لا کر Python میں دوبارہ سمیٹا جائے۔
CI میں regressions پکڑنا
N+1 کا بہترین حل ایک وقتی cleanup pass نہیں — بلکہ regression کو ship ہونے سے پہلے test میں fail کروانا ہے۔ django-test-plus کا assertNumQueries، یا Django کا اپنا built-in version، کسی view یا code path کے لیے متوقع query count کو pin کر دیتا ہے:
from django.test.utils import CaptureQueriesContext
from django.db import connection
def test_order_list_view_query_count(self):
with self.assertNumQueries(2):
response = self.client.get("/orders/")
self.assertEqual(response.status_code, 200)
ایسا test اسی لمحے زور سے fail ہو جاتا ہے جب کوئی شخص template یا serializer میں نیا order.customer.name access شامل کر دے مگر ساتھ ہی متعلقہ queryset کی select_related/prefetch_related chain update نہ کرے — یوں regression دو سیکنڈ کی CI run میں پکڑی جاتی ہے، بجائے اس کے کہ ہفتوں بعد production APM dashboard میں سامنے آئے۔ پہلے سب سے زیادہ traffic والی list اور detail views پر count pin کریں؛ فی request N+1 fan-out وہیں سب سے زیادہ نقصان کرتا ہے، اور fix شدہ test وہیں سب سے تیزی سے اپنی قیمت وصول کرتی ہے۔
عام غلطیاں
غلطی: reverse foreign key پر select_related استعمال کرنے کی کوشش۔ Django FieldError دے گا — select_related واقعی ایسے relationship کو follow نہیں کر سکتا جہاں کئی rows match کر سکتی ہوں۔ حل: “many” side کی ہر چیز کے لیے reflexively prefetch_related استعمال کریں۔
غلطی: کسی relationship کو prefetch کرنا، پھر loop کے اندر دوبارہ .filter() لگانا۔ prefetch_related("orders") کے بعد loop کے اندر customer.orders.filter(status="pending") ہر row کے لیے بالکل نئی query trigger کرتا ہے، کیونکہ نئی .filter() call cached prefetch سے مختلف queryset ہوتی ہے۔ حل: filter کو ابتدا ہی میں Prefetch object کے queryset argument کے اندر لگائیں، اور to_attr استعمال کریں تاکہ غلطی سے unfiltered، unprefetched call پر واپس جانے کا امکان ہی نہ رہے۔
غلطی: بہت چوڑی join chains میں select_related کے ذریعے over-fetching۔ select_related کو پانچ hops گہرا chain کرنے سے chain کی ہر table کے تمام columns ہر row میں کھنچ آتے ہیں، چاہے اصل میں صرف سب سے گہری table کا ایک field ہی استعمال ہو رہا ہو۔ حل: joined columns محدود کرنے کے لیے select_related کے ساتھ .only() استعمال کریں، یا دوبارہ غور کریں کہ اس view کے لیے یہ deep join واقعی ضروری بھی ہے یا نہیں۔
غلطی: یہ سمجھ لینا کہ کم total query count کا مطلب N+1 مسئلہ نہیں۔ دس rows کے لیے دس queries ایک سادہ “queries کسی حد سے کم ہوں” check سے تو گزر سکتی ہیں، مگر پھر بھی یہ ایک حقیقی N+1 pattern ہے جو table کے بڑھنے کے ساتھ خطی طور پر خراب ہوگا۔ حل: صرف raw count نہیں بلکہ repeated-query-shape والے signal کو خاص طور پر دیکھیں، اور test ہمیشہ حقیقی row counts کے ساتھ دوبارہ کریں، صرف fixture-size data کے ساتھ نہیں۔
غلطی: view میں N+1 ٹھیک کر دینا مگر serializer میں نہیں۔ nested relations والے Django REST Framework serializers بالکل وہی lazy-loading مسئلہ دوبارہ trigger کرتے ہیں، چاہے view کے queryset نے پہلے کیا کیا ہو — nested OrderSerializer(many=True) والا CustomerSerializer خوشی خوشی ہر customer کے لیے ایک query چلا دے گا اگر view کے queryset نے کبھی prefetch_related("orders") نہ بلایا ہو، چاہے view خود کتنی ہی احتیاط سے لکھی گئی ہو۔ حل: select_related/prefetch_related اسی queryset پر لگائیں جسے serializer حقیقت میں consume کرتا ہے (عموماً ViewSet کے get_queryset() میں)، اور تصدیق serialized response پر assertNumQueries کے ساتھ کریں، صرف raw queryset پر نہیں — serializer اپنے lazy access points خود متعارف کروا سکتا ہے جنہیں queryset-only test کبھی پکڑ ہی نہیں پائے گا۔
خلاصہ
N+1 queries Django کا ایسا bug نہیں جس کے گرد راستہ نکالا جائے — یہ lazy relationship loading کا ایک متوقع خرچ ہے جب وہ ایسے code سے ملتی ہے جو ORM کو کبھی نہیں بتاتا: “اسے batch کر دو۔” اس کا حل کبھی یہ نہیں کہ “ہر جگہ prefetch_related لگا دو اور امید رکھو” — بلکہ یہ ہے کہ relationship کی اصل cardinality پہچانی جائے (ایک row یا کئی)، اس کے مطابق select_related یا prefetch_related منتخب کیا جائے، جب related set کو filtering یا ordering درکار ہو تو Prefetch objects استعمال کیے جائیں، اور یہ سمجھا جائے کہ کب اصل مقصد related rows نہیں بلکہ کوئی aggregate number تھا جسے database کو براہِ راست calculate کرنا چاہیے، نہ کہ ایسا iteration جو Python ہاتھ سے کرے۔
بنیادی database کو آخرکار وہی query count execute کرنا ہوتا ہے جو آپ کا ORM code پیدا کرتا ہے، اس لیے جب query count خود درست ہو جائے تو نتیجے میں بننے والا plan بھی پڑھنا فائدہ مند ہوتا ہے — select_related join اتنی ہی تیز ہوتی ہے جتنا index اسے سہارا دے رہا ہو، اور یہیں سے EXPLAIN ANALYZE guide اگلا مرحلہ سنبھالتی ہے۔
کیا آپ نے اپنے سب سے زیادہ traffic والے view کی query count کو اس کے اصل production row counts کے مقابلے میں چیک کیا ہے، یا صرف اپنے local fixtures کے مقابلے میں؟
