Menu
flag Ar iconالعربيةdown icon
جرّب في Playground

الفهارس المركبة في SQLite وقاعدة البادئة اليسرى

كيف تعمل الفهارس متعددة الأعمدة في SQLite، ولماذا يُحدث ترتيب الأعمدة فرقًا كبيرًا، ومتى يفيدك الفهرس المركب فعلًا ومتى يكون مجرد إهدار للمساحة.

تحتوي هذه الصفحة على محررات قابلة للتشغيل — حرّر، شغّل، وشاهد النتيجة فوراً.

فهرس واحد يغطّي عدّة أعمدة

الفهرس المركّب في SQLite (أو ما يُعرف بفهرس متعدد الأعمدة) هو فهرس واحد يُبنى فوق عمودين أو أكثر معًا. لإنشائه، تكتب الأعمدة بالترتيب الذي تريده هكذا:

الفهرس idx_orders_customer_status يخزّن المُدخلات مرتّبةً أولًا حسب customer_id، ثم حسب status داخل كل عميل. هذا الترتيب هو جوهر الموضوع كله، وكل ما يخص الفهارس المركّبة في SQLite يتفرّع منه.

التصوّر الذهني: دفتر هاتف مُرتَّب

تخيّل دفتر هاتف قديم. الأسماء فيه مرتّبة حسب اسم العائلة، وداخل كل عائلة تكون مرتّبة حسب الاسم الأول. هذا بالضبط شكل أي فهرس على (last_name, first_name).

بعض عمليات البحث رخيصة، وبعضها مُكلف:

  • "ابحث عن كل من يحمل اسم العائلة Patel" — سهل، فكل آل Patel موجودون متجاورين.
  • "ابحث عن Priya Patel" — سهل أيضًا، تقفز إلى Patel ثم تمسح حتى تصل إلى Priya.
  • "ابحث عن كل من يحمل الاسم الأول Priya" — بطيء، لأنك مضطر لتصفّح كل الصفحات. فأسماء Priya متفرّقة بين كل أسماء العائلات.

الفهرس المركّب في SQLite يعمل بنفس الأسلوب تمامًا. العمود الأول هو مفتاح الترتيب الرئيسي، والعمود الثاني لا يُرتَّب إلا بين المُدخلات التي تتشارك في نفس قيمة العمود الأول.

قاعدة البادئة اليسرى (Leftmost Prefix Rule)

لا يستطيع SQLite استخدام الفهرس المركّب لاستعلام ما إلا إذا كانت جملة WHERE تُقيّد بادئة يسرى من أعمدته. لنأخذ فهرسًا على (a, b, c):

  • التصفية باستخدام a — يستخدم الفهرس.
  • التصفية باستخدام a وb — يستخدم الفهرس.
  • التصفية باستخدام a وb وc — يستخدم الفهرس.
  • التصفية باستخدام b وحده، أو c وحده، أو b وc معًا — لن يستخدم الفهرس.

ويمكنك التحقّق من ذلك مباشرةً عبر EXPLAIN QUERY PLAN:

الخطة الأولى تُظهر SEARCH events USING INDEX idx_events_user_kind_time، بينما تتراجع الخطة الثانية إلى SCAN events — لأن التصفية على kind وحده يتخطى العمود الأول user_id، فيصبح الفهرس عديم الفائدة لهذا الاستعلام.

ترتيب الأعمدة في الفهرس قرار تصميمي

بما أن قاعدة البادئة اليسرى (leftmost prefix rule) هي الحاكمة، فإن ترتيب الأعمدة الذي تكتبه داخل CREATE INDEX خيار حقيقي، لا مجرد مسألة شكلية. إليك قاعدتين عمليتين:

  1. ابدأ بالعمود الذي تُصفّي به أكثر من غيره. هذا العمود هو الذي يفتح الفهرس أمام أوسع قدر ممكن من الاستعلامات.
  2. ضع أعمدة المساواة قبل أعمدة النطاق. يستطيع SQLite الوصول المباشر داخل الفهرس عبر =، ثم يمسح نطاقًا متصلًا باستخدام < أو > أو BETWEEN — لكن فقط على آخر عمود مُستخدَم.

تُظهر الخطة SEARCH sales USING INDEX idx_sales_region_time (region=? AND sold_at>?). هنا ينتقل SQLite مباشرة إلى region = 'EU'، ثم يتقدّم عبر النطاق الزمني. لكن لو عكست ترتيب الأعمدة إلى (sold_at, region)، فسيضطر نفس الاستعلام إلى المرور على كل صف ضمن النطاق الزمني وإعادة فحص region في كل مرة.

الفهرس المركب مقابل عدة فهارس أحادية العمود

سؤال يتكرر كثيرًا: هل الأفضل إنشاء فهرس واحد على (a, b)، أم فهرسين منفصلين على a و b؟

بالنسبة للاستعلام المركب الذي يجمع شرطين، يتفوق الفهرس المركب في السرعة — إذ ينتقل SQLite مباشرة إلى السجلات المطابقة لقيم (project_id, state). أما عند استخدام فهرسين منفصلين لكل عمود، فإن SQLite يختار عادةً أحدهما لتضييق نطاق النتائج، ثم يتحقق من العمود الآخر يدويًا في كل صف ناتج. صحيح أنه قد يلجأ أحيانًا إلى تقاطع الفهرسين، لكن يبقى الفهرس المركب هو الحل الأنظف حين تُستعلم الأعمدة مجتمعةً.

أما إذا كنت تستعلم عن project_id و state كلٌّ على حدة أيضًا، فقد تحتاج إلى الاثنين معًا: الفهرس المركب للاستعلامات المشتركة، وفهرس مفرد على state للاستعلامات التي تعتمد عليه وحده.

الفهرس المغطي (Covering Index) في SQLite

عندما يحتوي الفهرس على جميع الأعمدة التي يطلبها الاستعلام — سواء أعمدة التصفية أو الأعمدة المُختارة في SELECT — يستطيع SQLite الإجابة دون الرجوع إلى الجدول الأصلي إطلاقًا. هذا ما يُعرف بـ covering index، وهو أسرع شكل يمكن أن يصل إليه الاستعلام.

الخطة تُظهر USING COVERING INDEX idx_invoices_cover. الاستعلام يقرأ issued_at و total مباشرةً من الفهرس — أمّا notes و id فلا حاجة إليهما، ولذلك لا يُفتح الجدول الأصلي إطلاقًا. إضافة عمود إلى فهرس مركّب لمجرّد تغطية استعلام كثير الاستخدام صفقة مربحة عندما يكون هذا الاستعلام يعمل باستمرار.

قيود التفرّد المركّبة (Composite UNIQUE)

الفهرس المركّب في SQLite يصلح أيضًا لفرض التفرّد على مجموعة أعمدة مجتمعة. هذا مفيد حين لا يكون أيّ عمود بمفرده فريدًا، لكن المطلوب أن يكون الجمع بينها فريدًا:

عملية الإدخال الثالثة سترفع الخطأ UNIQUE constraint failed: enrollments.student_id, enrollments.course_id، لأن نفس الزوج موجود مسبقًا في الفهرس، فترفض SQLite تكرار القيمة.

مزالق ينبغي الانتباه إليها

  • استخدام OR بين أعمدة ليست في المقدمة يُعطّل الفهرس. الشرط WHERE a = 1 OR b = 2 على فهرس (a, b) لا يستطيع غالبًا الاستفادة من الفهرس إطلاقًا، لأن SQLite مضطرة لمعالجة كل فرع على حدة.
  • استدعاء الدوال على الأعمدة المفهرسة يُعطّل الفهرس. الشرط WHERE lower(email) = 'x' لن يستفيد من فهرس على العمود email. الحل أن تفهرس التعبير ذاته، أو أن تُطبّع البيانات لحظة الإدخال.
  • الفهارس ليست مجانية. كل فهرس يُحدَّث مع كل عملية INSERT، ومع كل UPDATE تمسّ أعمدة مفهرسة، ومع كل DELETE. ثلاثة فهارس مركّبة على جدول كثير الكتابة قد تستهلك معظم تكلفة الكتابة.
  • شغّل ANALYZE بعد بناء الفهارس. يعتمد مخطّط الاستعلامات في SQLite على الإحصاءات التي يجمعها ANALYZE للموازنة بين الفهارس المرشّحة. وبدون هذه الإحصاءات يلجأ إلى تقديرات تقريبية ليست دائمًا الأمثل.

سير عمل عملي

عند ضبط استعلام بطيء، تكون الدورة المعتادة كالتالي:

  1. شغّل EXPLAIN QUERY PLAN على الاستعلام لترى ماذا تفعل SQLite حاليًا.
  2. إذا كانت تُجري مسحًا كاملًا، تأمّل عبارة WHERE: ما العمود ذو شرط المساواة؟ وما العمود ذو شرط النطاق؟ وما الأعمدة المختارة في SELECT؟
  3. ابنِ فهرسًا مركّبًا مرتّبًا بحيث تأتي أعمدة المساواة أولًا ثم أعمدة النطاق، مع إلحاق الأعمدة المختارة إذا كان الفهرس المُغطّي (covering index) سيُفيد.
  4. شغّل ANALYZE.
  5. شغّل EXPLAIN QUERY PLAN مرة أخرى، وتحقّق من أن الخطة تغيّرت فعلًا وأن الفهرس قيد الاستخدام.
  6. قِس زمن تنفيذ الاستعلام قبل التعديل وبعده، على بيانات تمثّل الواقع.

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

المحطة التالية: الفهارس الجزئية

الفهرس المركّب يُغطّي جميع صفوف الجدول. لكن في أغلب الحالات، يهمّك جزء صغير فقط من الصفوف: التذاكر المفتوحة، أو المهام غير المُعالَجة، أو السجلات غير المحذوفة. هنا يأتي دور الفهرس الجزئي الذي يسمح لك بفهرسة هذه الصفوف فقط، عبر شرط WHERE يُدمج داخل تعريف الفهرس نفسه. وهذا موضوع الصفحة التالية.

الأسئلة الشائعة

ما هو الفهرس المركب في SQLite؟

الفهرس المركب هو فهرس واحد يغطي عمودين أو أكثر، وتُنشئه بالأمر CREATE INDEX idx_name ON table(col_a, col_b). يخزّن SQLite المُدخلات مرتبةً حسب col_a أولًا ثم حسب col_b داخل كل قيمة من col_a — تمامًا مثل دليل هاتف مرتّب باسم العائلة ثم الاسم الأول.

هل يهم ترتيب الأعمدة في الفهرس المركب؟

يهم كثيرًا. لا يستطيع SQLite استخدام الفهرس المركب إلا إذا كانت جملة WHERE تُرشّح على بادئة يسرى من أعمدة الفهرس. فهرس على (a, b, c) يفيد الاستعلامات التي تُرشّح على a وحده، أو على a وb، أو على الثلاثة معًا — لكنه لا يفيد استعلامًا يُرشّح على b وحده أو c وحده.

متى أستخدم فهرسًا مركبًا بدلًا من فهارس منفصلة لكل عمود؟

استخدم الفهرس المركب عندما تعمل استعلاماتك بانتظام على نفس مجموعة الأعمدة معًا، سواء في الترشيح أو الترتيب. أما الفهارس المنفردة فهي أنسب حين يُستعلم عن كل عمود باستقلالية. شغّل EXPLAIN QUERY PLAN لترى أي فهرس اختاره SQLite فعلًا — هذه هي التغذية الراجعة الوحيدة التي يمكن الوثوق بها.

ما هو الفهرس المغطّي (Covering Index) في SQLite؟

الفهرس المغطّي هو فهرس يحتوي على كل الأعمدة التي يحتاجها الاستعلام، فيستطيع SQLite الإجابة من الفهرس مباشرةً دون لمس الجدول الأصلي. وعند تشغيل EXPLAIN QUERY PLAN ستظهر لك عبارة USING COVERING INDEX. وإضافة عمود إضافي إلى الفهرس المركب فقط لتغطية استعلام مهم هي تحسين شائع وفعّال.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن