كيف أدى عمود غير مفهرس إلى انهيار قاعدة بياناتنا

فهرس واحد مفقود حوّل واجهة برمجة تطبيقات (API) تستغرق 8 مللي ثانية إلى كابوس يستغرق 8 ثوانٍ.

أجرينا اختبار حمل (load test) قبل إصدار رئيسي. كان التطوير المحلي يعمل بشكل جيد مع 100 سجل. ثم قمنا بمحاكاة 500 مستخدم مقابل 1.5 مليون صف من البيانات بحجم بيئة الإنتاج.

كانت النتائج سيئة:

  • قفزت أوقات استجابة الـ API من 45 مللي ثانية إلى 8,000 مللي ثانية.
  • وصل استهلاك المعالج (CPU) لقاعدة البيانات إلى 100%.
  • استُنفدت مجموعات الاتصال (connection pools)، مما تسبب في أخطاء انتهاء المهلة (timeout errors).

فحصنا سجل الاستعلامات البطيئة (slow-query log) ووجدنا نقطة نهاية (endpoint) تجلب سجل الطلبات بناءً على معرف المستخدم (user ID) والحالة (status).

أظهر تشغيل EXPLAIN ANALYZE على PostgreSQL وجود مسح تسلسلي (Sequential Scan). قرأ المحرك جميع الـ 1.5 مليون صف لكل طلب لعدم وجود فهرس على user_id.

مع وجود 100 طلب متزامن، قام بمسح 150 مليون صف في وقت واحد.

استغرق الإصلاح خمس دقائق.

بدلاً من فهرس بسيط على user_id ، أنشأنا فهرساً مركباً (composite index) على (user_id, status, created_at DESC). سمح ذلك لقاعدة البيانات بـ:

  • التصفية حسب user_id.
  • التصفية حسب status.
  • إرجاع أحدث الصفوف فوراً.
  • تخطي خطوات الفرز الإضافية.

استخدمنا CREATE INDEX CONCURRENTLY لتبقى الجداول غير مقفلة أثناء العملية.

بعد الإصلاح:

  • انخفض وقت الاستعلام من 8,150 مللي ثانية إلى 0.14 مللي ثانية.
  • انخفض زمن استجابة الـ API من 8 ثوانٍ إلى 12 مللي ثانية.
  • انخفض استهلاك المعالج من 100% إلى أقل من 8%.

الدروس المستفادة:

  • الاختبار المحلي مضلل؛ فـ 100 صف لا تمثل مليون صف.
  • قم بفهرسة المفاتيح الخارجية (foreign keys) — فمعظم الـ ORMs تتجاهل ذلك.
  • قم بتشغيل EXPLAIN ANALYZE قبل شراء المزيد من الأجهزة.
  • صمم الفهارس بناءً على الاستعلامات التي تقوم بتشغيلها فعلياً.

لا تقم بتوسيع الخوادم أولاً. قم بتوسيع نطاق الاستعلامات.

المصدر: https://dev.to/mia_keller_ffd2584c046ecb/how-an-unindexed-column-silently-killed-our-database-under-load-and-the-5-minute-fix-m32