عندما يتسبب إضافة فهرس في تجميد تطبيقك

فريقك يتابع استعلامًا بطيئًا منذ أسابيع. جدول users نما ليحتوي على ملايين الصفوف، والبحث عن طريق البريد الإلكتروني يستغرق الآن ثوانٍ بدلاً من ميلي ثانية. يقترح أحدهم إضافة فهرس. يبدو الإصلاح بسيطًا: أمر CREATE INDEX واحد، والمشكلة محلولة.

تقوم بتشغيل الترحيل خلال ساعات العمل لأن الاستعلام يؤثر بالفعل على المستخدمين. يبدأ بناء الفهرس. ثم تبدأ التنبيهات في الظهور: انتهاء مهلات التطبيق، فشل المعاملات، تذاكر الدعم تتراكم. الفهرس الذي أضفته لتحسين الأداء تسبب في حادثة إنتاج.

هذا السيناريو يحدث أكثر مما تتوقع معظم الفرق. الفهارس والقيود تبدو كتغييرات بسيطة في المخطط، لكنها تحمل تكاليف خفية لا تظهر إلا تحت الحمل.

ماذا يحدث عند إنشاء فهرس

عند تشغيل CREATE INDEX idx_users_email ON users(email)، تحتاج قاعدة البيانات إلى قراءة كل صف في الجدول، بناء هيكل شجري متوازن، وكتابته على القرص. خلال هذه العملية، تقوم قاعدة البيانات عادةً بقفل الجدول لمنع تغييرات البيانات التي قد تفسد هيكل الفهرس.

نوع القفل يعتمد على قاعدة البيانات الخاصة بك. في PostgreSQL، CREATE INDEX العادي يحصل على SHARE LOCK. يمكن أن تستمر عمليات القراءة، لكن أي عملية كتابة - إدراج، تحديث، حذف - يجب أن تنتظر. في MySQL مع InnoDB، يختلف سلوك القفل حسب نوع العملية وإصدار محرك التخزين.

بالنسبة لجدول يخدم آلاف المستخدمين، حتى بضع ثوانٍ من قفل الكتابة يمكن أن تخلق تراكمًا. اتصالات التطبيق تصطف، المهلات تتتالى، وما كان من المفترض أن يكون تحسينًا للأداء يصبح حدث توقف.

الخيار المتزامن الذي تنساه معظم الفرق

معظم قواعد البيانات الرئيسية تقدم طريقة لبناء الفهارس دون حظر الكتابة. PostgreSQL لديه CREATE INDEX CONCURRENTLY. MySQL يدعم ALGORITHM=INPLACE و LOCK=NONE على الإصدارات المتوافقة.

-- PostgreSQL: بناء فهرس دون حظر الكتابة
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

-- MySQL: بناء فهرس دون حظر الكتابة (InnoDB)
ALTER TABLE users ADD INDEX idx_users_email (email), ALGORITHM=INPLACE, LOCK=NONE;

النهج المتزامن يستغرق وقتًا أطول. CREATE INDEX CONCURRENTLY في PostgreSQL يجب أن ينتظر حتى تنتهي المعاملات الجارية، ثم يقوم بتمريرات إضافية لالتقاط التغييرات التي حدثت أثناء البناء. لكن المقايضة واضحة: تطبيقك يستمر في العمل أثناء بناء الفهرس.

الخطأ هو اعتبار إنشاء الفهرس المتزامن كخيار اختياري أو متقدم. يجب أن يكون الافتراضي لأي ترحيل إنتاج على جدول نشط. اكتبه صراحة في نصوص الترحيل الخاصة بك. لا تعتمد على السلوك الافتراضي.

القيود أيضًا تقفل

القيود الخارجية (Foreign Key) وفريدة (Unique) تجلب مفاجآت القفل الخاصة بها. عند إضافة مفتاح خارجي، تتحقق قاعدة البيانات من أن جميع الصفوف الحالية تلبي العلاقة. هذا التحقق يقرأ الجدول بأكمله ويقفله أثناء العملية.

PostgreSQL يقدم مخرجًا عمليًا: NOT VALID. هذا يضيف القيد للبيانات الجديدة فقط، متجاوزًا التحقق من الصفوف الحالية. يمكنك بعد ذلك تشغيل VALIDATE CONSTRAINT بشكل منفصل، خلال ساعات خارج الذروة.

-- إضافة مفتاح خارجي دون التحقق من البيانات الحالية
ALTER TABLE orders ADD CONSTRAINT fk_user_id
    FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;

-- التحقق لاحقًا، عندما تكون حركة المرور منخفضة
ALTER TABLE orders VALIDATE CONSTRAINT fk_user_id;

القيود الفريدة لها قصة مماثلة. إضافة UNIQUE على عمود يتطلب التحقق من التكرارات عبر الجدول بأكمله. على جدول كبير، يمكن لهذا المسح أن يقفل الكتابة لمدة غير مريحة.

خطط لترحيل الفهارس بشكل منفصل

العديد من الفرق تجمع كل تغييرات المخطط في ملف ترحيل واحد: إضافة عمود، إنشاء فهرس، إضافة مفتاح خارجي. هذا يعمل بشكل جيد على الجداول الصغيرة. على جداول الإنتاج التي تحتوي على ملايين الصفوف، يخلق هذا نقطة فشل واحدة.

تغييرات الفهارس والقيود تستحق ملفات الترحيل الخاصة بها، مجدولة بشكل منفصل عن تغييرات هيكل الجدول. بعض الفرق تذهب أبعد من ذلك وتقوم بتشغيل ترحيل الفهارس يدويًا، خارج خط الأنابيب الآلي، للحفاظ على تحكم دقيق في التوقيت.

نهج عملي:

  • قم بتشغيل تغييرات هيكل الجدول (إضافة عمود، تعديل نوع) في ترحيل واحد
  • قم بتشغيل تغييرات الفهارس والقيود في ترحيلات منفصلة
  • جدول ترحيلات الفهارس خلال نوافذ حركة المرور المنخفضة
  • استخدم الخيارات المتزامنة صراحة في كل نص ترحيل
  • تحقق من القيود بشكل منفصل عن إضافتها

السؤال الحقيقي

بمجرد وضع الفهارس والقيود في مكانها، تحتاج إلى التحقق من أنها لا تكسر التطبيق الجاري. فهرس يغير خطط الاستعلام يمكن أن يسبب تحولات غير متوقعة في الأداء. مفتاح خارجي يمنع حذف متسلسل يمكن أن يعطل المهام الخلفية.

هذا يقود إلى سؤال أوسع: كيف تحافظ على سلامة الترحيلات عندما تصل الإصدارات القديمة والجديدة من تطبيقك إلى نفس قاعدة البيانات؟ هذا هو موضوع النقاش التالي، لكن المبدأ يبدأ هنا: تعامل مع كل تغيير في المخطط كحدث إنتاج محتمل، وليس مجرد نص لتشغيله.

قائمة التحقق لترحيل الفهارس والقيود

  • استخدم إنشاء فهرس متزامن (CREATE INDEX CONCURRENTLY أو ما يعادله) على أي جدول به كتابات نشطة
  • أضف المفاتيح الخارجية مع NOT VALID عندما يكون ذلك ممكنًا، وتحقق منها بشكل منفصل
  • قم بتشغيل ترحيلات الفهارس في ملفات منفصلة عن تغييرات هيكل الجدول
  • جدول بناء الفهارس الكبيرة خلال نوافذ حركة المرور المنخفضة
  • راقب مدة القفل وأداء الاستعلام أثناء وبعد الترحيل
  • اختبر الترحيل على نسخة من بيانات الإنتاج قبل تشغيله فعليًا

الخلاصة

الفهرس ليس مجرد أداة أداء. إنه تغيير في المخطط يمكن أن يقفل جدولك، ويصطف كتاباتك، ويؤدي إلى انتهاء مهلة المستخدمين. تعامل معه بنفس الحذر الذي تتعامل به مع أي نشر إنتاجي. استخدم الخيارات المتزامنة، وافصل ترحيلاتك، وجدولها من أجل السلامة. سرعة الاستعلام التي تكسبها لا قيمة لها إذا كان الترحيل نفسه يكسر التطبيق.