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

الفهارس الجزئية في SQLite: CREATE INDEX ... WHERE

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

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

الفهارس الجزئية في SQLite تغطي جزءًا من الصفوف فقط

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

الصيغة هي CREATE INDEX المعتاد مع إضافة WHERE في آخره:

idx_orders_pending يحتوي فقط على إدخالات للصفوف التي تكون فيها status = 'pending'. أمّا الطلبات المُرسَلة والملغاة والمُسترَدّة فلن تجد لها أثرًا فيه. لو كانت 95% من بيانات جدول orders لديك أرشيفًا قديمًا وكانت استعلاماتك تتمحور حول الطلبات المفتوحة فقط، فأنت أمام فهرس أصغر بعشرين مرّة يقدّم لك نفس سرعة الاستعلام.

متى يستخدم مخطّط الاستعلام الفهرس الجزئي فعليًا؟

لا يستطيع SQLite الاستفادة من الفهرس الجزئي إلّا إذا تمكّن من إثبات أنّ استعلامك مقصور على نفس الصفوف التي يغطّيها الفهرس. وأبسط طريقة لضمان ذلك هي تكرار شرط WHERE الخاص بالفهرس داخل الاستعلام نفسه:

الخطة يجب أن تذكر USING INDEX idx_orders_pending. لو شِلت status = 'pending' من الاستعلام، الـ planner راح يرجع لمسح الجدول كامل (full table scan) — ما عنده طريقة يعرف إن الاستعلام لسّه ضمن النطاق المفهرس.

القاعدة الذهبية: شرط WHERE في الاستعلام يجب أن يكون متضمَّناً في شرط WHERE الخاص بالفهرس. المساواة على نفس العمود وبنفس القيمة هي الحالة الواضحة والآمنة. أمّا المتباينات وOR فالأمر يصير أعقد؛ تأكّد دائماً عبر EXPLAIN QUERY PLAN.

لماذا تتعب نفسك؟ ثلاث مكاسب حقيقية

في ثلاث أسباب ملموسة تخلّي الفهارس الجزئية في SQLite تستاهل العناء:

  1. حجم أصغر على القرص. ما يتخزّن إلا الصفوف المطابقة للشرط. لو عندك حِمل عمل فيه «1% من الجدول هو الجزء النشط»، فالفهرس راح يكون تقريباً 1% من حجم الفهرس الكامل.
  2. عمليات كتابة أرخص. الـ INSERT وUPDATE ما يلمسون الفهرس إلا لمّا يتطابق الصف مع شرط الفلترة. فمثلاً INSERT بقيمة status = 'shipped' على الجدول السابق ما راح يلمس idx_orders_pending أبداً.
  3. سرعة بحث مماثلة. البحث في الـ B-tree لوغاريتمي بالنسبة لحجم الفهرس. فهرس أصغر يعني بحث أسرع شوي، لكن المكسب الأكبر يجي من حوالين هذا — إخفاقات أقل في الـ cache، و I/O أخف.

إذا كان توزيع القيم في عمود ما متحيّزاً بشدّة — معظم الصفوف تحمل قيمة واحدة، وأنت يهمّك لكن القيم النادرة الأخرى — فهذي هي الحالة المثالية التي تستدعي استخدام partial index.

الفهارس الجزئية الفريدة (الميزة القاتلة)

قيود UNIQUE العادية تنطبق على كل صف بلا استثناء. وهذي مشكلة تطفو على السطح بمجرّد ما تضيف نظام الحذف الناعم (soft delete):

-- يفشل: يوجد صفّان بقيمة email = 'a@x.com'، رغم أنّ أحدهما محذوف.
CREATE UNIQUE INDEX idx_users_email ON users(email);

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

ثلاثة صفوف، نفس البريد الإلكتروني، ولا يوجد أي خرق للقيد — والسبب أن الصف الذي ينطبق عليه deleted_at IS NULL فقط هو الذي يدخل في فحص التفرّد. جرّب إدراج صف ثانٍ "حيّ" بنفس البريد وستجد SQLite يرفع لك UNIQUE constraint failed.

هذا النمط تقابله في كل مكان: اشتراك فعّال واحد لكل عميل، عنوان رئيسي واحد لكل مستخدم، فاتورة مفتوحة واحدة لكل طلب. الفهارس الجزئية الفريدة (UNIQUE Partial Index) تعبّر عن هذه الحالات بشكل مباشر.

الفهرسة مع قيم NULL

تتعامل قيم NULL بشكل غريب مع الفهارس. وغالبًا ما يكون هدفك هو "تجاهل قيم NULL تمامًا" — مثلًا لديك عمود external_id المتفرّق، حيث معظم الصفوف فيه NULL، لكن الصفوف التي تحمل قيمة فعلية يجب أن تكون فريدة:

يتعايش اثنان من قيم NULL بسلام، ومع ذلك يبقى الصفّان EXT-001 وEXT-002 فريدَين بشكل مضمون. كما أن حجم الفهرس يصبح أصغر — إذ لا تُخزَّن صفوف NULL إطلاقًا — فيظل البحث عبر external_id سريعًا حتى لو تضخّم الجدول.

ماذا يمكن أن يحتوي شرط الفلتر؟

شرط WHERE في الفهرس الجزئي مقيَّد إلى حدٍّ بعيد. يمكنه الإشارة إلى:

  • أعمدة الجدول المُفهرَس نفسه.
  • قيم ثابتة (literals).
  • مجموعة محدودة من الدوال المضمَّنة الحتمية (deterministic).

ولا يمكنه الإشارة إلى:

  • جداول أخرى.
  • استعلامات فرعية (subqueries).
  • دوال غير حتمية مثل random() أو CURRENT_TIMESTAMP.
  • معاملات (parameters) أو متغيرات.

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

لكن WHERE created_at > date('now') لن ينفع — لأن date('now') قيمتها تتغيّر مع مرور الوقت، وبالتالي ستتبدّل مجموعة الصفوف المفهرسة من تحت قدمَي SQLite.

خطوات للتحقّق من سلامة الفهرس الجزئي

كلما أضفت فهرسًا جزئيًا، مُرّ على هذه الفحوصات الثلاثة:

الاستعلام الأول يفترض أن يستخدم idx_jobs_runnable، بينما يجب أن يلجأ الاستعلامان الثاني والثالث إلى مسح كامل (أو إلى فهرس آخر إن كان موجوداً). إذا اختار المخطِّط الفهرس الجزئي لاستعلام لم تكن تتوقّعه، فأعد قراءة شرط الفلتر — على الأرجح أنه أوسع ممّا تظنّ.

متى لا تلجأ إلى الفهرس الجزئي؟

الفهارس الجزئية أداة دقيقة وحادّة. إليك الحالات التي يُفضَّل فيها تجاهلها:

  • عندما يشمل الفلتر معظم الصفوف. لو كانت قيمة "active" تمثّل 90% من جدولك، فالفهرس الجزئي صار مجرّد فهرس عادي مع تعقيد إضافي. اكتفِ بفهرسة العمود مباشرةً.
  • عندما لا تتطابق استعلاماتك مع الفلتر حرفياً. إذا كان الكود يبني شرطاً مثل WHERE status IN (?, ?, ?) عبر ORM، أو يُكوِّن الفلتر ديناميكياً، فالمخطِّط في الغالب لن يتعرّف على التطابق. تحقّق بنفسك باستخدام EXPLAIN QUERY PLAN ولا تفترض.
  • عندما تتغيّر المجموعة الساخنة مع الوقت. فهرس جزئي على "طلبات آخر 30 يوماً" يبدو مغرياً، لكنه غير قابل للتعبير عنه — الفلتر يجب أن يكون حتمياً (deterministic). ستحتاج إلى إعادة بناء الفهرس باستمرار، أو اختيار تصميم مختلف (جدول منفصل باسم recent_orders، أو عمود archived تُحدِّثه ليلاً).

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

التالي: قراءة خطط التنفيذ

اعتمدنا في معظم هذه الصفحة على EXPLAIN QUERY PLAN للتأكّد من أن الفهرس استُخدم فعلاً. هذه الأداة تستحق صفحة كاملة لوحدها — كيف تقرأ مخرجاتها، ماذا تعني كلماتها المفتاحية، وكيف تُميِّز بين بحث سعيد عبر فهرس وبين مسح كامل متخفٍّ. هذا موضوع الصفحة القادمة.

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

ما المقصود بالفهرس الجزئي في SQLite؟

الفهرس الجزئي هو فهرس يغطي الصفوف المطابقة لشرط WHERE تحدده وقت الإنشاء فقط. تكتب CREATE INDEX name ON table(col) WHERE condition، فيخزّن SQLite مدخلات للصفوف التي يتحقق فيها الشرط فقط. النتيجة: فهرس أصغر، وكتابات أسرع، وسرعة بحث مماثلة للاستعلامات التي تطابق نفس الفلتر.

متى أستخدم فهرساً جزئياً بدلاً من فهرس كامل؟

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

هل يمكن للفهرس الجزئي فرض التفرّد (UNIQUE)؟

نعم، عبر CREATE UNIQUE INDEX ... WHERE ...، حيث يُفرض التفرّد على الصفوف المطابقة للفلتر فقط. الاستخدام الكلاسيكي هو قاعدة "سجل نشط واحد لكل مستخدم": الصفوف المحذوفة ناعماً تُستثنى، فيمكن أن يكون لديك عدة سجلات محذوفة بنفس المفتاح، لكن سجل حيّ واحد فقط.

Coddy programming languages illustration

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

ابدأ الآن