فصل ۵: ایندکس و کارایی

خواندن EXPLAIN (ANALYZE, BUFFERS) خط‌به‌خط

پلن اجرا، داستان کوئری است

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

چگونه بخوانیم

  1. از داخلی‌ترین گره به بیرون بخوانید؛ گره‌های تورفته‌تر زودتر اجرا می‌شوند و خروجی‌شان به والد می‌رود.
  2. cost دو عدد دارد: هزینه‌ی شروع (تا اولین ردیف) و هزینه‌ی کل. واحدش دلخواه است (یک صفحه‌ی ترتیبی = 1)، نه میلی‌ثانیه.
  3. rows تخمینی را با actual rows مقایسه کنید. در مثال بالا تخمین Hash Join پنج هزار و واقعیت شصت هزار است؛ ده برابر خطا. ریشه‌ی بیشتر پلن‌های بد همین‌جاست: آمار کهنه یا همبستگی بین ستون‌ها که برنامه‌ریز نمی‌داند.
  4. loops را ضرب کنید: actual time و rows برای هر بار اجرای گره‌اند. Index Scan با ‎time=0.05 و loops=200000 یعنی ده ثانیه.
  5. Buffers: hit یعنی از shared_buffers، read یعنی از سیستم‌عامل یا دیسک. هر صفحه 8KB است؛ read=412 حدود 3.3MB خواندن است.

علامت‌های خطر

آنچه می‌بینیدمعنیاقدام
Rows Removed by Filter بزرگردیف‌های زیادی خوانده و دور ریخته شدهایندکس یا partial index مناسب
Sort Method: external merge Diskwork_mem کافی نبودهافزایش work_mem برای همان نشست یا ایندکس برای ORDER BY
Nested Loop با loops عظیمتخمین سطر داخلی خیلی کم بودهANALYZE، CREATE STATISTICS
Heap Fetches بالا در Index Only Scanvisibility 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 خالی بود)؛ این گره را در تحلیل زمان نادیده بگیرید.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.