پلن اجرا، داستان کوئری است
EXPLAIN پلنی را نشان میدهد که برنامهریز انتخاب کرده با تخمینهایش؛ EXPLAIN ANALYZE کوئری را واقعاً اجرا میکند و عددهای واقعی را کنار تخمین میگذارد؛ BUFFERS میگوید چند صفحه از cache و چند صفحه از دیسک خوانده شده. همیشه این سه را با هم بخواهید.
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT c.full_name, sum(oi.line_total_rial) AS total
FROM factory.orders o
JOIN factory.customers c ON c.id = o.customer_id
JOIN factory.order_items oi ON oi.order_id = o.id
WHERE o.created_at >= '2026-03-21' AND o.status = 'delivered'
GROUP BY c.full_name
ORDER BY total DESC
LIMIT 10;
Limit (cost=4210.55..4210.58 rows=10 width=40) (actual time=38.112..38.115 rows=10 loops=1)
Buffers: shared hit=2950 read=412
-> Sort (cost=4210.55..4212.80 rows=900 width=40) (actual time=38.110..38.112 rows=10 loops=1)
Sort Key: (sum(oi.line_total_rial)) DESC
Sort Method: top-N heapsort Memory: 26kB
-> HashAggregate (... rows=900 ...) (actual ... rows=870 loops=1)
-> Hash Join (... rows=5200 ...) (actual ... rows=61340 loops=1)
Hash Cond: (oi.order_id = o.id)
-> Seq Scan on order_items oi (... rows=480000 ...) (actual ... rows=480000 loops=1)
-> Hash (... rows=1300 ...) (actual ... rows=15335 loops=1)
-> Index Scan using orders_status_created_at_idx on orders o ...
Index Cond: ((status = 'delivered') AND (created_at >= ...))
Planning Time: 0.9 ms
Execution Time: 38.4 ms
چگونه بخوانیم
- از داخلیترین گره به بیرون بخوانید؛ گرههای تورفتهتر زودتر اجرا میشوند و خروجیشان به والد میرود.
- cost دو عدد دارد: هزینهی شروع (تا اولین ردیف) و هزینهی کل. واحدش دلخواه است (یک صفحهی ترتیبی = 1)، نه میلیثانیه.
- rows تخمینی را با actual rows مقایسه کنید. در مثال بالا تخمین Hash Join پنج هزار و واقعیت شصت هزار است؛ ده برابر خطا. ریشهی بیشتر پلنهای بد همینجاست: آمار کهنه یا همبستگی بین ستونها که برنامهریز نمیداند.
- loops را ضرب کنید: actual time و rows برای هر بار اجرای گرهاند. Index Scan با time=0.05 و loops=200000 یعنی ده ثانیه.
- Buffers: hit یعنی از shared_buffers، read یعنی از سیستمعامل یا دیسک. هر صفحه 8KB است؛ read=412 حدود 3.3MB خواندن است.
علامتهای خطر
| آنچه میبینید | معنی | اقدام |
|---|---|---|
| Rows Removed by Filter بزرگ | ردیفهای زیادی خوانده و دور ریخته شده | ایندکس یا partial index مناسب |
| Sort Method: external merge Disk | work_mem کافی نبوده | افزایش work_mem برای همان نشست یا ایندکس برای ORDER BY |
| Nested Loop با loops عظیم | تخمین سطر داخلی خیلی کم بوده | ANALYZE، CREATE STATISTICS |
| Heap Fetches بالا در Index Only Scan | visibility map بهروز نیست | VACUUM |
| Batches بیش از 1 در Hash | جدول hash روی دیسک ریخته | work_mem یا hash_mem_multiplier |
برای DML از ANALYZE با احتیاط استفاده کنید؛ EXPLAIN ANALYZE DELETE واقعاً حذف میکند. آن را داخل BEGIN; ... ROLLBACK; بگذارید.
نکتههایی که کمتر کسی میداند
- گزینهی
SETTINGSپارامترهای غیرپیشفرضی را که روی پلن اثر گذاشتهاند فهرست میکند؛ وقتی پلن روی لپتاپ و سرور فرق دارد، اول همین را مقایسه کنید. - در نسخهی 18، BUFFERS همراه ANALYZE بهطور پیشفرض نمایش داده میشود؛ در 16 و 17 باید صریحاً بنویسید.
- اکستنشن
auto_explainپلن کوئریهای کندتر از یک آستانه را خودکار در لاگ مینویسد؛ تنها راه دیدن پلن کوئریای که فقط در ساعت شلوغی کند است. - زمانسنجی ANALYZE خودش سربار دارد (بهویژه روی ماشین مجازی با ساعت کند)؛
EXPLAIN (ANALYZE, TIMING OFF)فقط rows واقعی را میدهد و نزدیکتر به زمان واقعی اجراست. - عبارت «never executed» یعنی آن شاخه اصلاً اجرا نشده (مثلاً چون طرف دیگر Join خالی بود)؛ این گره را در تحلیل زمان نادیده بگیرید.