يبدأ العمل على أداء قاعدة البيانات عادةً باقتراح من أحدهم بترقية الخادم إلى نسخة أكبر، وينتهي عادةً باكتشاف أن استعلاماً واحداً كان يجري مسحاً تسلسلياً على 4 ملايين صف في كل تحميل للصفحة. لم تكن العتاد يوماً هو القيد. كانت خطة التنفيذ هي القيد.
هذا النمط متكرر بما يكفي ليستحق أن يُصاغ كافتراض مبدئي. فحين يكون التطبيق بطيئاً وقاعدة البيانات مشغولة، يكون السبب في الغالب الأعم عدداً صغيراً من الاستعلامات المحددة لا نقصاً عاماً في الطاقة الاستيعابية، والترقية إلى جهاز أكبر تُخفي المشكلة بالضبط بمقدار الوقت الذي يحتاجه الجدول كي ينمو من جديد.
قِس قبل أن تغيّر أي شيء. تحسين استعلام خمّنته بالحدس هو الطريقة التي تُنفق بها الفرق أسبوعاً كاملاً في إضافة فهارس تُبطئ الكتابة ولا تُسرّع القراءة إطلاقاً. كل قاعدة بيانات قادرة على إخبارك بالتعليمات التي تستهلك أكبر وقت إجمالي. ابدأ من هناك، وعالج الأسوأ، ثم قِس من جديد. دورتان أو ثلاث من هذا النوع تُنهي الحادثة في العادة.
أداء قاعدة البيانات يبدأ بالعثور على الاستعلام
الوقت الإجمالي أهم من أسوأ حالة منفردة. استعلام يستغرق ثانيتين ويعمل مرتين في اليوم لا وزن له. أما استعلام يستغرق أربعين جزءاً من الألف من الثانية ويعمل 8000 مرة في الدقيقة فهو مشكلتك الحقيقية، ولن يظهر أبداً في سجل الاستعلامات البطيئة إذا كانت عتبته ثانية واحدة.
في Postgres تُجمّع إضافة pg_stat_statements هذا بالضبط: عدد الاستدعاءات والوقت الإجمالي والوقت المتوسط لكل تعليمة بعد تطبيعها. رتّب حسب الوقت الإجمالي وستجد المتسبب عادةً ضمن الأسطر الثلاثة الأولى. ويقدّم MySQL تجميعاً مماثلاً عبر مخطط الأداء.
هناك أمران يستحقان الفحص قبل أن تحكم على الاستعلام نفسه. هل هو بطيء في كل مرة أم في ساعات بعينها فقط، وهو ما يشير إلى تنازع على الموارد لا إلى خطة سيئة؟ وهل هو بطيء وحده أم فقط تحت التزامن، وهو ما يشير إلى أقفال أو إلى حدود على الاتصالات؟
اقرأ الخطة بدلاً من التخمين
بعد أن تحدد التعليمة، اسأل قاعدة البيانات كيف تنوي تنفيذها. يُظهر Postgres ذلك عبر EXPLAIN
، والصيغة المهمة هي EXPLAIN ANALYZE التي تنفّذ الاستعلام فعلياً وتُبلّغ عن أزمنة حقيقية لا عن تقديرات.
ثلاثة أمور في ذلك المخرج تحمل معظم الإشارة.
مسح تسلسلي على جدول كبير. قاعدة البيانات تقرأ كل صف. على جدول صغير يكون هذا صحيحاً وسريعاً. أما على جدول كبير فيعني أنه لا يوجد فهرس صالح للشرط الذي كتبته، أو أن المخطط قرر أن الفهرس لا يستحق الاستخدام.
فجوة كبيرة بين عدد الصفوف المقدَّر والفعلي. يختار المخطط استراتيجيته من الإحصاءات، فحين يخطئ تقديره بمراتب من حيث الحجم فإنه يختار اختياراً سيئاً لأسباب لا علاقة لها باستعلامك. والإحصاءات القديمة سبب شائع وسهل العلاج.
وقت متركّز في عقدة واحدة. الخطط أشجار، والعلاج ينتمي إلى العقدة التي استهلكت الوقت. تحسين أي شيء آخر لا يغيّر شيئاً.
الغريزة التي تدفعك إلى إضافة فهرس فور رؤية مسح تسلسلي صحيحة في الغالب، ومع ذلك تستحق ثلاثين ثانية من التريث، لأن سبب عدم استخدام الفهرس أهم أحياناً من غيابه.
لماذا تفشل الفهارس في المساعدة
الفهرس الموجود ليس بالضرورة فهرساً مستخدَماً.
الشرط ليس قابلاً للفهرسة. تغليف عمود داخل دالة أو إجراء عملية حسابية عليه يمنع عادةً استخدام الفهرس المبني على ذلك العمود، لأن الفهرس يخزّن قيم العمود لا القيم المحوَّلة. وإعادة كتابة الشرط بحيث يبقى العمود مجرداً تُعيد الاستخدام في الغالب.
ترتيب الأعمدة في الفهرس المركّب خاطئ. الفهرس المركّب يخدم الاستعلامات التي تستخدم أعمدته الأولى. وفهرس على عمود ثم على آخر لا يفيد استعلاماً يرشّح على العمود الثاني وحده، وهذا فخ يقع فيه الناس باستمرار.
المخطط يرى أن المسح أرخص. إذا أعاد الاستعلام نسبة كبيرة من الجدول، فإن قراءته تسلسلياً أسرع فعلاً من القفز عبر فهرس. هذا سلوك صحيح، والعلاج هو أن تُعيد بيانات أقل.
الإحصاءات قديمة. بعد تحميل ضخم أو عملية حذف كبيرة، قد تكون صورة البيانات لدى المخطط خاطئة تماماً إلى أن تُحدَّث الإحصاءات.
ولكل فهرس ثمن. فعمليات الكتابة ملزمة بصيانته، وهو يستهلك ذاكرة كانت ستُستخدم للاحتفاظ بالبيانات في الذاكرة المؤقتة. والجدول الذي يحمل خمسة عشر فهرساً يضم عادةً عدة فهارس لا يحتاجها أحد، وكل واحد منها يجعل كل عملية إدراج أبطأ.
مشكلة N+1 ما زالت السبب المفرد الأكبر
بطء التطبيقات ينبع من هنا أكثر مما ينبع من أي مشكلة في الخطة، وهو لا يظهر أبداً كاستعلام بطيء لأن كل استعلام منفرد سريع.
الشكل مألوف. تجلب قائمة من 100 سجل، ثم تدور عليها وتجلب البيانات المرتبطة بكل سجل. النتيجة 101 رحلة ذهاب وإياب حيث كانت رحلة أو رحلتان تكفيان. كل استعلام يعود خلال ثلاثة أجزاء من الألف من الثانية، ومع ذلك تستغرق الصفحة نصف ثانية، لأن التكلفة في رحلات الشبكة لا في العمل نفسه.
أدوات الربط بين الكائنات والجداول تجعل كتابة هذا بالخطأ أمراً سهلاً، لأن الوصول إلى البيانات المرتبطة يبدو كقراءة خاصية لا كاستدعاء لقاعدة البيانات. والعلاج هو تحميل البيانات المرتبطة في استعلام واحد مع المجموعة الأصلية، وهو ما تدعمه كل أداة ORM ناضجة وما لا تفعله معظمها افتراضياً.
اكتشافها مباشر: عُدّ الاستعلامات لكل طلب. فإذا أصدرت صفحة عدداً من الاستعلامات يتناسب مع عدد العناصر المعروضة، فقد وجدتها. وهذا أيضاً أعلى الفحوص قيمةً حين يكون التطبيق بطيئاً على الحافة، كما يشرح دليلنا عن Cloudflare Hyperdrive ، لأن رحلات الشبكة تكلّف أكثر بكثير كلما زادت المسافة.
الاتصالات والتنازع
مشكلتان تبدوان بطئاً وليستا كذلك.
استنفاد الاتصالات. لكل قاعدة بيانات سقف للاتصالات المتزامنة، وكل اتصال يكلّف ذاكرة. وحين يفتح التطبيق أكثر مما يسمح به التجمع، تصطف الطلبات في انتظار اتصال ويبدو التطبيق بطيئاً بينما تجلس قاعدة البيانات عاطلة. العَرَض هو زمن استجابة مرتفع في التطبيق مع استهلاك منخفض للمعالج في قاعدة البيانات، والعلاج هو تجميع الاتصالات لا شراء جهاز أكبر.
التنازع على الأقفال. معاملة طويلة تحتفظ بقفل تحجب كل ما ينتظر خلفها. والسبب المعتاد معاملة تُركت مفتوحة عبر عمل لا يحتاج إلى قاعدة البيانات أصلاً، مثل استدعاء HTTP لخدمة أخرى. أبقِ المعاملات قصيرة ومحصورة في العمل على قاعدة البيانات نفسه.
يستحق كلا الاحتمالين الاستبعاد مبكراً، لأن كليهما يسهل قراءته خطأً على أنه مشكلة استعلام، ولا يُعالَج أي منهما بفهرس.
ماذا تفعل وبأي ترتيب
ابحث عن التعليمات التي تستهلك أكبر وقت إجمالي. شغّل EXPLAIN ANALYZE على أسوأها واقرأ إلى أين يذهب الوقت فعلاً. افحص عدد الاستعلامات لكل طلب لاستبعاد N+1 قبل أن تحسّن أي شيء. حدّث الإحصاءات قبل إضافة فهرس، لأن ذلك يكون أحياناً كل العلاج. ثم أضف أضيق فهرس يخدم الشرط، وقِس من جديد.
تقوم Mecanik بهذا ضمن أعمال تطوير البرمجيات لدينا، والنتيجة تكاد تكون واحدة دائماً: استعلامان أو ثلاثة كانت هي المسؤولة، والعلاج كان صغيراً، والجهاز الأكبر الذي لم يشتره أحد لم يكن مطلوباً قط.
تدوينات ذات صلة: إصدارات API: متى تكسر التوافق وكيف تتجنب ذلك ، كيفية بناء تطبيق ويب في 2026 - دليل المطور البريطاني ، تخزين كلمات المرور: ماذا تستخدم في 2026 ، تطوير البرمجيات المخصصة في المملكة المتحدة - دليل المشترين .
الأسئلة الشائعة
كيف أعرف أي استعلام يُبطئ تطبيقي؟ رتّب حسب الوقت الإجمالي لا حسب أسوأ حالة. استعلام مدته أربعون جزءاً من الألف من الثانية ويعمل 8000 مرة في الدقيقة يكلّف أكثر بكثير من استعلام مدته ثانيتان ويعمل مرتين في اليوم، ولن يظهر أبداً في سجل استعلامات بطيئة عتبته ثانية واحدة. في Postgres تُجمّع pg_stat_statements عدد الاستدعاءات والوقت الإجمالي لكل تعليمة، ويقدّم MySQL الشيء نفسه عبر مخطط الأداء.
عمّ أبحث في مخرجات EXPLAIN ANALYZE؟ ثلاثة أمور تحمل معظم الإشارة: مسح تسلسلي على جدول كبير، أي غياب فهرس صالح؛ وفجوة كبيرة بين عدد الصفوف المقدَّر والفعلي، أي مخطط يعمل بإحصاءات سيئة؛ ووقت متركّز في عقدة واحدة من شجرة الخطة، وهي الموضع الذي ينتمي إليه العلاج. تحسين أي عقدة أخرى لا يغيّر شيئاً.
لماذا لا يُستخدم الفهرس الذي أنشأته؟ عادةً لسبب من أربعة. الشرط يغلّف العمود داخل دالة أو عملية حسابية فلا يعود الفهرس مطابقاً. أو الفهرس المركّب يرتّب أعمدته ترتيباً لا يخدم الاستعلام. أو الاستعلام يُعيد نسبة من الجدول كبيرة بما يكفي ليكون المسح أرخص فعلاً. أو الإحصاءات قديمة بعد تحميل ضخم أو عملية حذف.
ما هي مشكلة استعلامات N+1؟ أن تجلب قائمة من السجلات ثم تُصدر استعلاماً منفصلاً للبيانات المرتبطة بكل سجل، فتنتج 101 رحلة ذهاب وإياب حيث كانت رحلة أو رحلتان تكفيان. لا تظهر في أي سجل استعلامات بطيئة لأن كل استعلام سريع، فالتكلفة في رحلات الشبكة. واكتشافها يكون بعدّ الاستعلامات لكل طلب والبحث عن عدد يتناسب مع العناصر المعروضة.
هل يعالج خادم قاعدة بيانات أكبر الاستعلامات البطيئة؟ نادراً، وبشكل مؤقت فقط. فحين يكون التطبيق بطيئاً وقاعدة البيانات مشغولة، يكون السبب في الغالب الأعم عدداً صغيراً من الاستعلامات المحددة لا نقصاً في الطاقة الاستيعابية، فالترقية تُخفي المشكلة إلى أن ينمو الجدول من جديد. وزمن الاستجابة المرتفع في التطبيق مع استهلاك منخفض للمعالج في قاعدة البيانات يشير عادةً إلى استنفاد تجمع الاتصالات، وهو ما لا يعالجه عتاد إضافي.
التعليقات