PostgreSQL: ضبط أداء PostgreSQL عمليًا

آخر تحديث: 2026-08-26

1. ما ستتعلمه


2. القصة

تولى Charlie نظام تجارة إلكترونية كانت صفحته الرئيسية تحمل في 8 ثوانٍ وكان تقريره الشهري ينتهي وقته. حلل كل استعلام بـ EXPLAIN ANALYZE ووجد ثلاث مشكلات: (1) جدول orders يفتقر إلى فهرس، مما تسبب في Seq Scan؛ (2) work_mem كان 4MB فقط، لذا انسكبت الترتيبات المعقدة إلى القرص؛ (3) autovacuum لم يستطع مواكبة معدل الكتابة، وكان الجدول منتفخًا بشدة. بعد إصلاح كل واحدة — إضافة فهرس بحيث تستخدم الاستعلامات Index Scan، ورفع work_mem إلى 64MB بحيث تبقى الترتيبات في الذاكرة، ومضاعفة تردد autovacuum للتحكم في الانتفاخ —▶�حسن الأداء العام 10 أضعاف وحملت الصفحة الرئيسية في 0.8 ثانية.


3. المفهوم: EXPLAIN ANALYZE بعمق

(1) معاملات خطة التنفيذ الأساسية

المعامل المعنى جيد لـ
Seq Scan مسح تسلسلي كامل للجدول جداول صغيرة، لا فهرس قابل للاستخدام، صفوف كثيرة معادة
Index Scan مسح فهرس B-Tree استعلام عالي الانتقائية (يعيد < 5% من الصفوف)
Bitmap Heap Scan مسح كومة نقطي انتقائية متوسطة (5%-15%)؛ يجمع TIDs ثم يجلب الصفوف
Bitmap Index Scan مسح فهرس نقطي يقترن مع Bitmap Heap Scan
Nest Loop ربط حلقة متداخلة جدول خارجي صغير + جدول داخلي مفهرس
Hash Join ربط تجزئة ربط مساواة، الجدول الداخلي يناسب الذاكرة
Merge Join ربط دمج كلا الجدولين مرتبين، ربط مساواة

(2) حقول مخرجات EXPLAIN الرئيسية

SQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 1001;
TEXT 📖 للعرض فقط
Index Scan using idx_orders_user_id on orders  (cost=0.42..8.44 rows=1 width=72) (actual time=0.015..0.016 rows=1 loops=1)
  Index Cond: (user_id = 1001)
  Buffers: shared hit=4
Planning Time: 0.085 ms
Execution Time: 0.032 ms
الحقل المعنى
cost=X..Y تكلفة البدء .. التكلفة الإجمالية (مقدرة)
rows=N عدد الصفوف المقدر إعادتها
actual time الوقت الفعلي المستغرق (مللي ثانية)
rows (actual) عدد الصفوف الفعلي المعاد
loops عدد مرات التنفيذ
Buffers: shared hit عدد إصابات المخزن المؤقت المشترك
Planning Time وقت التخطيط
Execution Time وقت التنفيذ

(3) تدفق قراءة خطة التنفيذ

100%
flowchart TD
    A["مخرجات EXPLAIN ANALYZE"] --> B{"نوع العقدة العليا؟"}
    B -->|"Seq Scan"| C{"الصفوف مقابل المقدر؟"}
    C -->|"التقدير غير دقيق"| D["شغّل ANALYZE<br/>حدث الإحصائيات"]
    C -->|"التقدير جيد"| E{"انتقائية المرشح؟"}
    E -->|"منخفضة (< 5%)"| F["أضف فهرس على عمود المرشح"]
    E -->|"مرتفعة (> 15%)"| G["Seq Scan جيد"]
    B -->|"Index Scan"| H["✅ جيد للانتقائية المنخفضة"]
    B -->|"Hash Join"| I{"انسكاب جدول التجزئة؟"}
    I -->|"نعم (work_mem منخفض)"| J["زد work_mem"]
    I -->|"لا"| K["✅ جيد"]
    B -->|"Nest Loop"| L{"الصفوف الخارجية × التكلفة الداخلية؟"}
    L -->|"مرتفع جدًا"| M["فكر في Hash Join<br/>أو أضف فهرس داخلي"]
    L -->|"معقول"| N["✅ جيد"]

4. العملية: مقارنة أنواع المسح

(1) ▶ مثال

SQL
-- لا فهرس على status، المخطط يختار Seq Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT COUNT(*) FROM orders WHERE status = 'pending';
TEXT 📖 للعرض فقط
Aggregate (actual time=45.123..45.124 rows=1 loops=1)
  -> Seq Scan on orders (actual time=0.012..42.890 rows=50000 loops=1)
       Filter: (status = 'pending'::text)
       Rows Removed by Filter: 950000

(2) ▶ مثال

SQL
CREATE INDEX idx_orders_user_id ON orders (user_id);

-- انتقائية عالية: المخطط يستخدم Index Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id = 1001;
TEXT 📖 للعرض فقط
Index Scan using idx_orders_user_id on orders (actual time=0.015..0.018 rows=3 loops=1)
  Index Cond: (user_id = 1001)

(3) ▶ مثال

SQL
-- انتقائية معتدلة: المخطط يختار Bitmap
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id BETWEEN 1000 AND 1100;
TEXT 📖 للعرض فقط
Bitmap Heap Scan on orders (actual time=0.523..2.145 rows=523 loops=1)
  Recheck Cond: (user_id >= 1000 AND user_id <= 1100)
  -> Bitmap Index Scan on idx_orders_user_id (actual time=0.412..0.412 rows=523 loops=1)
       Index Cond: (user_id >= 1000 AND user_id <= 1100)
نوع المسح الانتقائية نمط I/O الأفضل عندما
Seq Scan جدول كامل أو > 15% قراءة تسلسلية جدول صغير / نتيجة كبيرة
Index Scan < 5% قراءة عشوائية بحث دقيق
Bitmap Scan 5%-15% عشوائي ثم تسلسلي نطاق + ترتيب

(4) ▶ مثال

SQL
-- جدول خارجي صغير + داخلي مفهرس = Nest Loop
EXPLAIN (COSTS OFF)
SELECT o.id, u.name
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.id = 1001;

-- خارجي كبير + داخلي غير مرتب = Hash Join
EXPLAIN (COSTS OFF)
SELECT o.id, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date > '2024-01-01';

Output:

TEXT 📖 للعرض فقط
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(5) ▶ مثال

SQL
-- استعلام بطيء: Seq Scan على جدو�� كبير
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE created_at > now() - interval '7 days';
-- Seq Scan, cost=0.00..15432.00, actual time=120ms

-- أضف فهرسًا
CREATE INDEX idx_orders_created_at ON orders (created_at);

-- أعد التحليل لإحصائيات دقيقة
ANALYZE orders;

-- نفس الاستعلام الآن يستخدم Index Scan
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE created_at > now() - interval '7 days';
-- Index Scan, cost=0.42..890.00, actual time=2ms

Output:

TEXT 📖 للعرض فقط
CREATE TABLE

5. المفهوم: pg_stat_statements الاستعلامات البطيئة

(1) التمكين والتكوين

BASH
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.max = 10000
SQL
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

(2) تفسير المقاييس الرئيسية

العمود المعنى اتجاه التحسين
total_exec_time إجمالي وقت التنفيذ حسن الاستعلامات الأكثر استهلاكًا للوقت
mean_exec_time متوسط وقت التنفيذ استعلام بطيء واحد
calls عدد الاستدعاءات أولوية الاستعلامات عالية التردد
rows إجمالي الصفوف المعادة تحقق مما إذا تم إعادة صفوف كثيرة جدًا
shared_blks_hit إصابات المخزن المؤقت معدل إصابة منخفض → زد shared_buffers
shared_blks_read قراءات القرص قراءات قرص عالية → أضف فهرس / زد الذاكرة المؤقتة

(6) ▶ مثال

SQL
-- أفضل 10 حسب الوقت الإجمالي
SELECT left(query, 80) AS query_preview,
       calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 1) AS avg_ms,
       rows,
       round((100.0 * shared_blks_hit /
           nullif(shared_blks_hit + shared_blks_read, 0))::numeric, 1) AS cache_hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Output:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

(7) ▶ مثال

SQL
-- الاستعلامات ذات أكبر عدد قراءات قرص
SELECT left(query, 80) AS query_preview,
       shared_blks_read AS disk_reads,
       shared_blks_hit  AS cache_hits,
       calls,
       round(mean_exec_time::numeric, 1) AS avg_ms
FROM pg_stat_statements
WHERE shared_blks_read > 1000
ORDER BY shared_blks_read DESC
LIMIT 10;

Output:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

6. المفهوم: تجمع الاتصالات وضبط التكوين

(1) تجمع اتصالات PgBouncer

الوضع الوصف الأنسب لـ
Session pooling الاتصال مرتبط بالعميل يحتاج متغيرات جلسة / جداول مؤقتة
Transaction pooling يعاد الاتصال بعد المعاملة معظم تطبيقات الويب
Statement pooling يعاد الاتصال بعد العبارة استعلامات بسيطة بدون معاملات
BASH
# pgbouncer.ini
[databases]
shop_db = host=127.0.0.1 port=5432 dbname=shop

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
المعامل موصى به الوصف
max_client_conn 500-1000 الحد الأقصى لاتصالات العميل
default_pool_size 20-50 حجم التجمع لكل قاعدة بيانات/مستخدم
reserve_pool_size 5-10 تجمع احتياطي لحركة الاندفاع
pool_mode transaction موصى به لتطبيقات الويب

(2) معلمات PostgreSQL الأساسية

BASH
# ضبط postgresql.conf لخادم 16GB RAM
shared_buffers = 4GB           # 25% من RAM
work_mem = 64MB                # ذاكرة لكل عملية ترتيب
effective_cache_size = 12GB    # 75% من RAM
maintenance_work_mem = 1GB     # لـ VACUUM, CREATE INDEX
effective_io_concurrency = 200 # SSD; 2 لـ HDD
random_page_cost = 1.1         # SSD; 4.0 لـ HDD
المعامل الافتراضي موصى به (16GB RAM) الوصف
shared_buffers 128MB 25% RAM مخازن مؤقتة مشتركة
work_mem 4MB 32-128MB ذاكرة الترتيب/التجزئة
effective_cache_size 4GB 75% RAM تقدير ذاكرة التخزين المؤقت للمخطط
maintenance_work_mem 64MB 512MB-1GB ذاكرة عمليا�� الصيانة
max_parallel_workers 8 نوى CPU عمليات العامل المتوازية

(8) ▶ مثال

SQL
-- تحقق من الإعدادات الحالية
SHOW shared_buffers;
SHOW work_mem;
SHOW effective_cache_size;

-- تغيير في وقت التشغيل (لا حاجة لإعادة تشغيل للبعض)
ALTER SYSTEM SET work_mem = '64MB';
SELECT pg_reload_conf();

-- المعلمات التي يجب إعادة التشغيل لها
SELECT name, setting, boot_val, context
FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem', 'effective_cache_size');

Output:

TEXT 📖 للعرض فقط
ALTER TABLE

7. المفهوم: مبدأ وضبط autovacuum

(1) لماذا نحتاج autovacuum

آلية MVCC في PostgreSQL: UPDATE/DELETE تنتج صفوفًا ميتة، تحتاج إلى VACUUM لاستعادة المساحة — وإلا تنتفخ الجداول وتبطئ الاستعلامات.

SQL
-- تحقق من نسبة الصفوف الميتة
SELECT relname,
       n_live_tup,
       n_dead_tup,
       round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
       last_vacuum,
       last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

(2) معلمات autovacuum الرئيسية

المعامل الافتراضي اقتراح الضبط
autovacuum on يجب تمكينه
autovacuum_vacuum_threshold 50 عدد الصفوف الميتة الأساسي لتشغيل vacuum
autovacuum_vacuum_scale_factor 0.2 تشغيل vacuum عند 20% من الصفوف كصفوف ميتة
autovacuum_analyze_scale_factor 0.1 تشغيل analyze عند 10% تغيير
autovacuum_vacuum_cost_delay 2ms تأخير تخفيف vacuum
autovacuum_vacuum_cost_limit 200 حد I/O لـ vacuum لكل جولة

(9) ▶ مثال

SQL
-- لجداول الكتابة العالية، خفض عامل المقياس لـ vacuum أكثر تواترًا
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor = 0.05,
    autovacuum_analyze_scale_factor = 0.02,
    autovacuum_vacuum_cost_delay = '1ms',
    autovacuum_vacuum_cost_limit = 1000
);

-- للطلبات بـ 10M صف، يشغل vacuum عند:
-- 10M × 0.05 = 500K صف ميت (مقابل 2M الافتراضي)

Output:

TEXT 📖 للعرض فقط
-- SQL statement executed successfully

(10) ▶ مثال

SQL
-- PG 12+: تتبع تقدم vacuum
SELECT pid,
       relid::regclass AS table_name,
       phase,
       heap_blks_total,
       heap_blks_scanned,
       heap_blks_vacuumed,
       index_vacuum_count
FROM pg_stat_progress_vacuum;

Output:

TEXT 📖 للعرض فقط
 count 
-------
     5
(1 row)

8. العملية: الاستعلام المتوازي والمراقبة

(11) ▶ مثال

SQL
-- تمكين الاستعلام المتوازي (PG 10+)
SET max_parallel_workers_per_gather = 4;
SET max_parallel_workers = 8;
SET parallel_tuple_cost = 0.001;
SET min_parallel_table_scan_size = '8MB';

-- التحقق من الخطة المتوازية
EXPLAIN (ANALYZE, COSTS OFF)
SELECT COUNT(*), category
FROM products
GROUP BY category;
TEXT 📖 للعرض فقط
Finalize Aggregate (actual time=12.3..12.4 rows=5 loops=1)
  -> Gather (actual time=12.1..12.3 rows=15 loops=1)
       Workers Planned: 2
       Workers Launched: 2
       -> Partial Aggregate (actual time=8.5..8.6 rows=5 loops=3)
            -> Parallel Seq Scan on products (actual time=0.2..6.1 rows=33333 loops=3)

(12) ▶ مثال

SQL
SELECT relname AS table_name,
       seq_scan,
       seq_tup_read,
       idx_scan,
       idx_tup_fetch,
       round(100.0 * idx_scan / nullif(idx_scan + seq_scan, 0), 2) AS idx_scan_pct,
       n_live_tup,
       last_vacuum,
       last_autovacuum,
       last_analyze,
       last_autoanalyze
FROM pg_stat_user_tables
ORDER BY seq_tup_read DESC;

Output:

TEXT 📖 للعرض فقط
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
مقياس المراقبة حد التنبيه الإجراء
seq_tup_read >> idx_tup_fetch seq_scan مرتفع أضف فهرسًا
dead_pct > 20% انتفاخ شديد اضبط autovacuum
cache_hit_pct < 95% مخزن مؤقت غير كافٍ زد shared_buffers
last_autovacuum > 7 أيام vacuum غير منتظم اخفض scale_factor

(13) ▶ مثال

SQL
-- نمط مضاد 1: شرط OR يمنع استخدام الفهرس
-- سيء
SELECT * FROM orders WHERE user_id = 1001 OR status = 'pending';
-- جيد: استخدم UNION ALL
SELECT * FROM orders WHERE user_id = 1001
UNION ALL
SELECT * FROM orders WHERE status = 'pending' AND user_id != 1001;

-- نمط مضاد 2: دالة تلف عمودًا مفهرسًا
-- سيء
SELECT * FROM orders WHERE lower(status) = 'pending';
-- جيد
SELECT * FROM orders WHERE status = lower('PENDING');

-- نمط مضاد 3: LIKE بحرف بدل بادئ
-- سيء
SELECT * FROM products WHERE name LIKE '%phone%';
-- جيد: pg_trgm أو بحث نصي كامل
SELECT * FROM products WHERE name % 'phone';

Output:

TEXT 📖 للعرض فقط
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. مثال شامل

SQL
-- سير عمل تحسين الأداء الكامل لنظام التجارة الإلكترونية لـ Charlie

-- الخطوة 1: تحديد الاستعلامات البطيئة
SELECT left(query, 60) AS q, calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 1) AS avg_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 5;

-- الخطوة 2: تحليل أسوأ استعلام
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total_amount, u.name, p.name AS product_name
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31'
  AND o.status = 'completed';

-- الخطوة 3: إضافة الفهارس المفقودة
CREATE INDEX idx_orders_date_status ON orders (order_date, status);
CREATE INDEX idx_order_items_order_id ON order_items (order_id);
CREATE INDEX idx_order_items_product_id ON order_items (product_id);

-- الخطوة 4: تحديث الإحصائيات
ANALYZE orders;
ANALYZE order_items;
ANALYZE products;

-- الخطوة 5: ضبط autovacuum لجداول الكتابة العالية
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor = 0.05,
    autovacuum_analyze_scale_factor = 0.02,
    autovacuum_vacuum_cost_limit = 1000
);

-- الخطوة 6: زيادة work_mem للترتيبات المعقدة
ALTER SYSTEM SET work_mem = '64MB';
ALTER SYSTEM SET effective_cache_size = '12GB';
SELECT pg_reload_conf();

-- الخطوة 7: التحقق من التحسن
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total_amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31'
  AND o.status = 'completed';

-- الخطوة 8: مراقبة صحة الجدول
SELECT relname,
       n_live_tup, n_dead_tup,
       round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
       last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'order_items', 'products')
ORDER BY n_dead_tup DESC;

❓ أسئلة شائعة

س ما الفرق بين EXPLAIN و EXPLAIN ANALYZE؟
ج EXPLAIN يعرض فقط الخطة المقدرة ولا ينفذ الاستعلام؛ EXPLAIN ANALYZE يشغله فعليًا ويعيد التوقيت الحقيقي وعدد الصفوف. لاحظ أن ANALYZE ينفذ INSERT/UPDATE/DELETE، لذا عند تحليل DML، لفه في معاملة و ROLLBACK بعده.
س قيمة cost كبيرة لكنها سريعة فعليًا — لماذا؟
ج cost هي قيمة وحدة مقدرة من قبل المخطط (ليست مللي ثانية)، تتأثر بالإحصائيات. إذا كا��ت إحصائيات ANALYZE قديمة، قد تكون cost غير دقيقة. شغّل ANALYZE بانتظام للحفاظ على الإحصائيات حديثة.
س كم يجب تعيين shared_buffers؟
ج عمومًا 25% من RAM الفعلي، لا تتجاوز 40%. على Linux، فوق 40% قد تتعارض مع ذاكرة التخزين المؤقت لنظام التشغيل. على Windows، حافظ عليها ضمن 512MB-1GB.
س هل يمكن لـ work_mem كبير أن يسبب OOM؟
ج work_mem هو حد الذاكرة لكل عملية ترتيب/تجزئة، وقد يكون لاستعلام معقد واحد عدة عمليات من هذا القبيل تعمل معًا. تعيينها كبيرة جدًا يمكن أن يسبب OOM بالفعل. موصى به 32-128MB، مضبوطة بمراقبة الاستخدام الفعلي.
س ماذا لو لم يستطع autovacuum مواكبة معدل الكتابة؟
ج اخفض scale_factor (مثلاً 0.02)، ارفع cost_limit (مثلاً 2000)، قلل cost_delay (مثلاً 1ms)، وعيّن هذه لكل جدول كبير. في الحالات القصوى، شغّل VACUUM يدويًا بالتوازي.
س متى يسري الاستعلام المتوازي؟
ج عندما يتجاوز حجم الجدول min_parallel_table_scan_size (افتراضي 8MB)، وتتجاوز تكلفة الاستعلام عتبة بدء التوازي، ويكون لدى max_parallel_workers سعة حرة. الجداول الصغيرة أو الاستعلامات البسيطة لن تتوازى.
س هل Seq Scan دائمًا أسوأ من Index Scan؟
ج ليس بالضرورة. عندما يجب إعادة معظم صفوف الجدول، تكون قراءات Seq Scan التسلسلية أكثر كفاءة من قراءات Index Scan العشوائية. مخطط PG يختار تلقائيًا الخيار الأقل تكلفة.

📖 ملخص


📝 تمارين

  1. ⭐ لجدول اختبار بـ مليون صف، لاحظ فرق actual-time بين Seq Scan و Index Scan باستخدام EXPLAIN ANALYZE، وسجل الفجوة بين تقدير cost والوقت الفعلي.

  2. ⭐⭐ كون pg_stat_statements، اجمع إحصائيات استعلام 24 ساعة، واكتب تقرير استعلامات بطيئة: اسرد أفضل 5 حسب كل من total_exec_time و mean_exec_time و shared_blks_read، وقدم اقتراحات تحسين.

  3. ⭐⭐⭐ حاكِ سيناريو Charlie: أنشئ 5 جداول (orders/order_items/users/products/payments)، أدرج بيانات اختبار، استخدم EXPLAIN ANALYZE لإيجاد 3 مشكلات أداء (فهرس مفقود، work_mem غير كافٍ، autovacuum متأخر)، أصلح كل واحدة، ثم استخدم EXPLAIN ANALYZE للتحقق من مضاعف تحسن الأداء لكل إصلاح.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%