بلاگز / Postgres EXPLAIN ANALYZE: سوالات کو سمجھنا اور بہتر بنانا

Postgres EXPLAIN ANALYZE: سوالات کو سمجھنا اور بہتر بنانا

شائع ہوا
1 ستمبر، 2026
مصنف
Faizan Nadeem
ٹیگز
PostgreSQL Database Performance Backend Development
کمپیوٹر مانیٹر کی ایک قریب سے لی گئی تصویر جس پر ایک لائن گراف دکھایا گیا ہے
تصویر از Nicholas Cappello on Unsplash

زیادہ تر ڈویلپرز EXPLAIN ANALYZE کو تقریباً اسی طرح چلاتے ہیں جیسے وہ کسی ایسی error پر stack trace چلاتے ہیں جسے وہ سمجھتے نہیں — query میں paste کیا، جواب میں متن کی ایک دیوار ملی، کسی بڑے نمبر پر نظر دوڑائی، اور پھر اندازہ لگا لیا۔ یہ اندازہ عموماً “index شامل کریں” یا “مسئلہ JOIN میں ہے” ہوتا ہے، اور کبھی کبھار اتفاقاً درست بھی نکل آتا ہے۔ حالانکہ plan خود آپ کو پہلے ہی بالکل واضح طور پر بتا رہا ہوتا ہے کہ کیا سست ہے اور کیوں؛ اصل مسئلہ یہ ہے کہ ہم میں سے اکثر کو کبھی یہ سکھایا ہی نہیں گیا کہ اسے monospace متن کے خوفناک بلاک کے بجائے ایک structured document کے طور پر کیسے پڑھا جائے۔

یہ خلا حقیقی وقت ضائع کراتا ہے۔ جو ڈویلپر plan پڑھ نہیں سکتا وہ درست حل تک پہنچنے سے پہلے تین غیر متعلقہ fixes آزما لیتا ہے — پہلے SELECT * کم کرنے کی کوشش، پھر cache layer، اور آخر میں جا کر index، جبکہ اگر آپ کو دیکھنے کی جگہ معلوم ہو تو plan پہلے دس سیکنڈ میں ہی “missing index” کہہ دیتا ہے۔ یہ post Postgres query plan کی اصل grammar سے گزارتی ہے: ہر line میں کیا encode ہوتا ہے، کون سے نمبر estimates ہیں اور کون سے حقیقت، loop counts کیسے hidden costs کو multiply کرتے ہیں، buffer counters اصل میں کیا measure کرتے ہیں، اور وہ تین بار بار آنے والی plan shapes جو Postgres کے انداز میں آپ کو بتاتی ہیں کہ index موجود نہیں۔

آپ یہ سیکھیں گے:

  • plan کی tree structure کیسے پڑھی جائے اور کیسے پہچانا جائے کہ کون سا node واقعی cost کو drive کر رہا ہے
  • planner کی estimated rows اور PostgreSQL کی actual rows میں کیا فرق ہے، اور ان دونوں کے درمیان بڑا gap پورے plan میں سب سے مفید signal کیوں ہے
  • کیوں ایک بظاہر سستا node اگر loop کے اندر چلے تو کل query time پر غالب آ سکتا ہے
  • BUFFERS output کیسے پڑھیں — shared hit بمقابلہ read، اور یہ cache pressure کے بارے میں کیا بتاتا ہے
  • وہ تین plan patterns — filtered بڑی table پر sequential scan، high loop count کے ساتھ nested loop، اور disk پر spill ہونے والا sort — جو تقریباً ہمیشہ missing یا غلط index کی طرف اشارہ کرتے ہیں
  • ایک مکمل worked example جس میں صرف plan کی بنیاد پر واقعی سست query کی تشخیص کی گئی ہے

فہرستِ مضامین

  1. EXPLAIN ANALYZE اصل میں کیا کرتا ہے
  2. Plan line کی ساخت
  3. Estimated بمقابلہ Actual Rows
  4. Loops: کیوں ایک سستا Node غالب آ سکتا ہے
  5. Buffers پڑھنا: Cache Hits بمقابلہ Disk Reads
  6. وہ تین Patterns جو Missing Index کی علامت ہیں
  7. ایک مکمل Walkthrough
  8. عام غلطیاں
  9. یہاں سے آگے کہاں جائیں

EXPLAIN ANALYZE اصل میں کیا کرتا ہے

اکیلا EXPLAIN planner سے پوچھتا ہے کہ وہ کیا کرتا — وہ table statistics کی بنیاد پر ایک plan estimate کرتا ہے اور کچھ بھی چلائے بغیر اسے print کر دیتا ہے۔ EXPLAIN ANALYZE واقعی query execute کرتا ہے، ہر step کا وقت لیتا ہے، اور پھر اسی tree کو اس annotation کے ساتھ print کرتا ہے کہ حقیقت میں کیا ہوا۔ یہ فرق سننے سے زیادہ اہم ہے: EXPLAIN کسی بھی چیز پر محفوظ طریقے سے چلایا جا سکتا ہے، حتیٰ کہ ایسے DROP پر بھی جو transaction میں لپٹا ہو اور جسے آپ roll back کرنا چاہتے ہوں، لیکن EXPLAIN ANALYZE حقیقتاً INSERT, UPDATE, یا DELETE چلاتا ہے جب تک کہ آپ اسے BEGIN; ... ROLLBACK; میں wrap نہ کریں۔

حقیقی debugging کے لیے آپ کو جو output چاہیے، وہ یہ ہے:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at > now() - interval '7 days'
  AND o.status = 'pending';

ANALYZE آپ کو حقیقی timings اور حقیقی row counts دیتا ہے۔ BUFFERS آپ کو cache-hit معلومات دیتا ہے، جو تاریخی وجوہات کی بنا پر default میں بند ہوتی ہے لیکن ہر حقیقی investigation میں واقعی مفید ہوتی ہے — اسے ہر بار آن کریں۔ COSTS OFF کو تب تک چھوڑ دیں جب تک آپ plan کہیں paste نہیں کر رہے جہاں runs کے درمیان diff ہونا ضروری ہو؛ آپ کو actual numbers کے مقابلے کے لیے cost estimates چاہییں۔

ایک plan ایک tree ہوتا ہے، جسے نیچے سے اوپر اور اندر سے باہر پڑھا جاتا ہے۔ سب سے اندرونی، زیادہ indented nodes پہلے چلتے ہیں؛ ان کا output ان کے اوپر والے node میں جاتا ہے۔ سب سے اوپر والی line وہ آخری چیز ہے جو ہوتی ہے اور اسی میں total time اور total cost report ہوتے ہیں۔ اسے نثر کی طرح اوپر سے نیچے پڑھنے کا دل چاہتا ہے، لیکن اصل data flow — اور عموماً اصل bottleneck — نیچے موجود leaves میں ہوتا ہے۔

Plan line کی ساخت

یہ ایک حقیقی plan سے لیا گیا ایک node ہے، جسے ٹکڑوں میں annotate کیا گیا ہے:

Seq Scan on orders o  (cost=0.00..18734.00 rows=812 width=24)
                       (actual time=0.021..142.558 rows=790 loops=1)
  Filter: (status = 'pending'::text)
  Rows Removed by Filter: 199210
  Buffers: shared hit=210 read=8312

اسے یوں سمجھیں:

  • Seq Scan on orders o — operation اور وہ table (یا alias) جس پر یہ چل رہا ہے۔ Sequential scan table یا index کی ہر row کو physical order میں پڑھتا ہے؛ یہ خودبخود بری چیز نہیں، لیکن بڑی table پر یہی وہ چیز ہے جس پر نظر جاتی ہے۔
  • cost=0.00..18734.00 — planner کی estimated cost range، arbitrary planner cost units میں (milliseconds نہیں)۔ پہلا نمبر پہلی row واپس لانے کی estimated cost ہے؛ دوسرا تمام rows واپس لانے کی estimated cost۔ ان نمبروں کا موازنہ صرف اسی plan کے دوسرے cost numbers سے کیا جا سکتا ہے، کبھی بھی مختلف queries یا servers کے درمیان نہیں۔
  • rows=812 — planner کا estimate کہ یہ node کتنی rows پیدا کرے گا، table statistics کی بنیاد پر۔
  • width=24 — estimated average row width bytes میں۔
  • actual time=0.021..142.558 — حقیقی، measured milliseconds: پہلی row تک کا وقت، پھر completion تک کا وقت، اور یہ سب loop iterations کے average کے طور پر (اس پر نیچے مزید بات ہوگی)۔
  • rows=790 loops=1 — حقیقت میں اس node نے کتنی rows واپس کیں، اور یہ node کتنی بار execute ہوا۔
  • Filter / Rows Removed by Filter — rows fetch ہونے کے بعد لگایا جانے والا post-scan filter۔ چھوٹی final row count کے ساتھ بڑا “rows removed” نمبر ایک مضبوط signal ہے کہ scan ضرورت سے کہیں زیادہ کام کر رہا ہے۔
  • Buffers — کتنے 8KB pages touch ہوئے، cache hits اور disk reads میں تقسیم ہو کر۔

tree میں ہر node تقریباً اسی shape کا حامل ہوتا ہے۔ جب آپ ایک line parse کر لیتے ہیں تو پورا plan parse کر سکتے ہیں — اصل skill صرف یہ جاننے میں ہے کہ کن نمبروں کا ایک دوسرے سے موازنہ کرنا ہے۔

Estimated بمقابلہ Actual Rows

پورے plan میں یہ سب سے زیادہ قدر والا موازنہ ہے۔ planner کچھ بھی چلانے سے پہلے rows=812 estimate کرتا ہے، ان statistics کی بنیاد پر جو ANALYZE نے جمع کی ہوتی ہیں (EXPLAIN option والا نہیں بلکہ maintenance command والا ANALYZE — الجھن کی بات ہے کہ دونوں کا نام ایک ہی ہے)۔ execution کے بعد، Postgres report کرتا ہے کہ حقیقت میں کیا نکلا۔ جب یہ دونوں نمبر قریب ہوں، تو planner کے پاس اچھی معلومات تھیں اور اس نے تقریباً یقیناً اچھا plan منتخب کیا۔ جب ان میں 10x، 100x، یا اس سے زیادہ فرق ہو، تو اس estimate پر بنی ہر چیز — join strategy، memory allocation، execution order — خراب معلومات کے ساتھ طے کی گئی ہوتی ہے، اور نتیجے میں plan اکثر ایسے طریقوں سے برا نکلتا ہے جن کی پیش گوئی صرف estimate سے کرنا مشکل ہوتا ہے۔

-> Index Scan using idx_orders_status on orders
     (cost=0.42..8.44 rows=1 width=24)
     (actual rows=48000 loops=1)

1 row کا estimate اور 48,000 کی actual count ایک بہت بڑا misestimate ہے۔ یہ عموماً چند وجوہات میں سے کسی ایک کی بنا پر ہوتا ہے:

  • Stale statistics. آخری ANALYZE کے بعد table کافی بدل چکی ہے، اور autovacuum ابھی تک پیچھے ہے۔ ANALYZE orders; دستی طور پر چلائیں اور موازنہ کریں۔
  • Correlated columns. planner default میں یہ فرض کرتا ہے کہ columns ایک دوسرے سے independent ہیں۔ WHERE status = 'pending' AND created_at > now() - interval '7 days' جیسا filter دونوں columns کو ملا کر انفرادی طور پر کسی ایک column سے کہیں زیادہ (یا کم) selective ہو سکتا ہے، اور default statistics یہ correlation capture نہیں کرتیں۔ Postgres کی CREATE STATISTICS extended statistics کے لیے خاص اسی مسئلے کو درست کرنے کے لیے موجود ہے۔
  • Non-uniform data distribution. اگر skewed column میں 90% rows ایک ہی value رکھتی ہوں، تو default statistics target (default میں 100 buckets) اس skew کو کافی باریکی سے resolve نہیں کر پاتا۔ default_statistics_target بڑھانا، یا ALTER TABLE ... ALTER COLUMN ... SET STATISTICS کے ذریعے per-column set کرنا، planner کو زیادہ fine-grained histogram دیتا ہے۔

جب آپ عمومی postgres slow query debugging کر رہے ہوں، تو سب سے پہلے یہیں دیکھیں — buffers سے پہلے، cost numbers سے پہلے، ہر چیز سے پہلے — کیونکہ خراب estimate عموماً اصل وجہ ہوتا ہے، اور باقی سب downstream symptoms ہوتے ہیں۔

Loops: کیوں ایک سستا Node غالب آ سکتا ہے

loops=1 کا مطلب ہے node ایک بار execute ہوا۔ nested loop join کے اندر ایک inner node، outer side سے پیدا ہونے والی ہر row پر ایک بار execute ہو سکتا ہے — اور اس node کے لیے report ہونے والا actual time ہر loop کا average ہوتا ہے، total نہیں۔ پورے plan میں غلط فہمی کی یہ سب سے عام وجہ ہے۔

Nested Loop  (actual time=0.045..891.223 rows=48000 loops=1)
  -> Seq Scan on customers c (actual time=0.010..12.400 rows=4000 loops=1)
  -> Index Scan using idx_orders_customer on orders o
       (actual time=0.008..0.019 rows=12 loops=4000)

یہ inner index scan بہت معمولی طور پر سستا لگتا ہے: 0.019ms۔ مگر یہ 4,000 بار چلا — outer scan سے آنے والی ہر customer row کے لیے ایک بار — اس لیے اس کی حقیقی contribution تقریباً 0.019ms * 4000 ≈ 76ms ہے، 0.019ms نہیں۔ کسی node کی حقیقی cost پر فیصلہ کرنے سے پہلے ہمیشہ actual time کو loop count سے multiply کریں۔ بہت کم per-loop time لیکن پانچ ہندسوں کے loop count کے ساتھ اکثر اصل bottleneck ہوتا ہے جو صاف نظر کے سامنے چھپا ہوتا ہے، جبکہ وہ node جس کا واحد actual time نمبر سب سے بڑا ہو، نسبتاً کم contribution دے رہا ہوتا ہے۔

یہی mechanism اس کلاسک bug کے پیچھے بھی ہوتا ہے: “100 rows پر ٹھیک چلتا ہے، 100,000 پر گر جاتا ہے”۔ nested loop ایک چھوٹی outer side کے لیے ٹھیک strategy ہے، اور جیسے جیسے outer side بڑھتی ہے ویسے ویسے linearly خراب ہوتی جاتی ہے — یہی N+1 problem کی شکل ہے جو ORM level پر بھی نظر آتی ہے۔ application side سے اس pattern کے لیے Django N+1 post دیکھیں۔

Buffers پڑھنا: Cache Hits بمقابلہ Disk Reads

Buffers: shared hit=210 read=8312 shared buffer cache کے خلاف 8KB page accesses report کرتا ہے:

  • shared hit — وہ pages جو پہلے ہی PostgreSQL کے shared buffer cache میں موجود تھے۔ تیز؛ عملی طور پر RAM speed۔
  • shared read — وہ pages جو disk سے لانے پڑے (یا OS page cache سے، جسے Postgres حقیقی disk I/O سے الگ نہیں پہچان سکتا) کیونکہ وہ buffer cache میں نہیں تھے۔
  • shared dirtied — وہ pages جو اس operation میں modify ہوئے، لکھائیوں میں relevant۔
  • shared written — وہ pages جو جگہ بنانے کے لیے باہر لکھے گئے، جو اکثر buffer cache pressure کی نشانی ہوتے ہیں۔

اگر frequently چلنے والی query میں read کا تناسب hit کے مقابلے میں زیادہ ہو، تو یہ signal ہے کہ working set آرام سے shared_buffers میں fit نہیں ہوتا — یا پھر یہ صرف کسی کم چلنے والی query کا cold cache ہے۔ ایک ہی EXPLAIN (ANALYZE, BUFFERS) کو لگاتار دو بار چلائیں؛ اگر دوسری بار وہاں زیادہ hits ہوں جہاں پہلی بار reads تھیں، تو آپ cold-cache numbers دیکھ رہے تھے، steady-state cost نہیں۔

Buffers سب سے زیادہ ایماندار cost signal بھی ہیں، کیونکہ یہ arbitrary planner cost units سے scale نہیں ہوتے — یہ literal page counts ہیں، جن کا براہِ راست موازنہ مختلف queries اور ایک ہی query کے مختلف plans کے درمیان کیا جا سکتا ہے۔ جب دو candidate indexes ایسے plans دیتے ہوں جن کا actual time ملتا جلتا ہو، تو وہ plan جس میں total buffer touches کم ہوں، حقیقتاً کم I/O کام کر رہا ہوتا ہے اور concurrent load میں بہتر ثابت ہوتا ہے۔

وہ تین Patterns جو Missing Index کی علامت ہیں

کافی plans دیکھنے کے بعد تین shapes بار بار سامنے آتی ہیں، اور تینوں ایک ہی root cause کی طرف اشارہ کرتی ہیں۔

1. بڑی table پر selective filter کے ساتھ sequential scan۔

Seq Scan on orders o  (cost=0.00..18734.00 rows=812 width=24)
                       (actual time=0.021..142.558 rows=790 loops=1)
  Filter: (status = 'pending'::text)
  Rows Removed by Filter: 199210

200,000 rows scan کر کے ان میں سے صرف 790 رکھنا، اس plan کے تقریباً تمام کام کا rows کو پھینکنے میں صرف ہونا ہے۔ Rows Removed by Filter کا final row count سے کئی orders of magnitude زیادہ ہونا، ایسی table پر جو cache میں آرام سے fit نہ ہو، Postgres کا بالواسطہ یہ کہنا ہے: “میرے پاس ایسی کوئی index نہیں جو سیدھا مطلوبہ rows تک پہنچا دے۔” status پر index — یا اس سے بہتر partial index (WHERE status = 'pending') اگر یہ value نایاب ہو — اسے ایسے index scan میں بدل دیتا ہے جو صرف matching rows کو touch کرتا ہے۔

2. بہت زیادہ loop count کے ساتھ nested loop جو ایک unindexed inner scan کو feed کر رہا ہو۔

-> Seq Scan on order_items oi
     (actual time=0.412..3.891 rows=6 loops=4000)
   Filter: (order_id = o.id)

ایک inner sequential scan جو ہزاروں بار چل رہا ہو، اور ہر بار پوری order_items table کو filter کر کے چند matching rows تک لا رہا ہو، دراصل پچھلے section والے loop pattern اور join column پر missing index کا مجموعہ ہے۔ order_items(order_id) پر index ان 4,000 sequential scans میں سے ہر ایک کو ایک سستے index lookup میں بدل دیتا ہے، اور کل query time عموماً ایک order of magnitude یا اس سے بھی زیادہ کم ہو جاتا ہے۔

3. ایسا sort جو memory میں مکمل ہونے کے بجائے disk پر spill ہو جائے۔

Sort  (cost=41293.55..41808.36 rows=205925 width=32)
      (actual time=387.223..421.009 rows=205925 loops=1)
  Sort Method: external merge  Disk: 7128kB
  -> Seq Scan on orders ...

Sort Method: external merge Disk: ...kB کا مطلب ہے sort work_mem میں fit نہیں ہوا اور disk پر temporary files میں spill ہو گیا — جو in-memory quicksort یا top-N heapsort کے مقابلے میں معنی خیز طور پر سست ہے۔ یہاں دو الگ fixes لاگو ہوتے ہیں، اور دونوں ایک دوسرے کے مخالف نہیں: اگر server میں headroom ہو تو session یا query کے لیے work_mem بڑھا دیں، یا — جو عموماً بہتر fix ہے — ORDER BY clause سے match کرتی ہوئی index شامل کریں تاکہ sort مکمل طور پر avoid ہو جائے اور rows Sort node کے بجائے Index Scan سے پہلے سے ordered نکلیں۔

ان تینوں patterns کے پیچھے ایک ہی بنیادی کہانی ہے: planner اپنی پوری کوشش کر رہا ہے مگر اس کے پاس sequential یا brute-force strategy کے سوا کوئی بہتر راستہ موجود نہیں۔ ان shapes میں سے کس کے لیے کون سی index type مناسب ہے — B-tree, GIN, GiST, یا BRIN — اس کی مکمل وضاحت کے لیے Postgres index types guide اس فیصلے کو گہرائی سے سمجھاتی ہے۔

ایک مکمل Walkthrough

ایک واقعی سست endpoint لیں: “کسی customer account کے لیے پچھلے ہفتے کی pending orders کی فہرست، نئی سے پرانی ترتیب میں۔” Query:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total, created_at
FROM orders
WHERE customer_id = 4821
  AND status = 'pending'
  AND created_at > now() - interval '7 days'
ORDER BY created_at DESC
LIMIT 20;

کسی بھی تبدیلی سے پہلے پہلا plan:

Limit  (actual time=203.441..203.448 rows=14 loops=1)
  -> Sort  (actual time=203.439..203.443 rows=14 loops=1)
        Sort Key: created_at DESC
        Sort Method: quicksort  Memory: 26kB
        -> Seq Scan on orders  (actual time=0.033..201.887 rows=14 loops=1)
              Filter: ((customer_id = 4821) AND (status = 'pending')
                       AND (created_at > now() - interval '7 days'))
              Rows Removed by Filter: 611982
              Buffers: shared hit=402 read=9812

اسے پڑھیں: sort معمولی ہے (14 rows، in-memory quicksort)۔ Seq Scan نے 14 rows رکھنے کے لیے 611,982 rows filter out کیں، اور 10,000 سے زیادہ buffer pages touch کیے — pattern one، بالکل واضح طور پر۔ fix ایک composite index ہے جو filter columns سے match کرے، اور sort key کو بھی شامل کرے تاکہ database الگ sort step بھی ممکنہ طور پر چھوڑ سکے:

CREATE INDEX CONCURRENTLY idx_orders_customer_status_created
ON orders (customer_id, status, created_at DESC);

index بننے کے بعد وہی query دوبارہ چلائیں:

Limit  (actual time=0.061..0.089 rows=14 loops=1)
  -> Index Scan using idx_orders_customer_status_created on orders
        (actual time=0.060..0.086 rows=14 loops=1)
        Index Cond: ((customer_id = 4821) AND (status = 'pending')
                      AND (created_at > (now() - interval '7 days')))
        Buffers: shared hit=6

203ms سے 0.089ms تک، buffer touches ~10,200 سے 6 تک، اور Sort node مکمل طور پر غائب — index کی column order پہلے ہی LIMIT 20 کی ضرورت سے match کر رہی ہے۔ یہی پورا diagnostic loop ہے: tree پڑھیں، وہ node تلاش کریں جس کی حقیقی time contribution سب سے زیادہ ہو، اس کی shape کو ان تین patterns میں سے کسی ایک سے match کریں، index درست کریں، اور دوبارہ چلا کر confirm کریں کہ plan shape واقعی بدلی ہے۔

عام غلطیاں

غلطی: مختلف queries کے درمیان cost numbers کا موازنہ کرنا۔ Cost ایک unitless, planner-internal estimate ہے جو random_page_cost اور اس جیسے parameters کے مطابق calibrated ہوتی ہے — یہ صرف اسی plan کے دوسرے nodes کے نسبت معنی رکھتی ہے۔ حل: cross-query comparison کے لیے cost نہیں بلکہ actual time اور buffer counts کا موازنہ کریں۔

غلطی: looped node پر actual time کو total سمجھ لینا۔ جیسا اوپر دکھایا گیا، یہ نمبر per-loop average ہوتا ہے۔ حل: کسی node کی حقیقی contribution پر فیصلہ کرنے سے پہلے ہمیشہ اسے loops سے multiply کریں۔

غلطی: cold cache پر EXPLAIN ANALYZE ایک بار چلانا اور یہ نتیجہ نکالنا کہ production میں query سست ہے۔ restart کے فوراً بعد یا کم استعمال ہونے والے data پر پہلی run وہ disk I/O ادا کرتی ہے جو steady-state traffic میں عموماً نہیں ہوتا۔ حل: اسے دو بار چلائیں، اور صرف پہلے نمبر پر نہیں بلکہ buffer hit ratio پر بھروسا کریں۔

غلطی: یہ فرض کرنا کہ “index scan” ہمیشہ “seq scan” سے بہتر ہے۔ چھوٹی table پر، یا جب query کو ویسے بھی table کی زیادہ تر rows چاہیے ہوں، sequential scan حقیقتاً تیز ہوتا ہے — index traversal overhead کے بغیر۔ حل: صرف node name نہیں بلکہ actual time اور row counts کی بنیاد پر فیصلہ کریں۔

غلطی: index شامل کرنا اور پھر EXPLAIN ANALYZE دوبارہ نہ چلانا تاکہ confirm ہو سکے کہ plan واقعی بدلا ہے۔ Postgres ضروری نہیں کہ نئی index استعمال کرے اگر اس کی statistics اب بھی پرانے plan کو ترجیح دے رہی ہوں، یا اگر index query کے leading filter column سے match نہ کرے۔ حل: ہمیشہ دوبارہ چلائیں اور چیک کریں کہ plan shape بدلی ہے، صرف یہ نہیں کہ query تیز ہو گئی — صرف تیز وقت اس بات کا ثبوت نہیں کہ fix عمومی طور پر بھی کام کرے گی۔

یہاں سے آگے کہاں جائیں

یہ post ایک single plan کو تنہائی میں پڑھنے کا طریقہ بتاتی ہے۔ اس series کے باقی حصے ان فیصلوں کا احاطہ کرتے ہیں جو یہ طے کرتے ہیں کہ ابتدا میں کون سے plans ممکن ہی ہیں:

خلاصہ

Postgres query plan کوئی پراسرار output نہیں جسے صرف ڈراؤنے نمبروں کے لیے سرسری طور پر دیکھ لیا جائے — یہ ایک structured, honest report ہے کہ database نے بالکل کیا کیا، کس ترتیب سے کیا، اور یہ سب اس کی توقعات کے مقابلے میں کیسا رہا۔ اسے نیچے سے اوپر پڑھیں، پہلے estimated rows کا actual rows سے موازنہ کریں، looped nodes کی cost جانچنے سے پہلے ان کے loop count سے multiply کریں، cache pressure کے لیے buffer hit ratio دیکھیں، اور جو کچھ نظر آ رہا ہو اسے ان تین patterns سے match کریں جن کا مطلب ہوتا ہے “planner کے پاس یہاں کوئی اچھا option نہیں”۔ جب یہ reading order خودکار ہو جائے، تو EXPLAIN ANALYZE متن کی دیوار نہیں رہتا بلکہ پورے stack کا تیز ترین debugging tool بن جاتا ہے۔

اگلی بار جب کوئی query سست ہو، تو آپ کا پہلا قدم plan سے آئے گا یا اندازے سے؟

مزید مضامین