المستوى المطلوب: متوسط. المقال ده موجّه لمهندس عنده أساس في SQL وبيتعامل مع قاعدة بيانات فيها بيانات حقيقية على الإنتاج. لو لسه بتتعلّم، متقلقش: كل فكرة صعبة هنشرحها بمثال بسيط الأول، وبعدين نرجع نحطّها في تعريف دقيق.
تقدر تغيّر عمودًا في جدول فيه 50 مليون صف والموقع شغّال، من غير ثانية توقف واحدة. الشرط إنك متعملش التغيير في خطوة واحدة. الطريقة اسمها Expand-Contract، وهي المعيار في الفرق اللي بتنشر كل يوم.
الترحيل بدون توقف: نمط Expand-Contract للتعديل على جداول عليها ملايين الصفوف
الافتراض إنك على PostgreSQL 11 أو أحدث، وعندك جدول فيه ملايين الصفوف، وحركة كتابة مستمرة عليه. لو جدولك صغير (أقل من 100 ألف صف) الموضوع كله مش هيفرق معاك، وممكن تتجاهل المقال ده وتعمل ALTER عادي.
المشكلة باختصار
عايز تحوّل عمود phone النصّي الفوضوي لعمود منظّم phone_e164 بصيغة موحّدة. الطريقة اللي في بالك: تعمل ALTER TABLE، تنسخ البيانات، وتعدّل الكود. الطريقة دي بتفشل تحت الضغط، لأن بعض أوامر ALTER بتقفل الجدول كله لثوانٍ أو دقائق، وفي الوقت ده كل طلب كتابة أو قراءة بيقف في الطابور. النتيجة: أخطاء timeout ومستخدمين متعلّقين.
ليه ALTER المباشر بيوقّع الموقع؟ (المفهوم بمثال ثم علميًا)
المثال: تخيّل مطعم شغّال ومليان زباين، وعايز تغيّر محطة التقطيع في المطبخ. لو وقفت المطبخ كله عشان تفكّها وتركّب واحدة جديدة، كل الطلبات هتقف. الطريقة الذكية: تبني المحطة الجديدة جنب القديمة، تشغّل الاتنين مع بعض شوية، تنقل الطبخ بالتدريج، وبعد ما تتأكد إن الجديدة شغّالة، تفكّ القديمة في وقت فاضي. المطعم مقفلش لحظة.
علميًا: أوامر زي ALTER TABLE ... ALTER COLUMN TYPE أو إضافة عمود بقيمة افتراضية متغيّرة بتاخد قفل ACCESS EXCLUSIVE، وده أقوى قفل في PostgreSQL: بيمنع أي قراءة أو كتابة على الجدول لحد ما العملية تخلص. على 50 مليون صف ممكن ده يفضل دقايق. نمط Expand-Contract بيفصل تغيير المخطط عن تغيير الكود، فتفضل النسخة القديمة والجديدة شغّالين مع بعض، وميحصلش قفل طويل في أي لحظة.
الحل: Expand ثم Backfill ثم Contract
الفكرة إنك تخلّي العمودين موجودين مع بعض فترة، فأي نشر للكود ميحتاجش المخطط القديم والجديد يتغيّروا في نفس اللحظة.
- Expand: ضيف العمود الجديد فاضي (nullable). ده بياخد قفل لجزء من الثانية بس.
- خلّي التطبيق يكتب في العمودين مع بعض (dual-write) في أي صف جديد أو معدّل.
- Backfill: انسخ البيانات القديمة على دفعات صغيرة، مش دفعة واحدة.
- Contract: بعد ما تتأكد إن العمود الجديد مكتمل، حوّل التطبيق يقرا منه بس، وبعدها احذف العمود القديم.
أول خطوة، الـ Expand، وهي رخيصة:
-- إضافة عمود nullable = قفل لجزء من الثانية فقط
ALTER TABLE users ADD COLUMN phone_e164 text;الخطوة الحسّاسة هي الـ Backfill. متعملش UPDATE واحد على 50 مليون صف، لأنه هيقفل صفوف كتير ويكبّر الـ WAL بشكل عنيف. اعملها على دفعات:
#!/usr/bin/env bash
# نسخ على دفعات 5000 صف لحد ما ميفضلش صف ناقص
while true; do
n=$(psql -tAqc "
WITH batch AS (
SELECT id FROM users
WHERE phone_e164 IS NULL
ORDER BY id
LIMIT 5000
)
UPDATE users u
SET phone_e164 = '+2' || regexp_replace(u.phone, '\D', '', 'g')
FROM batch b
WHERE u.id = b.id;
SELECT COUNT(*) FROM users WHERE phone_e164 IS NULL;")
echo "متبقّي: $n"
[ "$n" -eq 0 ] && break
sleep 0.2 # نفس صغير للـ replicas وتقليل الضغط على الـ I/O
doneوأخيرًا الـ Contract، بعد التأكد ونشر الكود اللي بيقرا من العمود الجديد:
-- بعد ما التطبيق بقى يعتمد على phone_e164 بالكامل
ALTER TABLE users DROP COLUMN phone;لو محتاج فهرس على العمود الجديد، اعمله بـ CREATE INDEX CONCURRENTLY عشان ميقفلش الجدول أثناء البناء.
سيناريو واقعي بأرقام
جدول users فيه 50 مليون صف، وعليه حوالي 2000 كتابة/ثانية وقت الذروة. الطريقة المباشرة (ALTER يعيد كتابة الجدول) قاست قفلًا حوالي 4 دقايق على بيئة مشابهة، يعني 4 دقايق أخطاء للمستخدمين. بالـ Backfill على دفعات 5000: طلع 10 آلاف دفعة، كل دفعة ~30 مللي ثانية، الإجمالي حوالي ساعة ونص من الشغل في الخلفية بدون أي قفل يوقّف الترافيك. أطول قفل في العملية كلها كان الـ ADD COLUMN: أقل من 10 مللي ثانية.
الـ trade-offs
- بتكسب: صفر توقف. بتخسر: تعقيد مؤقت في الكود، لأنك بتكتب في عمودين فترة (dual-write) لحد ما تخلص الـ Contract.
- الـ Backfill على دفعات أبطأ من
UPDATEواحد (ساعة ونص مقابل دقايق)، بس مقابل إنه ميقفلش حاجة. المقايضة: زمن أطول في مقابل توفّر مستمر. - بتفضل تدير مخطط فيه عمودين وبيانات مكرّرة فترة، وده استهلاك مساحة زيادة مؤقت لحد الحذف.
متى لا تستخدم هذه الطريقة
لو الجدول صغير (أقل من 100 ألف صف)، القفل هيبقى أقصر من رمشة عين، فالتعقيد ده مضيعة وقت؛ اعمل ALTER عادي في نافذة صيانة قصيرة. وكمان في PostgreSQL 11+، إضافة عمود بقيمة افتراضية ثابتة بقت عملية فورية (تغيير في الميتاداتا بس)، فمش كل إضافة عمود محتاجة الرقصة دي. اعرف نوع الأمر بالظبط قبل ما تفترض إنه هيقفل.
الخطوة التالية
قبل أي ترحيل على الإنتاج، شغّل الأمر على نسخة staging وقيس القفل الفعلي بأمر SELECT * FROM pg_locks WHERE NOT granted; في جلسة تانية أثناء التنفيذ. لو شفت قفل AccessExclusiveLock على جدولك مستني، ده معناه إنك على وشك توقّف الترافيك، وابدأ قسّم العملية لمراحل زي فوق.
المصادر
- PostgreSQL Documentation — ALTER TABLE: postgresql.org/docs/current/sql-altertable.html
- PostgreSQL Documentation — Explicit Locking (مستويات الأقفال): postgresql.org/docs/current/explicit-locking.html
- PostgreSQL 11 Release Notes — إضافة عمود بقيمة افتراضية بشكل فوري: postgresql.org/docs/release/11.0
- PostgreSQL Documentation — CREATE INDEX CONCURRENTLY: postgresql.org/docs/current/sql-createindex.html
- Martin Fowler — ParallelChange (الاسم الأكاديمي لنمط Expand-Contract): martinfowler.com/bliki/ParallelChange.html
- GitLab — Migration Style Guide (ممارسات ترحيل بدون توقف): docs.gitlab.com/ee/development/migration_style_guide.html