الحذف القابل للتوسع الوحيد في Postgres هو DROP TABLE
Tom Pang | 11 يونيو 2026
على نحو غير بديهي، تُضيف عمليات DELETE الكبيرة عبئًا على قاعدة البيانات.
من خلال الخبرة يمكننا القول بوضوح ما يلي: أكثر استراتيجيات حذف البيانات في Postgres قابلية للتوسع تدور حول حذف الجداول بالكامل.
حذف الصفوف الفردية باستخدام DELETE يكون مناسبًا على نطاق صغير. ومع ذلك، فإن عمليات DELETE الضخمة على دفعات لا تُفرغ مساحة القرص الفعلية على الفور، وتضيف عبئًا على الكتابة والنسخ المتماثل، وفي النهاية ليست جيدة لتنظيف الصفوف على نطاق واسع.
إذا كان تطبيقك يحتاج إلى حذف كميات كبيرة من البيانات، حتى وإن كان ذلك نادرًا جدًا، نوصي بالانتقال إلى تصاميم مخطط تسمح لك بالتعبير عن ذلك كـ DROP TABLE أو TRUNCATE.
دعونا ندرس السبب من خلال النظر في طريقة عمل DELETE في Postgres.
Deletes hurt
عند تعديل الصفوف، يمكن لـ Postgres الحفاظ على إصدارات متعددة من نفس الصف، بحيث يمكن للمعاملات المختلفة رؤية قيم الصفوف كما كانت في وقت الاستعلام. هذا هو تنفيذ Postgres لمفهوم “التحكم المتعدد النسخ المتزامن” (Multi-Version Concurrency Control) (MVCC) ومبدأ أساسي في تصميمه.
يقوم Postgres بعمل مقايضة مقصودة هنا. فهو يخزن الصفوف المعدلة والمحذوفة جنبًا إلى جنب مع الصفوف الحالية، معتمدًا على معرفات المعاملات وخرائط الرؤية لتجاوز “الصفوف الميتة”. لاحقًا، يأتي عملية vacuum وتقول: “هيه، هذه البايتات في صفحة الـ heap هذه أصبحت الآن حرة، يمكنك الكتابة فوقها.”
كما يجب أن تُنسخ عمليات الحذف بالكامل؛ فهي لا تزال عمل كتابة، مما يعني أن عمليات DELETE الكبيرة يمكن أن تؤثر على كُتاب آخرين في تطبيقك وتُجبرهم على الانتظار حتى يكتمل نسخ DELETE (تحت النسخ المتماثل المتزامن وشبه المتزامن).
من الجدير بالذكر هنا أن DELETE أو حتى autovacuum لا يعيدان عادةً البيانات إلى نظام التشغيل؛ بل يكتفيان بالقول «يمكن الكتابة فوق المساحة في تلك الصفحات». هذا اختيار متعمد من قبل Postgres. فهو يُحسّن للحالة التي تُخلط فيها أحمال عمل DELETE مع أحمال INSERT، وإطلاق المساحة إلى نظام التشغيل ثم طلب استعادتها مرة أخرى يُعد مكلفًا نسبيًا ويجب تجنبه. يسمح VACUUM FULL بذلك، لكنه يأخذ قفلًا مكلفًا لفترة طويلة.
توازن آخر مرتبط يتبناه Postgres هو أن بيانات الفهرس لا تُلمَس على الإطلاق عند إصدار DELETE؛ بل يتعين على القارئ الذي يقرأ الفهرس أن يقرر «هل هذا الصف ميت». هناك أيضًا تحسين بأفضل جهد حيث يمكن لعملية مسح الفهرس التي تجد صفًا ميتًا أن تُعلِّم الإدخال بأنه ميت بنفسه.
بشكل عام، DELETE هو في الحقيقة «عمل مضاف»، وليس «عملًا مُنجزًا». إذا أردت مزيدًا من التفاصيل حول MVCC في Postgres، راجع Keeping a Postgres queue healthy.
إذا كنت تُنفّذ DELETE على كمية كبيرة من البيانات، يمكنك تخيل كيف يضيف ذلك عملًا إلى كل استعلام قراءة وautovacuum. كن على علم بأن استخدام المفاتيح الأجنبية وCASCADE للحذف قد يتسبب في حذف صف واحد ليفرغ جيجابايتات من البيانات، مما يؤدي إلى نفس مجموعة المشكلات.
Drop DELETE for DROP
على النقيض، يتطلب DROP TABLE وTRUNCATE قفلًا ثقيلًا من نوع AccessExclusiveLock على الجدول، لكنهما مستقلان إلى حد ما عن حجم البيانات. على المستوى الفيزيائي، يقومان بإزالة الملفات من نظام التشغيل مباشرة، بالإضافة إلى مسح ذاكرة التخزين المؤقت لـ Postgres لإزالة الصفحات المتعلقة بالجدول.
ذلك المسح قد يكون أقل بساطةً في قواعد البيانات ذات المخازن المشتركة الكبيرة، لكنه مجرد مسح للبيانات الوصفية. يحتفظ Postgres برأس صغير ثابت الحجم (BufferDesc، مُحَشَّى إلى 64 بايت) لكل مخزن بحجم 8KB، وعند حذف جدول يتم فحص تلك الرؤوس، وليس الصفحات نفسها. عند 64 بايت لكل صفحة 8KB، فهذا يمثل 1/128 من حجم الذاكرة المؤقتة: مع 128 GB من المخازن المشتركة، فإنك تمسح نحو ~1 GB من الذاكرة فقط، بشكل متسلسل، وهو أمر سريع جداً على الأجهزة الحديثة.
DROP TABLE و TRUNCATE يتوسّعان بشكل أفضل بكثير من DELETE. فهما لا ينتجان أي صفوف ميتة، ولا ديون تفريغ، ولا عمل للقراء. يحرران المساحة للنظام التشغيلي فوراً.
حذف أحادي الأداء
حالة شائعة يحتاج فيها الأشخاص إلى حذف كميات كبيرة من البيانات هي “جدولي مليء بالقمامة بسبب خطأ”. صادفنا ذلك مؤخرًا في أداة مراقبة داخلية. تسبب خطأ في كتابة الأداة لملايين الصفوف التي أردنا حذفها من قاعدة البيانات. كانت الصفوف السيئة تحمل طابعًا زمنيًا updated_at قديمًا؛ أي شيء بطابع حديث كان من المفترض الاحتفاظ به. كان هناك فقط بضع مئات آلاف من الصفوف للاحتفاظ بها؛ معظم البيانات كانت قُمامة.
في هذه الحالة، خاصةً لأن “قفل قاعدة البيانات لدقائق” لم يكن مشكلة على الإطلاق، قمنا ببعض الجراحة، مستندين إلى DDL المعاملية في Postgres:
BEGINLOCK TABLE... IN ACCESS EXCLUSIVE MODEصريح على الجدول المعني؛ هذا يمنع المعاملات الأخرى من قراءة أو كتابة الجدول، وبالتالي نحصل على بيانات متسقة.- إنشاء جدول مؤقت للاحتفاظ بالبيانات التي سيتم الاحتفاظ بها فقط، كما يلي:
CREATE TEMP TABLE temp_keep_big_table AS
SELECT * FROM big_table
WHERE updated_at >= '2026-04-01';
TRUNCATE big_table;INSERT INTO big_table SELECT * FROM temp_keep_big_table;. في مثالنا، استغرق ذلك بضع دقائق فقط للمعالجة على نسخة صغيرة جدًا تحتوي على مئات الآلاف من الصفوف.
عمل هذا بشكل جيد جدًا لمرة واحدة؛ فالبيانات الوحيدة المكتوبة إلى سجل الكتابة المسبق (Write Ahead Log أو WAL) هي الصفوف التي أُعيد إدراجها في big_table.
إذا كان الاحتفاظ بـ AccessExclusiveLock على الجدول لعدة دقائق أثناء تنفيذ TRUNCATE غير مقبول، فاستعن بنهج يعتمد على المشغلات (trigger): قم بإنشاء نسخة من الكتابات في جدول جديد، ثم استبدله بعملية إعادة تسمية ذرية (atomic rename).
يجب أن تعلم أيضًا أن هذه التقنية المتقدمة تقريبًا هي ما يفعله امتداد Postgres pg_squeeze (نسخة أكثر حداثة من pg_repack). يُستخدم pg_squeeze لتحسين الجداول التي تعاني بالفعل من تضخم كبير. هذه المقالة تتناول في الأساس منع التضخم من البداية. من خلال هيكلة المخطط لتجنب عمليات DELETE الضخمة، يصبح pg_squeeze أقل ضرورة.
في الحالات التي تكون فيها البيانات التي يجب الاحتفاظ بها أكبر بكثير من البيانات التي يجب حذفها، ولكن لا يزال حجم البيانات التي تُحذف كبيرًا، فإن النهج الشائع هو تنفيذ العديد من عمليات الحذف المجمعة المعزولة داخل حلقة، على سبيل المثال 10,000 صف في كل مرة. يحافظ هذا على قصر المعاملات، ويتجنب تراكم الأقفال، ويسمح لك بتنظيم العملية بحيث يواكب autovacuum العملية.
Postgres partitions for ongoing deletes
منذ الإصدار 10، توفر Postgres دعمًا ممتازًا للتقسيم. يمكن أن يحتوي جدول “أب” (parent) على جداول “أبناء” (child)، ويمكن توجيه الاستعلامات تلقائيًا إليها. تدعم Postgres مجموعة متنوعة من مخططات التقسيم؛ أحد أكثرها فائدة هو التقسيم القائم على التاريخ، لكن هناك العديد من الأنواع الأخرى المتاحة.
تقسيم الجداول يمكن أن يحول عبء عمل يقوم بـ “الكثير من DELETE” إلى عبء عمل يقوم بـ “حذف DROP TABLE” بين الحين والآخر. على سبيل المثال، إذا كان لديك بيانات تاريخية تحتاج إلى إزالتها مع مرور الوقت، يمكنك إنشاء قسم فرعي لكل يوم، وعملية دورية تحذف الأقسام الفرعية الأقدم (أو استخدام امتداد pg_partman).
يمكنك الذهاب إلى أبعد من ذلك. التقسيم في Postgres متكرر، لذا يمكنك تقسيم المستوى الأعلى باستخدام LIST (مثلاً، قسم الصفوف “المرئية”)، ثم تقسيم جدول الطفل “غير المرئي” باستخدام RANGE لإزالة البيانات القديمة.
تقدم وDROP
هيكلة المخطط والتطبيق بحيث يتحول DELETE على نطاق واسع إلى DROP أو TRUNCATE يمكن أن يحسن قاعدة البيانات بشكل كبير. يساعد ذلك على تقليل زمن استجابة استعلامات القراءة في بعض الحالات، ويخفف من spikes تأخر النسخ المتماثل، ويحسن بشكل عام من صحة قاعدة البيانات.
فريق التحرير
مساهم في اختيار المواد وترجمتها وتحريرها للقارئ العربي.