التجزئة والصيانة: Partitioning و ANALYZE
حين يصبح الجدول ضخمًا: قسّمه، وحافظ على إحصاءاته، ونظّف صفوفه الميتة. ⏱ نحو 40 دقيقة.
- تقسّم جدولًا كبيرًا بالتجزئة
RANGEأوLIST. - تقرأ تقليم الأجزاء في خطة التنفيذ.
- تؤرشف جزءًا بـ
DETACH PARTITION. - تشرح دور
VACUUMوANALYZEفي الأداء.
١التجزئة: جدول كبير مقسّم إلى أجزاء
جدول مبيعات يكبر سنة بعد سنة، وأغلب الاستعلامات عن السنة الحالية. التجزئة Partitioning تقسمه إلى جداول أصغر (أجزاء) بحسب قاعدة، ويبقى من الخارج جدولًا واحدًا:
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_2024 | 2662 |
| sales_2025 | 2338 |
أدخلنا في الجدول الأب sales، فوزّع PostgreSQL كل صف على جزء سنته تلقائيًّا. tableoid::regclass يُظهر اسم الجزء الذي يسكنه الصف.
| طريقة التقسيم | الصيغة | مثال |
|---|---|---|
| بالمدى RANGE | FOR VALUES FROM (…) TO (…) | سنة أو شهر |
| بالقائمة LIST | FOR VALUES IN ('الرياض', 'جدة') | المنطقة |
| بالتجزئة HASH | FOR VALUES WITH (MODULUS 4, REMAINDER 0) | توزيع متساوٍ بلا معنى |
٢التقليم: لا يقرأ إلا الجزء المطلوب
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';
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 لملايين الصفوف القديمة، نفصل الجزء كاملًا:
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; | يعيد كتابة الجدول ويقلص حجمه (يقفل الجدول: بحذر) |
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;
| stage | size |
|---|---|
| بعد الإدخال | 256 kB |
| بعد حذف 80% | 256 kB |
| بعد VACUUM FULL | 56 kB |
حذف 80% من الصفوف لم يصغّر الجدول: صارت صفوفًا ميتة تشغل مكانها. VACUUM العادي يجعل مكانها قابلًا لإعادة الاستخدام، و VACUUM FULL يعيد كتابة الجدول فيصغر فعلًا. على الخوادم الحقيقية تعمل autovacuum تلقائيًّا في الخلفية، لكن بعد حذف أو تحميل كبير نشغّل VACUUM ANALYZE يدويًّا.
توقّع وتحقّق
ما فائدة تقليم الأجزاء؟
ماذا يجمع ANALYZE؟
أسرع طريقة لأرشفة سنة كاملة من جدول مجزّأ:
الصفوف الميتة تنتج عن:
الخلاصة
- التجزئة تقسم الجدول الكبير إلى أجزاء، ويبقى من الخارج جدولًا واحدًا.
- تقليم الأجزاء: الاستعلام الذي يذكر عمود التجزئة يقرأ الأجزاء المطابقة فقط.
- الأرشفة بـ
DETACH PARTITIONلحظية. VACUUMيستعيد مساحة الصفوف الميتة، و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;حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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;