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

٢٠ مايو ٢٠٢٦

pg_partman + pg_cron: Postgres 18 Auto Partitions (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)1 و pg_partman 5.4.3 (الصادر في 05-03-2026)2. لا تستخدم pg_partman 5.4.2 — ملف التحكم في الإضافة يأتي بسلسلة إصدار خاطئة مما يؤدي لفشل التحديث من 5.4.1؛ الإصدار 5.4.3 يصلح هذا التراجع2.

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

الصورة الرسمية postgres:18.43 لا تتضمن pg_partman أو pg_cron، ولكنها تأتي مع مستودع PGDG apt مهيأ مسبقاً في /etc/apt/sources.list.d/4 — لذا فإن أمر apt-get install postgresql-18-partman postgresql-18-cron العادي يعمل دون أي إعداد إضافي للمستودع. أنشئ ملف 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)5؛ يتم إجراء الجدولة عبر قواعد البيانات باستخدام cron.schedule_in_database() لاحقاً.
  • pg_partman_bgw.dbname = (فارغة) تبقي عامل الخلفية الخاص بـ pg_partman محملاً ولكن خاملاً — عندما تكون هذه القائمة (CSV) غير محددة، يتخطى العامل استدعاء run_maintenance() تماماً6. يصبح pg_cron هو المجدول الوحيد.

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

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

قم بتشغيله:

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:

Docker compose exec pg18 psql -U postgres -d events

يتوقع pg_partman أن يتواجد في مخطط (schema) خاص به؛ الاسم التقليدي هو partman6. يتم تثبيت pg_cron دائماً في cron. تنشئ كلتا الإضافتين أنواعاً وجداولاً ووظائف، لذا يجب تشغيل هذا كمستخدم خارق (superuser):

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)، وترث الأقسام الفرعية ذلك78. إذا كنت تريد أقساماً فرعية غير مسجلة، فيجب عليك تمييز جدول قالب pg_partman كغير مسجل بعد الحقيقة؛ سنغطي ذلك في قسم استكشاف الأخطاء وإصلاحها. في الوقت الحالي، جدول أبوي LOGGED عادي:

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_partman96:

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)6.
  • p_interval — سلسلة نصية للفترة الزمنية. الأشكال الشائعة هي '1 day'، و'1 hour'، و'1 week'، و'1 month'؛ كما يقبل pg_partman 5 السلاسل النصية المريحة 'daily'، و'hourly'، و'weekly'، و'monthly' ويخزنها كفترات زمنية (intervals)610.
  • p_premake — عدد الأقسام (partitions) التي سيتم إنشاؤها مسبقاً قبل الوقت "الحالي". القيمة الافتراضية هي 4؛ قمنا بتعيينها هنا إلى 5، لذا سينتهي بك الأمر مع يوم اليوم الحالي بالإضافة إلى 5 أيام مستقبلية.
  • p_start_partition — يحدد حدود أقدم قسم. نبدأ قبل يومين لمنح العرض التوضيحي قسمين قديمين قليلاً يمكنك مشاهدتهما وهما يُحذفان فوراً بواسطة سياسة الاستبقاء (retention).

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

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

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

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

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

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

بشكل افتراضي، لا يقوم pg_partman بحذف الأقسام القديمة. الاستبقاء هو ميزة اختيارية عبر part_config، وهناك ملاحظة هامة: القيمة الافتراضية retention_keep_table = true تقوم فقط بإلغاء توريث الأقسام القديمة، مما يتركها كجداول مستقلة في قاعدة البيانات6. للحصول على تنظيف تلقائي حقيقي، يجب عليك تغييرها إلى false:

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

بضع جمل حول سبب كون الإعداد الافتراضي متحفظاً: تفترض إعدادات pg_partman الافتراضية أن فقدان البيانات العرضي هو أسوأ نتيجة ممكنة، لذا فهي تميل إلى "إلغاء التوريث، وترك جدول يمكنك إعادة ربطه لاحقاً" بدلاً من DROP TABLE6. بالنسبة لسجل الأحداث حيث يمثل 30 يوماً الحد الأقصى القانوني للاستبقاء، فإن القيمة false هي ما تريده حقاً.

الخطوة 5 — جدولة الصيانة باستخدام pg_cron

يأتي pg_partman مع عامل خلفية خاص به (pg_partman_bgw) يقوم باستدعاء run_maintenance_proc() كل pg_partman_bgw.interval ثانية (الافتراضي 3600)6. إذاً لماذا نكلف أنفسنا عناء استخدام 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 هناك مباشرة5:

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

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

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

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

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

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

CALL partman.run_maintenance_proc();

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

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

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

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 لقسم اليوم الحالي وصفر نتائج في الجدول الأب نفسه):

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

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

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) تماماً9.

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

الأقسام القديمة لا تُحذف — تحقق من 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 يشرح عملية الانتقال. بالنسبة لأحمال عمل المتجهات (vector workloads) على نفس المخطط المقسم، راجع دليل ضبط pgvector + HNSW في بيئة إنتاج Postgres 18. وإذا كنت تفضل وضع مهام الخلفية في طابور داخل Postgres بدلاً من جدولتها باستخدام cron، فإن دليل طابور مهام pg-boss هو الشقيق الشبيه بالطابور لهذا المنشور الشبيه بالمجدول.

قراءات خارجية تستحق الحفظ: وثائق pg_partman على فرع التطوير6، وملاحظات إصدار pg_partman لسلسلة 5.x2، وملف README الخاص بـ pg_cron لمعرفة صيغ الجدولة المتقدمة بما في ذلك متغير cron.schedule_in_database عندما تستهدف مهامك قاعدة بيانات مختلفة عن تلك التي تم تثبيت pg_cron فيها5.

Footnotes

  1. PostgreSQL 18.4 release announcement, May 14 2026 — https://www.postgresql.org/docs/release/

  2. pg_partman 5.4.3 on PGXN, released March 5 2026 — https://pgxn.org/dist/pg_partman/5.4.3/ 2 3

  3. Postgres official Docker image tag list — https://hub.Docker.com/_/postgres

  4. PostgreSQL Debian/Ubuntu APT repository documentation — https://wiki.postgresql.org/wiki/Apt

  5. pg_cron README and CHANGELOG — https://GitHub.com/citusdata/pg_cron 2 3

  6. pg_partman canonical documentation (development branch) — https://GitHub.com/pgpartman/pg_partman/blob/development/doc/pg_partman.md 2 3 4 5 6 7 8 9 10

  7. PostgreSQL hackers thread on partitioned-table unlogged restriction (Michael Paquier) — https://www.postgresql.org/message-id/ZiiyGFTBNkqcMQi_@paquier.xyz 2

  8. pg_partman issue #774 — Postgres 18 unlogged parent restriction handling — https://GitHub.com/pgpartman/pg_partman/issues/774 2

  9. pg_partman discussion #691 — 'native' is no longer a valid p_type in 5.x — https://GitHub.com/pgpartman/pg_partman/discussions/691 2

  10. 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


نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.