طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 122 · برمجة قواعد البيانات
الدرس 14 · الوحدة 6: تحسين أداء جداول البيانات وإنشاء الفهارس

التجزئة والصيانة: Partitioning و ANALYZE

حين يصبح الجدول ضخمًا: قسّمه، وحافظ على إحصاءاته، ونظّف صفوفه الميتة. ⏱ نحو 40 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تقسّم جدولًا كبيرًا بالتجزئة RANGE أو LIST.
  • تقرأ تقليم الأجزاء في خطة التنفيذ.
  • تؤرشف جزءًا بـ DETACH PARTITION.
  • تشرح دور VACUUM و ANALYZE في الأداء.

١التجزئة: جدول كبير مقسّم إلى أجزاء

جدول مبيعات يكبر سنة بعد سنة، وأغلب الاستعلامات عن السنة الحالية. التجزئة Partitioning تقسمه إلى جداول أصغر (أجزاء) بحسب قاعدة، ويبقى من الخارج جدولًا واحدًا:

query.sql
CREATE TABLE sales (  id        int,  sold_on   date NOT NULL,  amount    numeric(10,2)) PARTITION BY RANGE (sold_on); CREATE TABLE sales_2024 PARTITION OF sales FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');CREATE TABLE sales_2025 PARTITION OF sales FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); INSERT INTO salesSELECT g, date '2024-01-01' + (g % 700), 10 + g % 90 FROM generate_series(1, 5000) g; SELECT tableoid::regclass AS الجزء, count(*) FROM sales GROUP BY 1 ORDER BY 1;
النتيجة · صفّان
الجزءcount
sales_20242662
sales_20252338

أدخلنا في الجدول الأب sales، فوزّع PostgreSQL كل صف على جزء سنته تلقائيًّا. tableoid::regclass يُظهر اسم الجزء الذي يسكنه الصف.

طريقة التقسيمالصيغةمثال
بالمدى RANGEFOR VALUES FROM (…) TO (…)سنة أو شهر
بالقائمة LISTFOR VALUES IN ('الرياض', 'جدة')المنطقة
بالتجزئة HASHFOR VALUES WITH (MODULUS 4, REMAINDER 0)توزيع متساوٍ بلا معنى

٢التقليم: لا يقرأ إلا الجزء المطلوب

query.sql
CREATE TABLE sales (  id        int,  sold_on   date NOT NULL,  amount    numeric(10,2)) PARTITION BY RANGE (sold_on); CREATE TABLE sales_2024 PARTITION OF sales FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');CREATE TABLE sales_2025 PARTITION OF sales FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); INSERT INTO salesSELECT g, date '2024-01-01' + (g % 700), 10 + g % 90 FROM generate_series(1, 5000) g; EXPLAIN SELECT sum(amount) FROM sales WHERE sold_on >= '2025-06-01';
خطة التنفيذ (EXPLAIN)
  • Aggregate (cost=40.22..40.23 rows=1 width=32)
  • -> Seq Scan on sales_2025 sales (cost=0.00..38.51 rows=680 width=16)
  • Filter: (sold_on >= '2025-06-01'::date)

الخطة تمر على sales_2025 وحده: تجاهل المحرك جزء 2024 لأن الشرط لا يطابقه. هذا تقليم الأجزاء Partition Pruning.

والأرشفة تصبح عملية لحظية: بدل DELETE لملايين الصفوف القديمة، نفصل الجزء كاملًا:

query.sql
CREATE TABLE sales (  id        int,  sold_on   date NOT NULL,  amount    numeric(10,2)) PARTITION BY RANGE (sold_on); CREATE TABLE sales_2024 PARTITION OF sales FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');CREATE TABLE sales_2025 PARTITION OF sales FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); INSERT INTO salesSELECT g, date '2024-01-01' + (g % 700), 10 + g % 90 FROM generate_series(1, 5000) g; ALTER TABLE sales DETACH PARTITION sales_2024;   -- صار جدولًا مستقلًا (أرشيف)SELECT count(*) AS في_sales FROM sales;
النتيجة · صف واحد
في_sales
2338

٣الصيانة: VACUUM و ANALYZE

في PostgreSQL، التعديل والحذف لا يمحوان الصف القديم فورًا؛ يبقى «صفًّا ميتًا» حتى تنظفه عملية VACUUM. ويحتاج المخطِّط إحصاءات حديثة عن توزيع القيم ليختار خطة جيدة، وهذه يجمعها ANALYZE:

الأمروظيفته
VACUUM t;يعيد استخدام مساحة الصفوف الميتة
ANALYZE t;يحدّث إحصاءات الأعمدة للمخطِّط
VACUUM ANALYZE t;الاثنان معًا
VACUUM FULL t;يعيد كتابة الجدول ويقلص حجمه (يقفل الجدول: بحذر)
query.sql
CREATE TABLE logs (id int, msg text);INSERT INTO logs SELECT g, 'حدث رقم ' || g FROM generate_series(1, 5000) g;CREATE TABLE sizes (stage text, size text); INSERT INTO sizes VALUES ('بعد الإدخال', pg_size_pretty(pg_relation_size('logs')));DELETE FROM logs WHERE id <= 4000;INSERT INTO sizes VALUES ('بعد حذف 80%', pg_size_pretty(pg_relation_size('logs')));VACUUM FULL logs;INSERT INTO sizes VALUES ('بعد VACUUM FULL', pg_size_pretty(pg_relation_size('logs'))); SELECT * FROM sizes;
النتيجة · 3 صفوف
stagesize
بعد الإدخال256 kB
بعد حذف 80%256 kB
بعد VACUUM FULL56 kB

حذف 80% من الصفوف لم يصغّر الجدول: صارت صفوفًا ميتة تشغل مكانها. VACUUM العادي يجعل مكانها قابلًا لإعادة الاستخدام، و VACUUM FULL يعيد كتابة الجدول فيصغر فعلًا. على الخوادم الحقيقية تعمل autovacuum تلقائيًّا في الخلفية، لكن بعد حذف أو تحميل كبير نشغّل VACUUM ANALYZE يدويًّا.

توقّع وتحقّق

ما فائدة تقليم الأجزاء؟

ماذا يجمع ANALYZE؟

أسرع طريقة لأرشفة سنة كاملة من جدول مجزّأ:

الصفوف الميتة تنتج عن:

الخلاصة

تحدٍّ برمجي: أجزاء الطلبات حسب المنطقة

متقدم

أنشئ جدول regional_orders مجزّأ بالقائمة حسب المدينة: جزء للمنطقة الوسطى (الرياض)، وجزء للغربية (جدة، مكة، المدينة)، وجزء افتراضي لبقية المدن. ثم انسخ إليه طلبات المتجر مع مدن عملائها.

المطلوب:
  • regional_orders (order_id int, city text NOT NULL, order_date date) مجزّأ بـ PARTITION BY LIST (city).
  • الأجزاء: ro_central و ro_west و ro_other (بـ DEFAULT).
  • املأه بـ INSERT … SELECT من orders و customers.
👀 اعرض الناتج المطلوب
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • كل الطلبات نُسخت (12)
  • كل جزء فيه طلبات منطقته
  • تجزئة بالقائمة
  • جزء افتراضي

💡 تلميح 1
الجزء بالقائمة: CREATE TABLE ro_central PARTITION OF regional_orders FOR VALUES IN ('الرياض');
💡 تلميح 2
الجزء الافتراضي: … PARTITION OF regional_orders DEFAULT;

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

solution.sql
CREATE TABLE regional_orders (  order_id   int,  city       text NOT NULL,  order_date date) PARTITION BY LIST (city); CREATE TABLE ro_central PARTITION OF regional_orders FOR VALUES IN ('الرياض');CREATE TABLE ro_west    PARTITION OF regional_orders FOR VALUES IN ('جدة', 'مكة', 'المدينة');CREATE TABLE ro_other   PARTITION OF regional_orders DEFAULT; INSERT INTO regional_ordersSELECT o.id, c.city, o.order_date FROM orders o JOIN customers c ON c.id = o.customer_id;
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس