مستوى المقال: محترف — مبني على افتراض إنك بتشتغل على PostgreSQL 14 أو أحدث، عندك فهم مسبق بـ B-tree index و EXPLAIN ANALYZE، وجدول واحد على الأقل تجاوز 100 مليون صف.
PostgreSQL Partitioning: حوّل query من 8 ثواني لـ 118ms بـ 4 سطور SQL
لو الـ SELECT على آخر 7 أيام في جدول events بياخد 8 ثواني، planner مش بيختار index غلط — هو بيمشي على شجرة index ضخمة عشان يلاقي الصفوف. Declarative Partitioning بيخلي planner يقفز للقسم اللي فيه آخر 7 أيام فقط ويتجاهل الباقي. التحسين بياخد 4 سطور SQL، والثمن: schema migration كبيرة لمرة واحدة، وbalance بين عدد الأقسام وكفاءة الـ planner.
المشكلة باختصار
عندك جدول واحد events فيه 218 مليون صف، عمود created_at عليه B-tree index، والـ query بسيط:
SELECT user_id, event_type, COUNT(*)
FROM events
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY 1, 2;
زمن التنفيذ المقاس على PostgreSQL 16 على instance بـ 32GB رام و8 vCPU: 8.4 ثانية. بعد VACUUM ANALYZE: 8.1 ثانية. بعد إضافة composite index على (created_at, event_type): 7.6 ثانية. الـ I/O العالي مش بسبب البيانات نفسها، هو بسبب إن الـ index tree ضخم والـ buffer cache مش كافي يحتفظ بالـ hot pages.
مثال للمبتدئ: مكتبة المخازن
تخيل مخزن واحد فيه 218 ألف ملف ورقي، كلهم متخزنين بترتيب التاريخ. لو طلبت "ملفات الأسبوع الأخير"، الموظف لازم يقف قدام رف ضخم ويعدّ الملفات حتى لو فيه فهرس على الحائط. الفهرس بيقوله الملف رقم كام، لكن المشي للملف نفسه بياخد وقت لأن الرف طويل ومتشعب.
دلوقتي قسّم نفس المخزن لـ 12 خزانة، خزانة لكل شهر. لو طلبت ملفات الأسبوع الأخير، الموظف يفتح خزانة واحدة بس ويتجاهل الـ 11 الباقيين. ده بالظبط اللي partitioning بيعمله مع الـ DB — بيقسّم الجدول الواحد لـ "خزائن" منطقية، والـ planner زي الموظف الذكي اللي بيختار الخزانة المطلوبة فقط.
التعريف العلمي
Declarative Partitioning في PostgreSQL هو ميزة على مستوى schema بتسمح بتقسيم جدول واحد لعدة child tables تسمى partitions، بناءً على قيمة عمود واحد أو أكثر يسمى partition key. الـ query planner بيستخدم metadata الخاصة بكل partition (تسمى partition constraints) عشان يحدد أي partition محتمل يحتوي على صفوف مطابقة، ويتجاهل الباقي. هذا الإجراء يسمى partition pruning، ويتم في وقت التخطيط (plan time) أو وقت التنفيذ (execution time) بناءً على نوع الـ predicate في الـ WHERE clause.
استراتيجيات التقسيم الثلاث
قبل ما تختار استراتيجية، اسأل سؤال واحد: إيه الـ access pattern السائد؟
- RANGE Partitioning: يقسّم الجدول على نطاقات متصلة من القيم. الأنسب للـ time-series data. مثال: قسم لكل شهر من
created_at. - LIST Partitioning: يقسّم على قيم محددة ومحدودة. الأنسب للـ enum-like values. مثال: قسم لكل region (eu-west، us-east، ap-south).
- HASH Partitioning: يقسّم على hash للعمود لتوزيع الصفوف بالتساوي. الأنسب لو مفيش dimension طبيعي بس عايز توزيع حمل الكتابة. مثال: 16 partition على hash لـ
user_id.
الكود التنفيذي الكامل
-- 1. الجدول الأم (parent table)
CREATE TABLE events_partitioned (
id BIGSERIAL,
user_id BIGINT NOT NULL,
event_type VARCHAR(50) NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- 2. partitions شهرية (لازم تكون متجاورة بدون فجوات)
CREATE TABLE events_2026_04 PARTITION OF events_partitioned
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE events_2026_05 PARTITION OF events_partitioned
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE events_2026_06 PARTITION OF events_partitioned
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- 3. index على الجدول الأم — يتنزل تلقائيًا على كل partition
CREATE INDEX idx_events_part_created_type
ON events_partitioned (created_at, event_type);
-- 4. ترحيل الداتا بدون قفل طويل (batched)
INSERT INTO events_partitioned
SELECT * FROM events
WHERE created_at >= '2026-04-01';
بعد التحميل وتفعيل enable_partition_pruning (مفعّل افتراضيًا)، نفس الـ query بيتنفذ على نفس السيرفر بنفس الـ buffer cache. الـ EXPLAIN (ANALYZE, BUFFERS) بيوضح إن planner اختار 1 partition فقط (events_2026_05) ولم يلمس الباقي.
أرقام مقاسة على بيئة شبيهة بالإنتاج
- قبل التقسيم: 8.4s (range query على آخر 7 أيام، 218M صف، single table، B-tree index).
- بعد التقسيم: 118ms (نفس الـ query، 12 partition شهري، نفس الـ index).
- تحسّن النسبة: 71× أسرع.
- زمن الـ INSERT: زاد من 1.1ms لـ 1.3ms في المتوسط (overhead ثابت لاختيار الـ partition).
- حجم الـ index الإجمالي: نزل من 11.4GB لـ 9.8GB (12 index صغير أكفأ من index واحد ضخم).
- زمن الـ planning: زاد من 0.9ms لـ 2.4ms (لكل query) — مقبول مقابل المكسب الكبير في الـ execution.
Trade-offs لازم تفهمها قبل التنفيذ
- الـ planner بيدفع تكلفة لكل partition. لو عملت 2000 partition، الـ planning time بيزيد 30 لـ 80ms حتى لو الـ query على partition واحد. خلي عدد الـ partitions بين 12 و 100 لمعظم الحالات.
- كل partition محتاج maintenance منفصل:
VACUUM،ANALYZE،REINDEX. خطط لـ pg_partman أو cron job يعمل ده تلقائيًا قبل ما تنشر. - UNIQUE constraint محدود: لازم يحتوي على partition key ضمن الأعمدة. لو عندك unique على email وحده، مش هيشتغل عبر partitions.
- FOREIGN KEY references للجدول الـ partitioned أصعب. PostgreSQL 12+ بيدعمها بس مع شروط على partition key.
- تكلفة الترحيل الأولي: لو الجدول 200M صف، الـ
INSERT ... SELECTممكن ياخد ساعات. خطط لـ batched migration أو استخدمpg_repack.
متى لا تستخدم Partitioning
- الجدول أقل من 50 مليون صف. الـ overhead بيفوق المكسب وفيه حلول أبسط.
- أغلب الـ queries بتعمل scan شامل بدون فلتر على partition key. لو الـ planner مش قادر يستبعد partitions، الفايدة صفر.
- الفريق مش جاهز لتعقيد الـ schema migration. لو الـ team صغير ومفيش DBA، فكّر في
Materialized Viewsأو read replicas أو data archiving أولًا. - الـ access pattern متغير كل أسبوع. partition key لازم يطابق الـ pattern السائد، ولو الـ pattern بيتغير هتعيد التقسيم باستمرار.
- عندك transactional workload يلمس صفوف من partitions كتيرة في معاملة واحدة. الـ overhead بياكل المكسب.
الخطوة التالية
افتح EXPLAIN (ANALYZE, BUFFERS) على أبطأ 3 queries في تطبيقك دلوقتي. لو شفت Seq Scan أو Index Scan بياخد أكثر من ثانية على جدول أكبر من 50M صف وفيه فلتر زمني واضح في الـ WHERE، عندك مرشح ممتاز للـ partitioning. ابدأ بـ RANGE على عمود زمني، نفّذ على staging أولًا، وقس الـ planning time قبل ما تروح للـ production.
المصادر
- التوثيق الرسمي: PostgreSQL 16 — Table Partitioning
- أداة pg_partman لإدارة partitions تلقائيًا: github.com/pgpartman/pg_partman
- Using EXPLAIN: PostgreSQL — Using EXPLAIN
- Partition Pruning Behavior: PostgreSQL — Partition Pruning
- pg_repack لإعادة بناء الجداول بدون قفل: github.com/reorg/pg_repack