جداول PostgreSQL ممكن تتضخم 40% أو أكتر من غير ما تضيف صف داتا واحد. السبب مش زيادة في الاستخدام، السبب اسمه table bloat، وغالبًا autovacuum مش شغّال زي ما انت فاكر. المقال ده هيوريك إزاي تكتشف المشكلة بدقة، تضبط autovacuum على مستوى الجدول، وتسترجع المساحة في إنتاج بدون ما تقفل قاعدة البيانات.
جداول PostgreSQL بتكبر بدون داتا جديدة؟ المشكلة في table bloat
المشكلة باختصار
عندك جدول users فيه 2 مليون صف، حجم البيانات الفعلية 800MB، لكن لما تعمل SELECT pg_total_relation_size('users') الرقم بيطلع 2.4GB. الفرق ده هو الـ bloat — مساحة محجوزة لصفوف اتمسحت أو اتعدّلت ولسه قاعدة محتلّاها على القرص. النتيجة: استعلامات أبطأ، backups أتقل، وفاتورة storage بتزيد بدون سبب واضح.
مثال للمبتدئين: كراسة وممحاة
تخيل كراسة بتكتب فيها 100 سطر كل يوم. لما تغلط في سطر، بدل ما تستخدم الممحاة، بتشطب السطر بقلم وتكتب الجديد تحته. بعد شهر هتلاقي الكراسة فيها 3000 سطر مكتوب، بس 1500 منهم مشطوب. الكراسة وزنها اتضاعف من غير ما تكتب معلومات جديدة فعلًا.
PostgreSQL بيشتغل بنفس الطريقة بالظبط. لما تعمل UPDATE على صف، PostgreSQL مش بيعدّل الصف القديم في مكانه؛ بيعمل نسخة جديدة في مكان تاني وبيسيب القديم محجوز. الصفوف القديمة دي اسمها dead tuples، والمسؤول عن مسحها فعليًا اسمه autovacuum. لو autovacuum ما لحقش، الكراسة بتكبر بلا توقف.
التعريف العلمي: ليه PostgreSQL بيعمل كده
PostgreSQL بيستخدم نموذج اسمه MVCC (Multi-Version Concurrency Control). الفكرة إن كل transaction بتشوف نسخة من الداتا متناسقة في لحظة بدايتها، حتى لو في transactions تانية بتعدّل في نفس اللحظة. علشان ده يحصل بدون locks ثقيلة، PostgreSQL بيحتفظ بنسخ متعددة من الصف الواحد، وكل نسخة بتاخد رقم transaction (XID) بيحدد إمتى تكون visible لمين.
لما الـ transactions بتنتهي، النسخ القديمة بتفضل في الجدول، وautovacuum مهمته يمشي كل فترة، يحدد النسخ اللي مفيش transaction نشطة فاكراها، ويفرّغ المساحة بتاعتها. لو autovacuum بطيء، أو الإعدادات الافتراضية بتاعته مش مناسبة لحجم الجدول، الجدول بيفضل bloated وبيكبر مع الوقت.
إزاي تكتشف bloat فعليًا
أول حاجة، نصب الـ extension اللي بيقيس bloat بدقة:
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT
schemaname || '.' || relname AS table_name,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
ROUND((s.dead_tuple_percent)::numeric, 1) AS dead_pct,
pg_size_pretty(s.dead_tuple_len) AS dead_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
CROSS JOIN LATERAL pgstattuple(c.oid) s
WHERE n.nspname = 'public'
AND c.relkind = 'r'
ORDER BY s.dead_tuple_len DESC
LIMIT 10;الافتراض: pgstattuple بيعمل full scan للجدول، فمتشغّلوش على جداول أكبر من 50GB في أوقات الذروة. لو الجدول كبير جدًا، استخدم pgstattuple_approx بدلها — أسرع 100×، ودقتها مقبولة (±2%).
القراءة: لو dead_pct على جدول معيّن أكبر من 20%، عندك مشكلة فعلية. لو وصلت 40%+، الجدول كان المفروض autovacuum يكون لمسه من زمان وما لمسهوش.
الحل الأول: ضبط autovacuum للجدول المعيّن
autovacuum بيشتغل افتراضيًا لما dead tuples تتعدّى 20% من حجم الجدول (autovacuum_vacuum_scale_factor = 0.2). بس للجداول الكبيرة (أكبر من 10GB)، 20% بقت رقم ضخم — يعني autovacuum مش هيشتغل غير لما يكون عندك 2GB dead tuples. اللي بيحصل فعلاً: الجدول بيوصل لـ 3-4 جيجا bloat قبل ما autovacuum يقرر يبدأ، وساعتها الـ vacuum نفسه بياخد ساعات.
الحل: ضبط الإعدادات لكل جدول حسب حجمه ومعدل التعديلات عليه:
ALTER TABLE users SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_vacuum_threshold = 1000,
autovacuum_vacuum_cost_limit = 2000,
autovacuum_vacuum_cost_delay = 10
);التعديل ده بيخلّي autovacuum يشتغل على الجدول لما dead tuples تبقى أكتر من 5% من الحجم + 1000 صف، ومع زيادة cost_limit الـ vacuum نفسه بيخلص أسرع. لو عندك 20 جدول كبير، اعمل الضبط ده لكل واحد فيهم بقيم مختلفة بناءً على معدل الـ UPDATE/DELETE.
الحل الثاني: لمّا يكون فات الأوان — pg_repack
لو الجدول bloated خلاص (40%+)، autovacuum مش هيرجّعك للحجم الأصلي — هو بيمنع زيادة الـ bloat بس مش بيضغط الجدول. علشان ترجّع المساحة فعليًا، عندك خيارين:
- VACUUM FULL: بيقفل الجدول كله بـ
ACCESS EXCLUSIVElock طول مدة العملية. ممنوع نهائيًا في إنتاج فيه load. على جدول 12GB ممكن ياخد 20-40 دقيقة، الجدول مقفول طول الوقت ده. - pg_repack: extension بيعمل نفس النتيجة بدون lock طويل. الجدول بيفضل يقبل reads/writes تقريبًا طول الوقت، وبس في النهاية بياخد lock قصير لفترة ثواني علشان يبدّل الجدول القديم بالجديد.
# Ubuntu/Debian
sudo apt install postgresql-15-repack
# تشغيل على جدول واحد
sudo -u postgres pg_repack -d mydb -t public.users --jobs 4
# تشغيل على كل الـ public schema
sudo -u postgres pg_repack -d mydb -s public --jobs 4على جدول 12GB، pg_repack بياخد حوالي 8-15 دقيقة، وبيستخدم disk مؤقت يساوي حجم الجدول الأصلي. لو قرصك ممتلئ 80%+ خلاص، pg_repack مش هيشتغل.
أرقام من حالة إنتاج حقيقية
على قاعدة بيانات SaaS بحجم 240GB، 18 جدول رئيسي، 12 منهم بـ UPDATE/DELETE كثيف:
- قبل: dead_pct متوسط = 32%، استهلاك القرص = 240GB، autovacuum بياخد 6 ساعات للجدول الأكبر، p95 على القراءة 380ms.
- بعد ضبط scale_factor + pg_repack مرة واحدة: dead_pct متوسط = 6%، استهلاك القرص = 142GB (تقليل 40%)، autovacuum بياخد 14 دقيقة، p95 على القراءة 95ms.
الأرقام دي مش نظرية — مأخوذة من pg_stat_user_tables وpg_stat_statements قبل وبعد العملية بأسبوع. الفرق في الأداء جاي من حاجتين: حجم الـ heap اللي بيتقرى أقل، والـ shared_buffers بقت تستوعب نسبة أكبر من الجدول الفعلي.
الـ trade-offs اللي لازم تعرفها
تخفيض autovacuum_vacuum_scale_factor بيخلي autovacuum يشتغل أكتر، يعني I/O زيادة على القرص. لو السيرفر CPU/IO مضغوطين أصلًا، vacuum أكتر هيزوّد المشاكل قبل ما يحلها. الحل: ضبط autovacuum_vacuum_cost_limit أعلى مع cost_delay أقل، علشان كل دورة vacuum تخلص أسرع وتفضي الـ I/O للـ workload الفعلي.
pg_repack محتاج قرص فاضي يساوي حجم الجدول الأصلي على الأقل، زائد حجم الـ indexes. لو قرصك مليان، شغّله جدول جدول وامسح الـ WAL القديم بين كل run. كمان pg_repack محتاج primary key أو unique index على الجدول — جداول بدون مفاتيح فريدة مش هيشتغل عليها.
متى لا تستخدم هذه الطريقة
لو الجدول صغير (أقل من 1GB) و dead_pct أقل من 15%، سيب autovacuum الافتراضي شغّال ومتعمّلش الإعدادات اليدوية — هتزود تعقيد بدون فايدة. الجداول الـ append-only (logs, events, audit_trail) مش محتاجة الضبط ده مطلقًا — مفيش UPDATE فيها أصلاً، وautovacuum الافتراضي كافي يحافظ على visibility map.
كمان، لو عندك logical replication بـ slots نشطة، VACUUM ممكن يأثر على slot lag، خصوصًا لو في slot واقف. اتأكد من pg_replication_slots.active = true قبل ما تشغّل pg_repack، واختبر الـ workflow كامل على staging الأول. الافتراض إن عندك monitoring على slot lag — لو مفيش، ابدأ به قبل أي حاجة.
المصادر
- PostgreSQL Documentation — Routine Vacuuming
- PostgreSQL Documentation — pgstattuple module
- pg_repack official documentation
- PostgreSQL Wiki — Show database bloat
- PostgreSQL Documentation — Automatic Vacuuming Config
الخطوة التالية
افتح psql على الإنتاج بتاعك دلوقتي وشغّل pgstattuple على أكبر 5 جداول. لو لقيت dead_pct أكبر من 20% على جدول واحد، اضبط autovacuum_vacuum_scale_factor = 0.05 وراقب الحجم لمدة 7 أيام عبر pg_total_relation_size. لو الحجم ما نزلش، خطّط لـ pg_repack في maintenance window. ابعتلي dead_tuple_percent قبل وبعد لو حابب تشاركني النتيجة.