Menu
flag Ar iconالعربيةdown icon

الفهارس في SQLite: CREATE INDEX ومتى تستخدمها

كيف تعمل الفهارس في SQLite، ومتى تفيدك ومتى تكون عبئاً، وكيف تتأكد أن مُخطِّط الاستعلامات يستخدمها فعلاً.

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

ما هو الفهرس فعلياً؟

الفهرس (Index) هو هيكل بيانات منفصل — عبارة عن شجرة B-tree مرتّبة — يتيح لـ SQLite العثور على الصفوف بناءً على قيمة عمود معيّن دون الحاجة إلى مسح الجدول بالكامل. بدون فهرس، فإن استعلاماً مثل WHERE email = 'rosa@example.com' يقرأ كل صف ويتحقّق منه واحداً تلو الآخر. أمّا مع وجود فهرس على عمود email، فإن SQLite يجتاز الشجرة في عدد خطوات يقارب log(n) ويصل مباشرة إلى النتيجة المطلوبة.

لكن هذا التسريع لا يأتي مجاناً. فالفهرس في حقيقته نسخة من العمود المُفهرَس مع مؤشّر يشير إلى الصف الأصلي. وهذا يعني أن كل عملية INSERT، وكل UPDATE يطال عموداً مُفهرَساً، وكل DELETE تستوجب تحديث الفهرس أيضاً. النتيجة: مساحة تخزين أكبر، وتراجع طفيف في سرعة الكتابة. الصفقة باختصار: تدفع قليلاً عند الكتابة، وتربح الكثير عند القراءة.

إنشاء فهرس باستخدام CREATE INDEX في SQLite

الصيغة الأساسية كالتالي:

اصطلاح التسمية: معظم الفِرَق تتبع النمط idx_<table>_<column> حتى يتضح الغرض من الفهرس بمجرد النظر إليه. لاحظ أن الاسم يجب أن يكون فريدًا على مستوى قاعدة البيانات بأكملها، لا على مستوى الجدول فقط، ولهذا السبب نُضمّن اسم الجدول داخل الاسم.

ولحذف الفهرس:

DROP INDEX idx_users_email;

الفهارس مجرد دعامات لتحسين الأداء، لا أكثر. حذف أي فهرس لن يمسّ بياناتك أبدًا، فهو يؤثر فقط على سرعة تنفيذ الاستعلامات.

الفهرس الفريد UNIQUE INDEX في SQLite

الفهرس الفريد يؤدي وظيفتين في آنٍ واحد: يُسرّع عمليات البحث ويضمن في الوقت نفسه ألّا يتكرر نفس القيمة في صفّين مختلفين.

يفشل الإدراج الثالث برسالة UNIQUE constraint failed: accounts.username. فـ SQLite ينشئ فهارس فريدة تلقائيًا لأعمدة PRIMARY KEY وUNIQUE، وستجدها بأسماء على شاكلة sqlite_autoindex_<table>_<n>. لذلك لا تحتاج إلى كتابة CREATE UNIQUE INDEX يدويًا إلا حين لا يكون القيد معرَّفًا أصلًا على الجدول.

ماذا يفعل مخطِّط الاستعلامات فعليًا؟

إنشاء الفهرس وحده لا يضمن أن يستخدمه SQLite. فمخطِّط الاستعلامات (Query Planner) يختار خطة تنفيذ مستقلة لكل استعلام، ويمكنك معاينة ما اختاره عبر EXPLAIN QUERY PLAN:

ابحث في الناتج عن SEARCH ... USING INDEX idx_orders_customer — وجوده يعني أن الفهرس مُستخدَم فعلًا. أما إذا ظهر لك SCAN orders، فهذا يعني أن مُخطِّط الاستعلام رأى أن المسح الكامل للجدول أرخص (وهذا صحيح غالبًا مع الجداول الصغيرة)، أو أن صياغة استعلامك منعته من استغلال الفهرس. وسنُفرد لاحقًا مقالًا كاملًا لقراءة هذه الخطط بالتفصيل.

متى لا يستفيد SQLite من الفهرس؟

للفهارس بعض النقاط العمياء المعروفة، وكل حالة من الحالات التالية كفيلة بتعطيل الفهرس المُنشأ على عمود email:

-- الدالة تُغلِّف العمود
SELECT * FROM users WHERE lower(email) = 'rosa@example.com';

-- حرف بدل في بداية LIKE
SELECT * FROM users WHERE email LIKE '%@example.com';

-- عدم تطابق النوع يفرض تحويلاً
SELECT * FROM users WHERE email = 12345;

شجرة B-tree مرتّبة وفق قيمة email الخام، لذا أي تحويل تُجريه على العمود وقت الاستعلام سيُجبر المحرّك على مسح الجدول بالكامل. الحلول متعدّدة: خزّن البيانات مُطبَّعة مسبقًا في عمود مستقل مثل email_lower، أو استخدم فهرسًا على تعبير بهذا الشكل CREATE INDEX idx ON users(lower(email))، أو اعتمد على البحث النصّي الكامل (FTS) في SQLite عند الحاجة لمطابقة جزء من النص.

الفهارس المُغطِّية (Covering Index في SQLite)

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

بما أن العمودين المطلوبين في الاستعلام موجودان داخل الفهرس نفسه، يُبلِّغ SQLite بـ USING COVERING INDEX، دون الحاجة لجلب الصف الأصلي. الـ covering index من أقوى أساليب تحسين أداء استعلامات SQLite في مسارات القراءة الساخنة — والمقابل طبعًا هو فهرس أكبر حجمًا. أما الفهارس متعددة الأعمدة فلها حديث مستقل، وسنتناولها بالتفصيل في الدرس القادم.

عرض الفهارس وفحصها

أمامك طريقتان لمعرفة الفهارس الموجودة:

هذا الاستعلام يعرض لك كل الفهارس الموجودة في قاعدة البيانات مع جملة CREATE الخاصة بكل واحد منها. ولو أردت الاطلاع على فهارس جدول واحد فقط، استخدم PRAGMA index_list('products'); لعرض فهارس هذا الجدول، ثم PRAGMA index_info('idx_products_name'); لمعرفة الأعمدة التي يغطيها كل فهرس. أي فهرس يبدأ اسمه بـ sqlite_autoindex_ فهو فهرس أنشأته SQLite تلقائيًا لخدمة قيد PRIMARY KEY أو UNIQUE، ولا يمكن حذفه.

متى لا يجب إضافة فهرس في SQLite

هناك حالات تجعل إضافة الفهرس تضرّ أكثر مما تنفع:

  • الجداول الصغيرة جدًا. بضع مئات من الصفوف يمسحها المحرك في أجزاء من الميكروثانية. غالبًا ما يتجاهل مخطط الاستعلامات الفهرس أصلًا، وتكون قد أضفت عبئًا على عمليات الكتابة دون أي فائدة.
  • الأعمدة كثيرة الكتابة ونادرة الاستعلام. كل عملية كتابة تُحدِّث كل فهرس. إذا فهرست عمودًا لا تكاد تُرشِّح به، فأنت تدفع التكلفة بلا مقابل.
  • الأعمدة منخفضة التنوّع بمفردها. فهرس على عمود status فيه ثلاث قيم محتملة لن يُضيِّق نطاق البحث كثيرًا. قد يفيد كعمود ثانٍ ضمن فهرس مركّب، أو كفهرس جزئي، لكنه بمفرده لا يستحق العناء في الغالب.
  • مغطّى مسبقًا. إذا كان عندك فهرس على (a, b) فلست بحاجة إلى فهرس آخر على (a) وحده. فـ SQLite يستخدم الأعمدة الأولى من الفهرس المركّب للاستعلامات التي ترشّح بـ a فقط.

الجواب الصادق على سؤال "هل أضيف هذا الفهرس؟" يكون دائمًا تقريبًا: جرّبه، شغّل EXPLAIN QUERY PLAN، اقِس الأداء ببيانات واقعية، ثم اتخذ القرار.

بعد ذلك: الفهارس المركّبة

الفهرس على عمود واحد يغطي كثيرًا من الحالات، لكن الاستعلامات الحقيقية كثيرًا ما تُرشِّح وتُرتِّب على عدة أعمدة في آنٍ واحد. هنا يأتي دور الفهارس المركّبة — أي الفهارس على (a, b, c) — وترتيب الأعمدة فيها أهم بكثير مما يتوقع الناس. وهذا موضوع الصفحة التالية.

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

كيف أنشئ فهرساً في SQLite؟

استخدم CREATE INDEX index_name ON table_name(column_name);. ولو أردت ضمان عدم تكرار القيم، استخدم CREATE UNIQUE INDEX. لاحظ أن اسم الفهرس يجب أن يكون فريداً على مستوى قاعدة البيانات كلها، لا على مستوى الجدول فقط. ولحذفه: DROP INDEX index_name;.

متى يجب أن أضيف فهرساً في SQLite؟

أضف فهرساً على الأعمدة التي تستخدمها كثيراً في WHERE أو JOIN أو ORDER BY، خصوصاً عندما يكون الجدول كبيراً والاستعلام يُرجع نسبة صغيرة من الصفوف. تجنّب فهرسة كل عمود؛ فكل فهرس إضافي يُبطئ عمليات INSERT وUPDATE وDELETE ويستهلك مساحة على القرص. وتأكد دائماً عبر EXPLAIN QUERY PLAN أن المُخطِّط يستخدم الفهرس فعلاً.

لماذا لا يستخدم SQLite الفهرس الذي أنشأته؟

الأسباب الشائعة: الجدول صغير لدرجة أن المسح الكامل أرخص، أو أن العمود ملفوف داخل دالة (مثلاً WHERE lower(email) = ... لن يستفيد من فهرس على email)، أو أن الاستعلام يستخدم OR على أعمدة غير مفهرسة، أو أن إحصائيات المُخطِّط قديمة. شغّل ANALYZE لتحديث الإحصائيات، ثم EXPLAIN QUERY PLAN لترى ما الذي اختاره المُخطِّط.

كيف أعرض كل الفهارس الموجودة على جدول معيّن في SQLite؟

استخدم PRAGMA index_list('table_name'); لعرض فهارس جدول محدد، أو استعلم مباشرة من sqlite_master بهذا الشكل: SELECT name, sql FROM sqlite_master WHERE type = 'index';. الإدخالات التي تبدأ بـ sqlite_autoindex_ هي فهارس تلقائية ينشئها SQLite لقيود PRIMARY KEY وUNIQUE.

Coddy programming languages illustration

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

ابدأ الآن