PostgreSQL: جداول PostgreSQL المقسمة ووراثة الجداول

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

1. ما ستتعلمه


2. القصة

يكتسب جدول orders في منصة التجارة الإلكترونية لـ Alice 100 ألف صف جديد كل يوم. بعد ثلاث سنوات يتجاوز الجدول الواحد 100 مليون صف؛ حتى مع الفهارس، لا يزال استعلام شهري يمسح الجدول بأكمله. يقترح مسؤول قاعدة البيانات تقسيم RANGE شهري — استعلام آخر 30 يومًا يمسح حينها قسمًا واحدًا فقط بدلاً من 3 سنوات من الجدول بأكمله. بعد تشغيل التقسيم، انخفض استعلام التقرير الشهري من 12 ثانية إلى 0.8 ثانية، وانخفض I/O القرص بنسبة 95%.


3. المفهوم: نظرة عامة على التقسيم التصريحي

(1) لماذا نحتاج التقسيم

بمجرد أن يتجاوز الجدول الواحد عشرات الملايين من الصفوف، يصبح B-Tree للفهرس أعمق، ويستغرق VACUUM وقتًا أطول بكثير، وقد يختار مخطط الاستعلام خطة دون المستوى. يقسم التقسيم جدولًا كبيرًا منطقيًا إلى عدة جداول فيزيائية صغيرة، لكل منها فهرس وصيانة مستقلة خاصة به.

المقياس غير مقسم (100M صف) أقسام شهرية (36 قسمًا)
الصفوف لكل قسم 100M ~2.8M
عمق الفهرس 5-6 مستويات 3-4 مستويات
وقت VACUUM 30+ دقيقة < 2 دقيقة/قسم
I/O الاستعلام الشهري مسح جدول كامل مسح قسم واحد فقط

(2) تطور التقسيم في PostgreSQL

الإصدار الميزة
PG 9.x وراثة الجداول + تقسيم يدوي قائم على المشغلات
PG 10 تقسيم RANGE / LIST تصريحي
PG 11 تقسيم HASH، تحسين تقليم الأقسام، ترحيل UPDATE عبر الأقسام
PG 12 أقسام ATTACH/DETACH
PG 13 تحسين تقليم الأقسام متعددة المستويات
PG 14+ تحسينات أداء إضافية لتقليم الأقسام

(3) استراتيجيات التقسيم الأربع

الاستراتيجية حالة الاستخدام متطلب مفتاح التقسيم
RANGE السلاسل الزمنية، النطاقات الرقمية نوع قابل للترتيب
LIST فئات التعداد (منطقة، حالة) قيم متقطعة
HASH توزيع متساوٍ، بدون نطاق واضح أي نوع قابل للتجزئة
متعدد المستويات تركيبة RANGE + LIST/HASH تركيبة متعددة الأعمدة

4. العملية: تقسيم RANGE

(1) ▶ مثال

SQL
-- الجدول الأم: مفتاح التقسيم ف��ط
CREATE TABLE orders (
    id          BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id     BIGINT NOT NULL,
    order_date  DATE NOT NULL,
    total_amount NUMERIC(12,2),
    status      TEXT DEFAULT 'pending'
) PARTITION BY RANGE (order_date);

-- أقسام شهرية لعام 2024
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

CREATE TABLE orders_2024_03 PARTITION OF orders
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');

-- القسم الافتراضي يلتقط الصفوف خارج النطاق
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

Output:

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

(2) ▶ مثال

SQL
-- توليد أقسام شهرية لسنة معينة
CREATE OR REPLACE FUNCTION create_monthly_partitions(
    p_parent  REGCLASS,
    p_year    INT
) RETURNS VOID AS $$
DECLARE
    m      INT;
    p_name TEXT;
    s_date TEXT;
    e_date TEXT;
BEGIN
    FOR m IN 1..12 LOOP
        p_name := format('%s_%s_%02s', p_parent::text, p_year, m);
        s_date := format('%s-%02s-01', p_year, m);
        e_date := format('%s-%02s-01', p_year, m + 1);
        EXECUTE format(
            'CREATE TABLE IF NOT EXISTS %I PARTITION OF %s
             FOR VALUES FROM (%L) TO (%L)',
            p_name, p_parent::text, s_date, e_date
        );
    END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT create_monthly_partitions('orders', 2025);

Output:

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

(3) ▶ مثال

SQL
INSERT INTO orders (user_id, order_date, total_amount, status)
VALUES
    (1001, '2024-01-15', 299.99, 'completed'),
    (1002, '2024-02-20', 159.50, 'shipped'),
    (1003, '2024-03-10', 89.00,  'pending');

-- استعلام مع تقليم الأقسام
SELECT * FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-29';
TEXT 📖 للعرض فقط
  id  | user_id | order_date | total_amount | status
------+---------+------------+--------------+--------
 1002 |    1002 | 2024-02-20 |       159.50 | shipped
(1 row)

5. العملية: تقسيم LIST و HASH

(4) ▶ مثال

SQL
CREATE TABLE users_by_region (
    id       BIGINT GENERATED ALWAYS AS IDENTITY,
    name     TEXT NOT NULL,
    region   TEXT NOT NULL,
    email    TEXT
) PARTITION BY LIST (region);

CREATE TABLE users_north_america PARTITION OF users_by_region
    FOR VALUES IN ('US', 'CA', 'MX');

CREATE TABLE users_europe PARTITION OF users_by_region
    FOR VALUES IN ('UK', 'DE', 'FR', 'ES');

CREATE TABLE users_asia PARTITION OF users_by_region
    FOR VALUES IN ('CN', 'JP', 'KR', 'SG');

CREATE TABLE users_other PARTITION OF users_by_region DEFAULT;

Output:

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

(5) ▶ مثال

SQL
CREATE TABLE access_logs (
    id         BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id    BIGINT,
    action     TEXT,
    log_time   TIMESTAMPTZ DEFAULT now()
) PARTITION BY HASH (user_id);

-- إنشاء 8 أقسام تجزئة
CREATE TABLE access_logs_p0 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE access_logs_p1 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE access_logs_p2 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE access_logs_p3 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE access_logs_p4 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE access_logs_p5 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE access_logs_p6 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE access_logs_p7 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 7);

Output:

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

(6) ▶ مثال

SQL
CREATE TABLE order_details (
    id          BIGINT,
    order_id    BIGINT,
    order_date  DATE NOT NULL,
    region      TEXT NOT NULL,
    product_id  INT,
    quantity    INT
) PARTITION BY RANGE (order_date);

-- المستوى الأول: حسب الشهر
CREATE TABLE order_details_2024q1 PARTITION OF order_details
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01')
    PARTITION BY LIST (region);

-- المستوى الثاني: حسب المنطقة داخل كل نطاق شهر
CREATE TABLE order_details_2024q1_na PARTITION OF order_details_2024q1
    FOR VALUES IN ('US', 'CA', 'MX');

CREATE TABLE order_details_2024q1_eu PARTITION OF order_details_2024q1
    FOR VALUES IN ('UK', 'DE', 'FR');

Output:

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

6. المفهوم: تقليم الأقسام

(1) مبدأ التقليم

تقليم الأقسام يسمح لمخطط الاستعلام باستبعاد الأقسام غير ذات الصلة في وقت التخطيط، متجنبًا المسح في وقت التشغيل.

100%
flowchart TD
    A["SELECT * FROM orders<br/>WHERE order_date = '2024-02-15'"] --> B["مخطط الاستعلام"]
    B --> C{"تقليم الأقسام"}
    C -->|"order_date في [2024-02-01, 2024-03-01)"| D["orders_2024_02 ✓"]
    C -->|"خارج النطاق"| E["orders_2024_01 ✗"]
    C -->|"خارج النطاق"| F["orders_2024_03 ✗"]
    C -->|"خارج النطاق"| G["orders_default ✗"]
    D --> H["مسح قسم واحد فقط"]

(2) التحقق من تأثير التقليم

SQL
-- تمكين تقليم الأقسام (ON افتراضيًا)
SET enable_partition_pruning = on;

-- تحقق من الأقسام التي تم مسحها
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date = '2024-02-15';
TEXT 📖 للعرض فقط
Append
  -> Seq Scan on orders_2024_02
       Filter: (order_date = '2024-02-15'::date)

(3) الأسباب الشائعة لفشل التقليم

السيناريو تم تقليمه؟ السبب
WHERE order_date = '2024-02-15' الثابت قابل للتقليم
WHERE order_date = $1 (عبارة محضرة) ✅ PG 11+ تقليم معامل عام
WHERE order_date = now() الدالة المستقرة قابلة للتقليم
WHERE order_date = random_func() الدالة المتقلبة غير قابلة للتقليم
WHERE to_char(order_date, 'YYYY-MM') = '2024-02' الدالة تلف مفتاح التقسيم

7. العملية: صيانة الأقسام

(7) ▶ مثال

SQL
-- فصل قسم يناير 2023 (بدون فقدان بيانات)
ALTER TABLE orders DETACH PARTITION orders_2023_01;

-- الآن هو جدول مستقل، يمكن نقله إلى تخزين أرخص
ALTER TABLE orders_2023_01 SET TABLESPACE archive_tbs;

-- أو تصدير وإسقاط
COPY orders_2023_01 TO '/archive/orders_2023_01.csv';
DROP TABLE orders_2023_01;

Output:

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

(8) ▶ مثال

SQL
-- إنشاء جدول جديد أولاً
CREATE TABLE orders_2025_01 (LIKE orders INCLUDING DEFAULTS);

-- إضافة قيد تحقق للتحقق (يسرع ATTACH)
ALTER TABLE orders_2025_01
    ADD CONSTRAINT orders_2025_01_check
    CHECK (order_date >= '2025-01-01' AND order_date < '2025-02-01');

-- إرفاق بالأم (يأخذ قفلاً حصريًا لفترة وجيزة)
ALTER TABLE orders ATTACH PARTITION orders_2025_01
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

Output:

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

(9) ▶ مثال

SQL
-- فصل دون منع القراءات المتزامنة
ALTER TABLE orders DETACH PARTITION orders_2023_01 CONCURRENTLY;

Output:

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

(10) ▶ مثال

SQL
-- الفهرس على الأم ينتشر إلى جميع الأقسام
CREATE INDEX idx_orders_user_id ON orders (user_id);

-- كل قسم يحصل على فهرسه الخاص
\d orders_2024_01
TEXT 📖 للعرض فقط
Indexes:
    "orders_2024_01_user_id_idx" btree (user_id)
استراتيجية الفهرسة الوصف
إنشاء فهرس على الأم ينتشر تلقائيًا إلى جميع الأقسام الموجودة والمستقبلية
إنشاء فهرس على قسم واحد يؤثر فقط على ذلك القسم؛ يجب المزامنة يدويًا عند ATTACH
فهرس فريد يجب أن يتض��ن مفتاح التقسيم (التفرد عبر الأقسام مضمون بمفتاح التقسيم)

(11) ▶ مثال

SQL
-- هذا يفشل: فريد بدون مفتاح تقسيم
CREATE UNIQUE INDEX idx_orders_id ON orders (id);
-- ERROR: unique constraint must contain partition key

-- هذا يعمل: فريد مع مفتاح تقسيم
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);

Output:

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

8. المفهوم: وراثة الجداول (INHERITS)

(1) صيغة وخصائص الوراثة

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

SQL
-- الجدول الأم
CREATE TABLE people (
    id    SERIAL PRIMARY KEY,
    name  TEXT NOT NULL,
    email TEXT
);

-- الجدول الابن يرث جميع الأعمدة + يضيف إضافات
CREATE TABLE employees (
    salary    NUMERIC(10,2),
    dept      TEXT,
    hire_date DATE
) INHERITS (people);

-- إضافة مفتاح أساسي للابن
ALTER TABLE employees ADD PRIMARY KEY (id);

(2) الوراثة مقابل التقسيم التصريحي

الميزة وراثة الجداول (INHERITS) التقسيم التصريحي
أعمدة إضافية على الابن ✅ مسموح ❌ يجب مشاركة نفس الهيكل
الأم تخزن بيانات ✅ نعم ❌ الأم هيكل فارغ
توجيه INSERT تلقائي ❌ يحتاج مشغل ✅ تلقائي
تقليم الأقسام ❌ يحتاج قيود CHECK ✅ تلقائي
قيد فريد عبر الجداول ❌ جدول واحد فقط ✅ مع مفتاح التقسيم، عبر الجداول
مفتاح خارجي للأم ❌ غير مدعوم ✅ مدعوم
المرونة عالية متوسطة
السيناريو الموصى به أنواع فرعية غير متجانسة تحسين تقسيم الجداول الكبيرة

(3) استعلام الوراثة: الكلمة المفتاحية ONLY

SQL
-- استعلام الأم + جميع الأبناء
SELECT * FROM people;

-- استعلام الأم فقط (بدون أبناء)
SELECT * FROM ONLY people;

-- تحقق من أي جدول يأتي كل صف
SELECT tableoid::regclass, * FROM people;

9. العملية: تحسين الاستعلام عبر الأقسام

(12) ▶ مثال

SQL
-- تحليل قسم محدد
ANALYZE orders_2024_02;

-- تحليل جميع الأقسام عبر الأم
ANALYZE orders;

-- التحقق من إحصائيات الأقسام
SELECT relname, n_live_tup, last_analyze
FROM pg_stat_user_tables
WHERE relname LIKE 'orders_%'
ORDER BY relname;

Output:

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

(13) ▶ مثال

SQL
-- ربط على مستوى الأقسام (PG 12+)
SET enable_partitionwise_join = on;

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';

Output:

TEXT 📖 للعرض فقط
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
المعامل الافتراضي الوصف
enable_partition_pruning on تقليم الأقسام
enable_partitionwise_join off JOIN على مستوى الأقسام
enable_partitionwise_aggregate off تجميع على مستوى الأقسام
constraint_exclusion partition استبعاد القيود (للوراثة)

(14) ▶ مثال

SQL
-- جدول تدقيق أساسي
CREATE TABLE audit_log (
    id         BIGINT GENERATED ALWAYS AS IDENTITY,
    table_name TEXT NOT NULL,
    action     TEXT NOT NULL,
    changed_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (changed_at);

-- أقسام شهرية، كل منها موروث بجداول نوعية محددة
CREATE TABLE audit_log_2024_01 PARTITION OF audit_log
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- أعمدة إضافية لنوع تدقيق محدد عبر الوراثة
CREATE TABLE audit_log_user_changes (
    old_email TEXT,
    new_email TEXT
) INHERITS (audit_log_2024_01);

Output:

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

10. مثال شامل

SQL
-- إعداد تقسيم شهري كامل لطلبات التجارة الإلكترونية
CREATE TABLE orders (
    id           BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id      BIGINT NOT NULL,
    order_date   DATE NOT NULL,
    total_amount NUMERIC(12,2) DEFAULT 0,
    status       TEXT DEFAULT 'pending',
    created_at   TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (order_date);

-- إنشاء أقسام للربع الأول 2024
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

-- الفهرس الفريد يجب أن يتضمن مفتاح التقسيم
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date);

-- إدراج بيانات اختبار
INSERT INTO orders (user_id, order_date, total_amount, status) VALUES
    (1001, '2024-01-05', 299.99, 'completed'),
    (1001, '2024-02-14', 159.50, 'shipped'),
    (1002, '2024-01-20', 450.00, 'completed'),
    (1002, '2024-03-01', 89.00,  'pending'),
    (1003, '2024-02-28', 1200.00,'completed');

-- التحقق من تقليم الأقسام
EXPLAIN (COSTS OFF) SELECT * FROM orders
    WHERE order_date BETWEEN '2024-02-01' AND '2024-02-28';

-- فصل قسم قديم للأرشفة
ALTER TABLE orders DETACH PARTITION orders_2024_01;
COPY orders_2024_01 TO '/archive/orders_2024_01.csv';

-- إرفاق قسم جديد للربع التالي
CREATE TABLE orders_2024_04 (LIKE orders INCLUDING DEFAULTS);
ALTER TABLE orders_2024_04
    ADD CONSTRAINT chk_2024_04
    CHECK (order_date >= '2024-04-01' AND order_date < '2024-05-01');
ALTER TABLE orders ATTACH PARTITION orders_2024_04
    FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');

-- مراقبة أحجام الأقسا��
SELECT relname,
       pg_size_pretty(pg_total_relation_size(oid)) AS size,
       reltuples::bigint AS row_estimate
FROM pg_class
WHERE relname LIKE 'orders_2024%' ORDER BY relname;

❓ أسئلة شائعة

س هل يجب أن يتضمن المفتاح الأساسي للجدول المقسم مفتاح التقسيم؟
ج نعم. تتطلب PostgreSQL أن يتضمن القيد الفريد للجدول المقسم (بما في ذلك المفتاح الأساسي) عمود مفتاح التقسيم، وإلا لا يمكن ضمان التفرد عبر الأقسام.
س هل يمكن تحويل جدول عادي موجود إلى جدول مقسم؟
ج ليس مباشرة. يجب إنشاء جدول أم مقسم جديد، ثم ترحيل البيانات باستخدام INSERT INTO...SELECT أو ATTACH.
س ما هو خطر القسم DEFAULT؟
ج القسم DEFAULT يتلقى جميع البيانات التي لا تطابق أي شيء آخر؛ إذا تداخل نطاق قسم جديد مع بيانات موجودة بالفعل في DEFAULT، يفشل إرفاق القسم الجديد. راجع محتويات القسم DEFAULT دوريًا.
س هل هناك حد أعلى لعدد الأقسام؟
ج ليس لدى PostgreSQL حد صارم، لكن كل قسم يضيف عبء مخطط. حافظ على عدد أقسام الجدول الواحد أقل من بضع مئات؛ بعد 1,000 قسم، يمكن أن يرتفع وقت التخطيط بشكل ملحوظ.
س هل يمكن لوراثة الجداول أن تحل محل التقسيم التصريحي؟
ج عمومًا لا ينصح بذلك. تفتقر الوراثة إلى التوجيه التلقائي والتقليم والقيود عبر الجداول، ومكلفة في الصيانة. استخدم الوراثة فقط عندما تحتاج جداول ابنة غير متجانسة (أعمدة إضافية).
س هل يمكن لتقسيم HASH استخدام تقليم الأقسام؟
ج نعم. طالما أن شرط WHERE يتضمن مقارنة مساواة على مفتاح التقسيم، يمكن لـ PG حساب دلو التجزئة الذي تقع فيه البيانات، وتقليم الأقسام الأخرى.
س هل UPDATE الذي ينقل صفًا عبر الأقسام يقفل الجدول؟
ج PG 11+ يدعم UPDATE عبر الأقسام؛ إنه داخليًا DELETE + INSERT، لا يقفل الجدول بأكمله لكنه يأخذ أقفال صف على كل من القسمين.

📖 ملخص


📝 تمارين

  1. ⭐ أنشئ جدول payments مقسمًا ربع سنوي بـ RANGE، مع أقسام للأرباع الأربعة من 2024، وتحقق من تأثير التقليم لـ WHERE payment_date BETWEEN '2024-Q2'.

  2. ⭐⭐ صمم مخطط تقسيم LIST لنظام SaaS متعدد المستأجرين: جزئ tenant_id إلى 4 أقسام، اكتب عبارات الإنشاء، واختبر عزل البيانات عبر المستأجرين المختلفين.

  3. ⭐⭐⭐ اكتب إجراءً مخزنًا يأخذ سنة كمدخل، وينشئ تلقائيًا 12 قسمًا شهريًا لجدول orders، ويضيف قيود CHECK، وينشئ فهارس، ويفصل قسم يناير السابق إلى مساحة جداول أرشيف. اختبر التدفق الكامل لـ INSERT، والاستعلام عبر الأقسام، والتحقق من التقليم.

Web-Tutorial.com

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

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

100%