إعادة تسمية الأعمدة، تقسيم الجداول، وتغيير القيود بدون توقف الخدمة
لديك جدول users يحتوي على عمود باسم full_name. يقرر الفريق تغيير الاسم إلى display_name. إذا قمت بإعادة التسمية مباشرة، فإن كل تطبيق لا يزال يقرأ full_name سيتعطل فور تطبيق التغيير في الإنتاج. سيختفي العمود، وستفشل الاستعلامات، وسيرى المستخدمون أخطاء.
هذه ليست مشكلة افتراضية. الفرق تعيد تسمية الأعمدة، وتقسم الجداول إلى اثنين، وتغير القيود في كل سباق تطوير. الطريقة الساذجة - تغيير المخطط (schema) ثم إصلاح الكود لاحقًا - تسبب حوادث إنتاج كان يمكن تجنبها. الحل هو نمط يسمى التوسيع والانكماش (expand-contract)، وهو يعمل لجميع السيناريوهات الثلاثة.
الفكرة الأساسية: أضف أولاً، حوّل تدريجياً، أزل أخيراً
يتكون نمط التوسيع والانكماش من ثلاث مراحل. أولاً، تقوم بتوسيع المخطط بإضافة هياكل جديدة بجانب الهياكل القديمة. ثم تقوم بترحيل التطبيقات والبيانات لاستخدام الهياكل الجديدة. أخيراً، تقوم بالانكماش بإزالة الهياكل القديمة بعد أن لا يعتمد عليها أي شيء.
يوضح الرسم البياني أدناه نمط التوسيع والانكماش ثلاثي المراحل وكيفية تطبيقه على إعادة تسمية عمود، وتقسيم جدول، وتغيير قيد.
الفكرة الرئيسية هي أنك لا تقوم أبدًا بتغيير مؤثر (breaking change) في خطوة واحدة. أنت دائمًا تبقي المسار القديم يعمل حتى يتم اعتماد المسار الجديد بالكامل. هذا يعني عدم وجود توقف للخدمة أثناء تغييرات المخطط، طالما أنك تتبع التسلسل بشكل صحيح.
إعادة تسمية عمود دون كسر أي شيء
دعنا نستعرض عملية إعادة تسمية full_name إلى display_name. في مرحلة التوسيع، تقوم بإضافة عمود جديد display_name إلى جدول users. لا تقم بحذف full_name. يبدأ الإصدار الجديد من تطبيقك بالكتابة في كلا العمودين. كل عملية إدراج أو تحديث تكتب نفس القيمة في full_name للمستهلكين القدامى وفي display_name للمستهلكين الجدد.
فيما يلي أوامر SQL لكل مرحلة من مراحل إعادة التسمية:
-- المرحلة 1: التوسيع - إضافة العمود الجديد
ALTER TABLE users ADD COLUMN display_name VARCHAR(255);
-- مثال على الكتابة المزدوجة (منطق التطبيق، ليس SQL فقط)
-- عند إدراج أو تحديث مستخدم، اكتب في كلا العمودين:
INSERT INTO users (full_name, display_name) VALUES ('Alice', 'Alice');
UPDATE users SET full_name = 'Bob', display_name = 'Bob' WHERE id = 42;
-- المرحلة 2: الترحيل - ملء البيانات الحالية بأثر رجعي
UPDATE users SET display_name = full_name WHERE display_name IS NULL;
-- المرحلة 3: الانكماش - حذف العمود القديم
ALTER TABLE users DROP COLUMN full_name;
يضمن هذا التسلسل أنه في أي لحظة، لا ترفض قاعدة البيانات استعلامًا أو تفقد بيانات.
بعد وجود العمود وكتابة التطبيق في كليهما، تقوم بعملية الملء بأثر رجعي (backfill). هذه عملية دفعة تنسخ جميع القيم الموجودة من full_name إلى display_name لكل صف. تتحقق من تطابق الأعداد وتفحص عشوائيًا بعض السجلات للتأكد من عدم فقدان أي شيء.
الآن يأتي دور التبديل. يجب تحديث جميع التطبيقات التي تقرأ أسماء المستخدمين للقراءة من display_name بدلاً من full_name. يمكن أن يحدث هذا تدريجيًا. بعض الخدمات تتحول أولاً، وأخرى تتبعها. خلال هذه الفترة، يظل كلا العمودين ممتلئين، لذا فإن أي خدمة لا تزال تقرأ full_name ستظل تعمل.
بمجرد ترحيل كل تطبيق وكل استعلام، تدخل في مرحلة الانكماش. تقوم بحذف عمود full_name. تستغرق العملية بأكملها أيامًا أو أسابيع اعتمادًا على عدد الخدمات التي تحتاج إلى تحديث، ولكن لا توجد لحظة لا يستطيع فيها المستخدمون الوصول إلى التطبيق.
تقسيم جدول واحد إلى اثنين
هذا السيناريو أكثر تعقيدًا. تخيل أن جدول orders يخزن تفاصيل الطلب ومعلومات الدفع في نفس الصف. يريد الفريق فصل بيانات الدفع إلى جدول مخصص payments. لا يمكنك فقط إنشاء الجدول الجديد والتوقف عن الكتابة في القديم، لأن التطبيقات الحالية لا تزال تقرأ من orders.
مرحلة التوسيع تنشئ جدول payments. يبدأ الإصدار الجديد من التطبيق بكتابة بيانات الدفع في كلا المكانين. في كل مرة يتم فيها إنشاء طلب أو تحديثه، يكتب التطبيق تفاصيل الدفع في جدول orders للمستهلكين القدامى وفي جدول payments للهيكل الجديد. هذا يسمى الكتابة المزدوجة (dual-write)، وهو الجزء الأصعب لتنفيذه بشكل صحيح. يجب أن تنجح كلتا العمليتين أو يتم التراجع عنهما معًا. الكتابة الجزئية ستؤدي إلى فساد بياناتك.
الملء بأثر رجعي (backfill) أمر بالغ الأهمية هنا. تحتاج إلى نسخ جميع بيانات الدفع الحالية من orders إلى payments. قم بتشغيل هذا على دفعات لتجنب قفل الجدول لفترة طويلة. بعد كل دفعة، تحقق من تطابق عدد السجلات وإجمالي مبالغ الدفع بين الجدولين. إذا لم يتطابقا، توقف وحقق قبل الاستمرار.
بمجرد التحقق من الملء بأثر رجعي وتحديث جميع التطبيقات لقراءة بيانات الدفع من payments بدلاً من orders، تدخل في مرحلة الانكماش. تقوم بإزالة أعمدة الدفع من orders. تتطلب هذه الخطوة ثقة بأنه لا يوجد استعلام أو تقرير أو خدمة قديمة لا تزال تصل إلى تلك الأعمدة. تحقق من سجلات قاعدة البيانات، وسجلات التطبيق، ومراقبة الاستعلامات قبل حذف أي شيء.
تغيير قيد قابلية القبول (Nullable) إلى غير قابل للقبول (Not Null)
هذا السيناريو يبدو بسيطًا لكنه غالبًا ما يفاجئ الفرق. جدول users لديه عمود email يسمح بقيم فارغة (null). تتطلب الشركة الآن أن يكون لكل مستخدم بريد إلكتروني. إذا قمت بتغيير العمود مباشرة إلى NOT NULL، سترفض قاعدة البيانات التغيير لأن الصفوف الحالية التي تحتوي على بريد إلكتروني فارغ تنتهك القيد.
مرحلة التوسيع هنا لا تضيف عمودًا جديدًا بالمعنى التقليدي. النهج الشائع هو إضافة عمود جديد email_not_null يعكس email ولكن مع قيد عدم القابلية للقبول. يكتب الإصدار الجديد من التطبيق في كلا العمودين. بالنسبة للإدراج، يحصل كلا العمودين على نفس القيمة. بالنسبة للتحديثات، يتم تحديث كليهما.
الملء بأثر رجعي هو الخطوة الحاسمة. يجب إصلاح كل صف يحتوي على بريد إلكتروني فارغ. تحتاج إما إلى توفير قيمة افتراضية، أو التنسيق مع المستخدمين لملء بريدهم الإلكتروني، أو العمل مع فرق أخرى لتوفير البيانات المفقودة. هذه ليست مشكلة تقنية فقط. إنها مشكلة جودة بيانات وتنظيمية. يجب أن يسجل سكريبت الملء بأثر رجعي كل صف لا يمكنه إصلاحه وينبه الفريق للتعامل مع هذه الحالات يدويًا.
بعد أن تحتوي جميع الصفوف على عناوين بريد إلكتروني صالحة وتقرأ جميع التطبيقات من email_not_null، تقوم بالانكماش بحذف عمود email القديم. إذا كنت ترغب في الاحتفاظ باسم العمود الأصلي، يمكنك إعادة تسمية email_not_null إلى email بعد اختفاء العمود القديم.
قائمة تحقق عملية لتغييرات المخطط
قبل تشغيل أي ترحيل للمخطط في الإنتاج، اتبع قائمة التحقق هذه:
- هل يمكن للمخطط القديم الاستمرار في خدمة الطلبات بعد التغيير؟
- هل يوجد مسار كتابة مزدوجة لجميع البيانات الجديدة؟
- هل تم اختبار سكريبت الملء بأثر رجعي على نسخة من بيانات الإنتاج؟
- هل تحققت من ترحيل جميع المستهلكين قبل حذف أي شيء؟
- هل لديك خطة للتراجع إذا كشفت مرحلة الانكماش عن تبعية مفقودة؟
الخلاصة
يجب أن يتبع كل تغيير في المخطط تقوم به في الإنتاج نفس التسلسل: أضف الهيكل الجديد، رحّل البيانات والتطبيقات تدريجيًا، وأزل الهيكل القديم فقط عندما لا يعتمد عليه أي شيء. سواء كنت تعيد تسمية عمود، أو تقسم جدولًا، أو تشدد قيدًا، فإن النمط هو نفسه. الثمن هو الصبر والتنسيق الدقيق. المكافأة هي عدم وجود توقف للخدمة وعدم وجود استعلامات مكسورة.