---
title: 'pg_partman + pg_cron: Postgres 18 التقسيم التلقائي (2026)'
description: 'شرح: تثبيت pg_partman 5.4.3 + pg_cron على Postgres 18.4، إعداد تقسيمات النطاق اليومية مع سياسة الاستبقاء، أتمتة الصيانة، وحل المشكلات.'
url: https://nerdleveltech.com/ar/pg-partman-pg-cron-postgres-18-partition-automation-tutorial
language: ar
date: '2026-05-20'
category: backend
tags:
  - pg_partman
  - pg_cron
  - postgres
  - postgresql
  - postgres-18
  - partitioning
  - declarative-partitioning
  - retention
  - background-jobs
  - backend
markdown_url: https://nerdleveltech.com/ar/pg-partman-pg-cron-postgres-18-partition-automation-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/pg-partman-pg-cron-postgres-18-partition-automation-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# pg_partman + pg_cron: Postgres 18 التقسيم التلقائي (2026)

لتقسيم جدول PostgreSQL 18 حسب اليوم باستخدام pg\_partman 5.4.3، قم بتثبيت `postgresql-18-partman` و `postgresql-18-cron`، وأضف كليهما إلى `shared_preload_libraries`، ثم استدعِ `partman.create_parent('schema.table', 'created_at', 'range', '1 day')`، واضبط `retention = '30 days'` و `retention_keep_table = false` في `part_config`، ثم قم بجدولة `CALL partman.run_maintenance_proc();` من pg\_cron كل ساعة. الدورة بأكملها — الإنشاء المسبق للأقسام المستقبلية، وحذف الأقسام القديمة التي تجاوزت فترة الاحتفاظ — تعمل دون المساس بكود التطبيق.

## ملخص

ستقوم ببناء جدول `events` عملي وقابل للتشغيل مقسم حسب اليوم على PostgreSQL 18.4 باستخدام pg\_partman 5.4.3، مع استخدام pg\_cron لتشغيل استدعاء الصيانة. بنهاية هذا الدليل، سيكون لديك 5 أقسام مستقبلية منشأة مسبقاً، وفترة احتفاظ لمدة 30 يوماً تحذف الأقدم تلقائياً، ومسار واضح لتوسيع هذا النمط إلى مليارات الصفوف. إجمالي وقت الشرح حوالي 25 دقيقة بدءاً من سحب Docker جديد.

## ما ستتعلمه

- كيفية تثبيت pg\_partman 5.4.3 و pg\_cron 1.6.7 على Postgres 18 باستخدام حزم PGDG الرسمية.
- توقيع `create_parent` الخاص بـ pg\_partman 5.x (ولماذا اختفت قيمة `p_type => 'native'` القديمة).
- كيفية تكوين فترة احتفاظ لمدة 30 يوماً مع إعداد `retention_keep_table` الصحيح لضمان حذف الأقسام فعلياً.
- كيفية جدولة `partman.run_maintenance_proc()` باستخدام pg\_cron — ومتى تفضله على عامل الخلفية الخاص بـ pg\_partman.
- كيفية التعامل مع التغيير الجذري في Postgres 18 الذي يمنع الجداول المقسمة الأبوية غير المسجلة (unlogged).
- كيفية التحقق من أن الأقسام، وفترة الاحتفاظ، ومهمة cron تعمل جميعها كما هو متوقع.

## المتطلبات الأساسية

- نسخة حديثة من Docker Desktop أو Docker Engine مع `Docker compose` الإصدار الثاني (صيغة الوصلة `Docker-compose` للإصدار الأول تعمل أيضاً إذا قمت بتعديل الأوامر).
- إلمام بـ `psql` وأساسيات SQL.
- حوالي 25 دقيقة و2 جيجابايت من مساحة القرص لصورة Postgres ووحدة تخزين (volume) صغيرة.

نحن نعتمد Postgres 18.4 (الصادر في 14-05-2026)[^pg18-4] و pg\_partman 5.4.3 (الصادر في 05-03-2026)[^partman-543]. **لا تستخدم pg\_partman 5.4.2** — ملف التحكم في الإضافة يأتي بسلسلة إصدار خاطئة مما يؤدي لفشل التحديث من 5.4.1؛ الإصدار 5.4.3 يصلح هذا التراجع[^partman-543].

## الخطوة 1 — بناء صورة Postgres 18 مع كلتا الإضافتين

الصورة الرسمية `postgres:18.4`[^postgres-image] لا تتضمن pg\_partman أو pg\_cron، ولكنها تأتي مع مستودع PGDG apt مهيأ مسبقاً في `/etc/apt/sources.list.d/`[^pgdg-apt] — لذا فإن أمر `apt-get install postgresql-18-partman postgresql-18-cron` العادي يعمل دون أي إعداد إضافي للمستودع. أنشئ ملف `Dockerfile`:

```dockerfile
# Dockerfile
FROM postgres:18.4

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        postgresql-18-partman \
        postgresql-18-cron \
    && rm -rf /var/lib/apt/lists/*

# Pass settings as -c overrides instead of replacing config_file outright —
# this way the image's auto-generated postgresql.conf (data_directory,
# hba_file, lc_messages, etc.) still loads underneath and we only set
# the knobs the extensions care about. Replacing config_file entirely is
# fragile because the auto-generated values live inside the data volume.
CMD ["postgres", \
    "-c", "shared_preload_libraries=pg_partman_bgw,pg_cron", \
    "-c", "cron.database_name=events", \
    "-c", "pg_partman_bgw.dbname="]
```

ثلاثة أشياء تقوم تجاوزات `-c` بإعدادها:

- `shared_preload_libraries` تقوم بتحميل كلا عاملي الخلفية عند بدء تشغيل الخادم. يجب أن يكونا موجودين قبل أول عملية `CREATE EXTENSION` وإلا ستنجح عملية الإنشاء ولكن لن يعمل شيء في الخلفية.
- `cron.database_name = 'events'` تخبر pg\_cron بقاعدة البيانات التي يجب تثبيت كتالوج `cron.*` الخاص بها فيها. **يمكن تثبيت pg\_cron في قاعدة بيانات واحدة فقط لكل عنقود (cluster)**[^pgcron-readme]؛ يتم إجراء الجدولة عبر قواعد البيانات باستخدام `cron.schedule_in_database()` لاحقاً.
- `pg_partman_bgw.dbname = ` (فارغة) تبقي عامل الخلفية الخاص بـ pg\_partman محملاً ولكن خاملاً — عندما تكون هذه القائمة (CSV) غير محددة، يتخطى العامل استدعاء `run_maintenance()` تماماً[^partman-docs]. يصبح pg\_cron هو المجدول الوحيد.

قم ببنائه وتشغيله باستخدام ملف Docker-compose للحفاظ على البيانات بين عمليات إعادة التشغيل:

```yaml
# Docker-compose.yml
services:
  pg18:
    build: .
    environment:
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: events
    ports: ["5432:5432"]
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:
```

قم بتشغيله:

```bash
Docker compose up -d --build
Docker compose logs pg18 | tail -5
```

يجب أن ترى `database system is ready to accept connections`. إذا رأيت `FATAL: extension "pg_cron" must be loaded via shared_preload_libraries`، فهذا يعني أن علامة `-c shared_preload_libraries=...` لا تصل إلى postgres — أعد التحقق من سطر `CMD` في Dockerfile وأعد البناء باستخدام `Docker compose up -d --build`.

## الخطوة 2 — إنشاء الإضافات والجدول الأبوي

اتصل باستخدام `psql`:

```bash
Docker compose exec pg18 psql -U postgres -d events
```

يتوقع pg\_partman أن يتواجد في مخطط (schema) خاص به؛ الاسم التقليدي هو `partman`[^partman-docs]. يتم تثبيت pg\_cron دائماً في `cron`. تنشئ كلتا الإضافتين أنواعاً وجداولاً ووظائف، لذا يجب تشغيل هذا كمستخدم خارق (superuser):

```sql
CREATE EXTENSION IF NOT EXISTS pg_cron;

CREATE SCHEMA IF NOT EXISTS partman;
CREATE EXTENSION IF NOT EXISTS pg_partman WITH SCHEMA partman;

\dx
```

يجب أن يسرد مخرج `\dx` كلاً من `pg_cron` (الإصدار 1.6.7) و `pg_partman` (الإصدار 5.4.3).

الآن قم بإنشاء الجدول الأبوي. قاعدة Postgres 18 الحاسمة: **لا يمكن أن يكون الجدول المقسم `UNLOGGED`** — يجب أن يكون الأب مسجلاً (logged)، وترث الأقسام الفرعية ذلك[^pg18-unlogged-mail][^partman-774]. إذا كنت تريد أقساماً فرعية غير مسجلة، فيجب عليك تمييز جدول *قالب* pg\_partman كغير مسجل بعد الحقيقة؛ سنغطي ذلك في قسم استكشاف الأخطاء وإصلاحها. في الوقت الحالي، جدول أبوي `LOGGED` عادي:

```sql
CREATE TABLE public.events (
    event_id    BIGINT      NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL,
    user_id     UUID        NOT NULL,
    event_type  TEXT        NOT NULL,
    payload     JSONB       NOT NULL DEFAULT '{}'::jsonb,
    PRIMARY KEY (event_id, created_at)
) PARTITION BY RANGE (created_at);
```

شيئان يجب ملاحظتهما: مفتاح التقسيم (`created_at`) هو جزء من المفتاح الأساسي — يتطلب Postgres ذلك لأي قيد فريد على جدول مقسم. والجدول فارغ: يتولى pg\_partman إدارة إنشاء الأقسام، لذا لا تقم بإنشاء الجداول الفرعية بنفسك.

## الخطوة 3 — استدعاء create\_parent بطريقة pg\_partman 5

توقيع `create_parent` في pg\_partman 5.x يختلف عن دروس 4.x التي ستجدها في أعلى نتائج بحث جوجل. الوسائط الأربعة الأولى مطلوبة، و `p_type` يقبل فقط `'range'` أو `'list'` — تمت إزالة قيمة `'native'` القديمة في الانتقال إلى الإصدار 5.0 للتقسيم التصريحي فقط وتظهر الآن خطأ `ERROR: native is not a valid partitioning type for pg_partman`[^partman-native-removed][^partman-docs]:

```sql
SELECT partman.create_parent(
    p_parent_table     => 'public.events',
    p_control          => 'created_at',
    p_type             => 'range',
    p_interval         => '1 day',
    p_premake          => 5,
    p_start_partition  => (date_trunc('day', now()) - interval '2 days')::text
);
```

ماذا تعني الوسائط:

- `p_control` — العمود الذي سيتم التقسيم بناءً عليه. يجب أن يكون موجوداً بالفعل في الجدول الأب.
- `p_type` — `'range'` (يعتمد على الوقت أو الرقم) أو `'list'` (رقمي فقط، interval=1)[^partman-docs].
- `p_interval` — سلسلة نصية للفترة الزمنية. الأشكال الشائعة هي `'1 day'`، و`'1 hour'`، و`'1 week'`، و`'1 month'`؛ كما يقبل pg\_partman 5 السلاسل النصية المريحة `'daily'`، و`'hourly'`، و`'weekly'`، و`'monthly'` ويخزنها كفترات زمنية (intervals)[^partman-docs][^crunchy-intervals].
- `p_premake` — عدد الأقسام (partitions) التي سيتم إنشاؤها مسبقاً قبل الوقت "الحالي". القيمة الافتراضية هي 4؛ قمنا بتعيينها هنا إلى 5، لذا سينتهي بك الأمر مع يوم اليوم الحالي بالإضافة إلى 5 أيام مستقبلية.
- `p_start_partition` — يحدد حدود أقدم قسم. نبدأ قبل يومين لمنح العرض التوضيحي قسمين قديمين قليلاً يمكنك مشاهدتهما وهما يُحذفان فوراً بواسطة سياسة الاستبقاء (retention).

تعيد `create_parent` القيمة `t` (true) عند النجاح وتنشئ الأقسام التابعة بالإضافة إلى صف في `partman.part_config`:

```sql
SELECT parent_table, control, partition_interval, premake
  FROM partman.part_config WHERE parent_table = 'public.events';
```

```
     parent_table     |  control   | partition_interval | premake
----------------------+------------+--------------------+---------
 public.events        | created_at | 1 day              |       5
```

والأقسام نفسها:

```sql
SELECT * FROM partman.show_partitions('public.events');
```

يجب أن ترى 8 جداول تابعة (من `events_p2026_05_18` إلى `events_p2026_05_25` إذا كان اليوم هو 20) — اثنان تاريخيان، واليوم الحالي، وخمسة أيام مستقبلية. تتبع الأسماء النمط `events_p{YYYY_MM_DD}` للفترات اليومية[^partman-docs].

## الخطوة 4 — تهيئة الاستبقاء وحذف الأقسام القديمة فعلياً

بشكل افتراضي، **لا** يقوم pg\_partman بحذف الأقسام القديمة. الاستبقاء هو ميزة اختيارية عبر `part_config`، وهناك ملاحظة هامة: القيمة الافتراضية `retention_keep_table = true` تقوم فقط *بإلغاء توريث* الأقسام القديمة، مما يتركها كجداول مستقلة في قاعدة البيانات[^partman-docs]. للحصول على تنظيف تلقائي حقيقي، يجب عليك تغييرها إلى `false`:

```sql
UPDATE partman.part_config
   SET retention             = '30 days',
       retention_keep_table  = false
 WHERE parent_table = 'public.events';
```

بضع جمل حول سبب كون الإعداد الافتراضي متحفظاً: تفترض إعدادات pg\_partman الافتراضية أن فقدان البيانات العرضي هو أسوأ نتيجة ممكنة، لذا فهي تميل إلى "إلغاء التوريث، وترك جدول يمكنك إعادة ربطه لاحقاً" بدلاً من `DROP TABLE`[^partman-docs]. بالنسبة لسجل الأحداث حيث يمثل 30 يوماً الحد الأقصى القانوني للاستبقاء، فإن القيمة `false` هي ما تريده حقاً.

## الخطوة 5 — جدولة الصيانة باستخدام pg\_cron

يأتي pg\_partman مع عامل خلفية خاص به (`pg_partman_bgw`) يقوم باستدعاء `run_maintenance_proc()` كل `pg_partman_bgw.interval` ثانية (الافتراضي 3600)[^partman-docs]. إذاً لماذا نكلف أنفسنا عناء استخدام pg\_cron؟

هناك ثلاثة أسباب. أولاً، يعمل عامل الخلفية لـ pg\_partman كل عدد معين من الثواني دون دقة لكل وظيفة — لا يمكنك قول "كل 5 دقائق خلال ساعات العمل، وكل ساعة في غير ذلك". أما pg\_cron فيقبل تعبيرات cron كاملة، مجدولة لكل وظيفة. ثانياً، يوفر لك pg\_cron تاريخاً لكل وظيفة في `cron.job_run_details`، وهو ما يسهل استخدامه للتنبيهات بدلاً من تحليل سجلات Postgres. ثالثاً، إذا كنت تستخدم pg\_cron بالفعل لجداول `VACUUM` أو تحديث الإحصائيات، فإن إضافة صيانة الأقسام إلى نفس المجدول يقلل من تعقيد النظام.

قم بجدولة إجراء الصيانة مرة واحدة كل ساعة. ولأن إضافة pg\_cron موجودة في قاعدة بيانات `events` (التي تم تعيينها بواسطة `cron.database_name` في الخطوة 1)، فإن `cron.schedule` ينفذ كود SQL هناك مباشرة[^pgcron-readme]:

```sql
SELECT cron.schedule(
    'pg_partman-maintenance',
    '0 * * * *',
    $$CALL partman.run_maintenance_proc()$$
);
```

سلسلة الجدول الزمني هي صيغة cron القياسية بتوقيت UTC. تحقق من تسجيل الوظيفة:

```sql
SELECT jobid, schedule, command, active
  FROM cron.job WHERE jobname = 'pg_partman-maintenance';
```

وتاريخ كل تشغيل (سيكون هذا فارغاً حتى بداية الساعة القادمة):

```sql
SELECT jobid, runid, status, return_message, start_time, end_time
  FROM cron.job_run_details
 ORDER BY start_time DESC LIMIT 5;
```

إذا كنت لا ترغب في الانتظار لمدة ساعة لرؤية أي شيء، فقم بتنفيذ الإجراء يدوياً مرة واحدة:

```sql
CALL partman.run_maintenance_proc();
```

هذا إجراء (procedure) وليس وظيفة (function)، لذا يجب استدعاؤه باستخدام `CALL`. بعد تشغيله، يجب أن يظل `partman.show_partitions('public.events')` يعرض سلمك الكامل للأقسام المستقبلية — فإجراء `run_maintenance_proc` هو إجراء متساوي القوى (idempotent).

## الخطوة 6 — التحقق من التوجيه، والاستبقاء، وحلقة cron

أدخل بضعة صفوف تمتد عبر نطاق زمني واسع، بما في ذلك صف خارج نافذة الاستبقاء:

```sql
INSERT INTO public.events (event_id, created_at, user_id, event_type)
SELECT gs, now() - (gs || ' hours')::interval,
       gen_random_uuid(), 'pageview'
  FROM generate_series(1, 200) gs;
```

تأكد من أن Postgres قام بتوجيهها إلى الجداول التابعة الصحيحة (يجب أن ترى عدداً > 0 لقسم اليوم الحالي وصفر نتائج في الجدول الأب نفسه):

```sql
SELECT tableoid::regclass AS partition, count(*)
  FROM public.events GROUP BY partition ORDER BY partition;
```

الآن أدخل صفاً داخل قسم أقدم من فترة الاستبقاء ولكنه أحدث من أقدم قسم تم إنشاؤه مسبقاً، ثم قم بتشغيل الصيانة:

```sql
INSERT INTO public.events VALUES
    (999, now() - interval '40 days',
     gen_random_uuid(), 'historical');

CALL partman.run_maintenance_proc();

SELECT * FROM partman.show_partitions('public.events');
```

لقد استقر الصف الذي يبلغ عمره 40 يوماً في قسم تجاوز الآن `retention = '30 days'`، لذا فإن استدعاء الصيانة التالي سيقوم بحذفه. (إذا كنت تريد بدلاً من ذلك الحفاظ على هذا القسم للتدقيق، فستقوم بتعيين `retention_keep_table = true` من الخطوة 4 وسيكون الجدول غير موروث ولكن لا يزال من الممكن الاستعلام عنه مباشرة).

## الأخطاء الشائعة

أوضاع الخطأ الخمسة التي تستحق المعرفة قبل إرسال هذا إلى الإنتاج:

**`ERROR: native is not a valid partitioning type for pg_partman`** — أنت تستخدم درساً تعليمياً لـ pg\_partman 4.x مع تثبيت إصدار 5.x. استبدل `p_type => 'native'` بـ `'range'` (أو `'list'`). تمت إزالة القيمة `native` في الإصدار 5.0 عندما تخلى pg\_partman عن التقسيم القائم على المشغلات (triggers) تماماً[^partman-native-removed].

**`ERROR: partitioned tables cannot be unlogged`** — لقد حاولت تنفيذ `CREATE UNLOGGED TABLE ... PARTITION BY RANGE`. يفرض Postgres 18 ما كانت تسمح به الإصدارات الأقدم بصمت[^pg18-unlogged-mail]. إذا كنت تريد أقساماً تابعة غير مسجلة (unlogged) لجدول من نوع المقاييس (metrics) حيث لا تهم المتانة، فأنشئ الجدول الأب كـ `LOGGED` ثم قم بتنفيذ `ALTER TABLE partman.template_public_events SET UNLOGGED;` — ترث الأقسام التابعة الجديدة علامة unlogged من خلال نظام القوالب في pg\_partman[^partman-774].

**الأقسام القديمة لا تُحذف** — تحقق من `retention_keep_table` في الصف الموجود في `partman.part_config`. القيمة الافتراضية `true` تقوم فقط بإلغاء توريث الأقسام القديمة، لذا فهي تتراكم كجداول مستقلة حتى تقوم بـ `DROP TABLE` يدوياً. اضبطها على `false` للتنظيف الفعلي.

**`FATAL: extension "pg_cron" must be loaded via shared_preload_libraries`** — يتطلب pg\_cron التحميل عند وقت بدء التشغيل وإعادة تشغيل Postgres. يجب أن يتضمن أمر `CMD` الخاص بحاوية Docker الخيار `-c shared_preload_libraries=pg_partman_bgw,pg_cron` (أو ما يعادله في postgresql.conf) *قبل* البدء الأول. بعد تغيير Dockerfile، استخدم `Docker compose up -d --build` لإعادة البناء.

**وظائف Cron تعمل ولكنها لا تكتب أبداً في `cron.job_run_details`** — غالباً ما يكون ذلك بسبب عدم تطابق في `cron.database_name`: يخزن pg\_cron التاريخ في قاعدة البيانات المسماة هناك. إذا قمت بتثبيت `pg_cron` في `events` ولكن `cron.database_name` لا تزال تشير إلى `postgres`، فلن يذهب التاريخ إلى أي مكان. أعد التشغيل بالقيمة الصحيحة.

## الخطوات التالية ومزيد من القراءة

يتوافق هذا النمط بشكل طبيعي مع بقية أدوات Postgres 18. إذا كنت تقوم بالترقية إلى Postgres 18 في بيئة الإنتاج، فإن [دليل الترقية بدون وقت توقف باستخدام pg\_createsubscriber](https://nerdleveltech.com/postgres-18-zero-downtime-upgrade-pg-createsubscriber-tutorial) يشرح عملية الانتقال. بالنسبة لأحمال عمل المتجهات (vector workloads) على نفس المخطط المقسم، راجع [دليل ضبط pgvector + HNSW في بيئة إنتاج Postgres 18](https://nerdleveltech.com/pgvector-hnsw-postgres-18-production-tuning-tutorial). وإذا كنت تفضل وضع مهام الخلفية في طابور داخل Postgres بدلاً من جدولتها باستخدام cron، فإن [دليل طابور مهام pg-boss](https://nerdleveltech.com/pg-boss-postgres-job-queue-node-TypeScript-production-tutorial) هو الشقيق الشبيه بالطابور لهذا المنشور الشبيه بالمجدول.

قراءات خارجية تستحق الحفظ: وثائق pg\_partman على فرع التطوير[^partman-docs]، وملاحظات إصدار pg\_partman لسلسلة 5.x[^partman-543]، وملف README الخاص بـ pg\_cron لمعرفة صيغ الجدولة المتقدمة بما في ذلك متغير `cron.schedule_in_database` عندما تستهدف مهامك قاعدة بيانات مختلفة عن تلك التي تم تثبيت pg\_cron فيها[^pgcron-readme].

[^pg18-4]: PostgreSQL 18.4 release announcement, May 14 2026 — <https://www.postgresql.org/docs/release/>

[^partman-543]: pg\_partman 5.4.3 on PGXN, released March 5 2026 — <https://pgxn.org/dist/pg_partman/5.4.3/>

[^postgres-image]: Postgres official Docker image tag list — <https://hub.Docker.com/_/postgres>

[^pgdg-apt]: PostgreSQL Debian/Ubuntu APT repository documentation — <https://wiki.postgresql.org/wiki/Apt>

[^pgcron-readme]: pg\_cron README and CHANGELOG — <https://GitHub.com/citusdata/pg_cron>

[^partman-docs]: pg\_partman canonical documentation (development branch) — <https://GitHub.com/pgpartman/pg_partman/blob/development/doc/pg_partman.md>

[^pg18-unlogged-mail]: PostgreSQL hackers thread on partitioned-table unlogged restriction (Michael Paquier) — <https://www.postgresql.org/message-id/ZiiyGFTBNkqcMQi_@paquier.xyz>

[^partman-774]: pg\_partman issue #774 — Postgres 18 unlogged parent restriction handling — <https://GitHub.com/pgpartman/pg_partman/issues/774>

[^partman-native-removed]: pg\_partman discussion #691 — `'native'` is no longer a valid p\_type in 5.x — <https://GitHub.com/pgpartman/pg_partman/discussions/691>

[^crunchy-intervals]: Crunchy Data — Time Partitioning and Custom Time Intervals in Postgres with pg\_partman — <https://www.crunchydata.com/blog/time-partitioning-and-custom-time-intervals-in-postgres-with-pg_partman>
