PostgreSQL: ضبط أداء PostgreSQL عمليًا
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- قراءة خطط تنفيذ EXPLAIN ANALYZE بعمق
- فهم Seq Scan / Index Scan / Bitmap Scan / Nest Loop / Hash Join / Merge Join
- استخدام pg_stat_statements لتحديد الاستعلامات البطيئة
- تكوين تجمع اتصالات (PgBouncer)
- ضبط المعلمات الأساسية: shared_buffers, work_mem, effective_cache_size
- فهم وضبط autovacuum
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 الرئيسية
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 1001;
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) تدفق قراءة خطة التنفيذ
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) ▶ مثال
-- لا فهرس على status، المخطط يختار Seq Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT COUNT(*) FROM orders WHERE status = 'pending';
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) ▶ مثال
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- انتقائية عالية: المخطط يستخدم Index Scan
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id = 1001;
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) ▶ مثال
-- انتقائية معتدلة: المخطط يختار Bitmap
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM orders WHERE user_id BETWEEN 1000 AND 1100;
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) ▶ مثال
-- جدول خارجي صغير + داخلي مفهرس = 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:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(5) ▶ مثال
-- استعلام بطيء: 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:
CREATE TABLE
5. المفهوم: pg_stat_statements الاستعلامات البطيئة
(1) التمكين والتكوين
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.max = 10000
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) ▶ مثال
-- أفضل 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:
result
----------
42.50
(1 row)
(7) ▶ مثال
-- الاستعلامات ذات أكبر عدد قراءات قرص
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:
result
----------
42.50
(1 row)
6. المفهوم: تجمع الاتصالات وضبط التكوين
(1) تجمع اتصالات PgBouncer
| الوضع | الوصف | الأنسب لـ |
|---|---|---|
| Session pooling | الاتصال مرتبط بالعميل | يحتاج متغيرات جلسة / جداول مؤقتة |
| Transaction pooling | يعاد الاتصال بعد المعاملة | معظم تطبيقات الويب |
| Statement pooling | يعاد الاتصال بعد العبارة | استعلامات بسيطة بدون معاملات |
# 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 الأساسية
# ضبط 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) ▶ مثال
-- تحقق من الإعدادات الحالية
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:
ALTER TABLE
7. المفهوم: مبدأ وضبط autovacuum
(1) لماذا نحتاج autovacuum
آلية MVCC في PostgreSQL: UPDATE/DELETE تنتج صفوفًا ميتة، تحتاج إلى VACUUM لاستعادة المساحة — وإلا تنتفخ الجداول وتبطئ الاستعلامات.
-- تحقق من نسبة الصفوف الميتة
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) ▶ مثال
-- لجداول الكتابة العالية، خفض عامل المقياس لـ 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:
-- SQL statement executed successfully
(10) ▶ مثال
-- 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:
count
-------
5
(1 row)
8. العملية: الاستعلام المتوازي والمراقبة
(11) ▶ مثال
-- تمكين الاستعلام المتوازي (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;
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) ▶ مثال
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:
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) ▶ مثال
-- نمط مضاد 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:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
9. مثال شامل
-- سير عمل تحسين الأداء الكامل لنظام التجارة الإلكترونية لـ 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 ANALYZE هي الأداة الأولى لضبط الأداء؛ قراءة المعاملات والتكلفة هي المفتاح
- Seq Scan / Index Scan / Bitmap Scan تناسب سيناريوهات انتقائية مختلفة
- pg_stat_statements يحدد الاستعلامات البطيئة: راقب total_time, calls, cache_hit
- وضع transaction في PgBouncer هو تجمع الاتصالات القياسي لتطبيقات الويب
- shared_buffers 25% RAM، work_mem حسب تعقيد الاستعلام، effective_cache_size 75% RAM
- Autovacuum على جداول الكتابة العالية يحتاج scale_factor أقل و cost_limit أعلى
- تجنب الأنماط المضادة الشائعة: شروط OR، أعمدة الفهرس الملفوفة بدوال، LIKE بأحرف بدل بادئة
📝 تمارين
-
⭐ لجدول اختبار بـ مليون صف، لاحظ فرق actual-time بين Seq Scan و Index Scan باستخدام EXPLAIN ANALYZE، وسجل الفجوة بين تقدير cost والوقت الفعلي.
-
⭐⭐ كون pg_stat_statements، اجمع إحصائيات استعلام 24 ساعة، واكتب تقرير استعلامات بطيئة: اسرد أفضل 5 حسب كل من total_exec_time و mean_exec_time و shared_blks_read، وقدم اقتراحات تحسين.
-
⭐⭐⭐ حاكِ سيناريو Charlie: أنشئ 5 جداول (orders/order_items/users/products/payments)، أدرج بيانات اختبار، استخدم EXPLAIN ANALYZE لإيجاد 3 مشكلات أداء (فهرس مفقود، work_mem غير كافٍ، autovacuum متأخر)، أصلح كل واحدة، ثم استخدم EXPLAIN ANALYZE للتحقق من مضاعف تحسن الأداء لكل إصلاح.