مؤشرات PostgreSQL لتطبيقات SaaS: اختَر بين btree و GIN و GiST اعتمادًا على أشكال الاستعلام الحقيقية مثل الفلاتر، البحث، JSONB، والمصفوفات.

tenant_id = ?, status = 'active', created_at \u003e= ?, الانضمام users.id = orders.user_id، أو عرض "الأحدث أولًا" بـ ORDER BY created_at DESC.\n\n### GIN و GiST: عندما يحتوي الصف على قيم قابلة للبحث بكثرة\n\nGIN (Generalized Inverted Index) مناسب عندما يحتوي عمود واحد على أعضاء كثيرين وتسأل "هل يحتوي على X؟". هذا شائع مع مفاتيح JSONB، عناصر المصفوفات، ومتجهات البحث النصي الكامل.\n\nأشكال أمثلة: metadata @\u003e {'plan':'pro'} على JSONB، tags @\u003e ARRAY['urgent']، أو to_tsvector(body) @@ plainto_tsquery('reset password').\n\nGiST (Generalized Search Tree) يناسب الأسئلة المتعلقة بالمسافة أو التداخل، حيث تتصرف القيم كمدى أو أشكال. يُستخدم غالبًا لأنواع النطاقات، والبيانات الهندسية، وبعض عمليات "أقرب تطابق".\n\nأشكال أمثلة: نوافذ زمنية متداخلة مع أعمدة نطاق، بعض عمليات البحث بالتشابه (مثلًا مع مشغلات trigram)، أو استعلامات مكانية (إذا كنت تستخدم PostGIS).\n\nطريقة عملية للاختيار:\n\n- إذا كنت تُصفّي أو تُرتّب حسب أعمدة عادية، ابدأ بـ B-tree.\n- إذا كنت تتحقق من الاحتواء أو العضوية، انظر إلى GIN.\n- إذا تسأل "كم قريب" أو "هل يتداخل؟"، فعلى الأرجح GiST هو الأنسب.\n\n- إذا كان الاستعلام نادرًا أو الجدول صغير، قد لا تحتاج فهرسًا جديدًا.\n- إذا لم تستطع وصف شكل الاستعلام، قِس أولًا (EXPLAIN) قبل الإضافة.\n\nالفهارس تسرّع القراءات، لكنها تكلف وقتًا في الكتابة ومساحة. في SaaS، هذه المقايضة تؤثر أكثر على الجداول الساخنة مثل events و sessions و activity logs.\n\n## أنماط B-tree للتصفية والفرز والترقيم\n\nمعظم شاشات القوائم في SaaS تشترك في نفس الشكل: حدود مستأجر، بضعة مرشحات، وترتيب متوقّع. فهارس B-tree هي الاختيار الافتراضي هنا، وغالبًا ما تكون الأرخص للصيانة.\n\nنمط شائع هو WHERE tenant_id = ? بالإضافة إلى مرشحات مثل status = ?, user_id = ?, ونطاق زمني مثل created_at \u003e= ?. بالنسبة للفهارس المركبة B-tree، ضع مرشحات المساواة أولًا (الأعمدة التي تطابق بـ =)، ثم أضف العمود الذي ترتّب عليه.\n\nقواعد تعمل جيدًا في معظم التطبيقات:\n\n- ابدأ بـ tenant_id إذا كان كل استعلام مقيدًا بالمستأجر.\n- ضع مرشحات = التالية (غالبًا status, user_id).\n- ضع عمود الـ ORDER BY أخيرًا (غالبًا created_at أو id).\n- استخدم INCLUDE لتغطية صفحات القوائم دون جعل مفتاح الفهرس أعرض.\n- فضّل ترقيم البحث (seek pagination) بدل الـ offset عندما تتعمّق الصفحات.\n\nمثال واقعي: صفحة Tickets تعرض الأحدث أولًا، مُصفاة حسب الحالة.\n\nsql\n-- Query\nSELECT id, status, created_at, title\nFROM tickets\nWHERE tenant_id = $1\n AND status = $2\nORDER BY created_at DESC\nLIMIT 50;\n\n-- Index\nCREATE INDEX tickets_tenant_status_created_at_idx\nON tickets (tenant_id, status, created_at DESC)\nINCLUDE (title);\n\n\nهذا الفهرس يدعم كلًا من الفلترة والفرز، لذا يمكن لـ Postgres تجنّب فرز مجموعة نتائج كبيرة. جزء INCLUDE (title) يساعد صفحة القائمة على لمس صفحات جدول أقل، مع الحفاظ على مفاتيح الفهرس مركزة على التصفية والترتيب.\n\nلنطاقات زمنية، تنطبق نفس الفكرة:\n\nsql\nSELECT id, created_at\nFROM events\nWHERE tenant_id = $1\n AND created_at \u003e= $2\n AND created_at \u003c $3\nORDER BY created_at DESC\nLIMIT 100;\n\nCREATE INDEX events_tenant_created_at_idx\nON events (tenant_id, created_at DESC);\n\n\nالترقيم هو حيث تبطأ كثير من تطبيقات SaaS. ترقيم الـ offset (OFFSET 50000) يجبر قاعدة البيانات على التجاوز عبر الكثير من الصفوف. ترقيم seek يبقى سريعًا باستخدام مفتاح الفرز الأخير المرئي:\n\nsql\nSELECT id, created_at\nFROM tickets\nWHERE tenant_id = $1\n AND created_at \u003c $2\nORDER BY created_at DESC\nLIMIT 50;\n\n\nمع الفهرس B-tree الصحيح، يبقى هذا سريعًا حتى عندما يكبر الجدول.\n\n## فهرسة صديقة للمستأجر بدون إفراط في الفهرسة\n\nمعظم تطبيقات SaaS متعددة المستأجرين: كل استعلام يجب أن يبقى داخل مستأجر واحد. إذا لم تتضمن فهارسك tenant_id، يمكن لـ Postgres أن يجد الصفوف بسرعة، لكنه غالبًا ما يمسح المزيد من قيّم الفهرس أكثر مما يلزم. الفهارس الواعية بالمستأجر تُبقي بيانات كل مستأجر مجمعة في الفهرس بحيث تبقى الشاشات الشائعة سريعة ومتوقعة.\n\nقاعدة بسيطة: ضع tenant_id أولًا في الفهرس عندما يكون الاستعلام دائمًا مقيدًا بالمستأجر. ثم أضف العمود الذي تُصفّي أو ترتّب بناءً عليه أكثر.\n\nفهارس بسيطة لكنها ذات تأثير غالبًا ما تبدو كالتالي:\n\n- (tenant_id, created_at) لقوائم العناصر الحديثة وترقيم الـ cursor\n- (tenant_id, status) لتصفية الحالة (Open, Paid, Failed)\n- (tenant_id, user_id) لشاشات "العناصر المملوكة لهذا المستخدم"\n- (tenant_id, updated_at) لواجهات الإدارة "المعدلة مؤخرًا"\n- (tenant_id, external_id) للبحث من webhooks أو الاستيراد\n\nالإفراط في الفهرسة يحدث عندما تضيف فهرسًا جديدًا لكل شاشة مختلفة قليلًا. قبل إنشاء واحد آخر، تحقق مما إذا كان فهرس مركب موجود يغطي الأعمدة اليسرى المطلوبة. على سبيل المثال، إذا كان لديك (tenant_id, created_at), عادة لا تحتاج أيضًا (tenant_id, created_at, id) ما لم تكن فعلاً تُصفّي على id بعد تلك الأعمدة.\n\nالفهارس الجزئية يمكن أن تقلل الحجم وتكلفة الكتابة عندما لا تكون معظم الصفوف مهمة. تعمل جيدًا مع الحذف الناعم وبيانات "النشطة فقط"، على سبيل المثال: فهرس فقط حيث deleted_at IS NULL، أو حيث status = 'active'.\n\nكل فهرس إضافي يجعل الكتابات أثقل. الإدراجات يجب أن تحدّث كل فهرس، والتحديثات يمكن أن تمسّ عدة فهارس حتى عندما تغير عمودًا واحدًا. إذا كان تطبيقك يستقبل الكثير من الأحداث (بما في ذلك التطبيقات المبنية بسرعة مع Koder.ai)، احتفظ بالفهارس مركزة على أشكال الاستعلام القليلة التي يضربها المستخدمون كل يوم.\n\n## فهرسة JSONB: GIN وفهارس التعبير المستهدفة\n\nJSONB مفيد عندما يحتاج تطبيقك لحقول مرنة إضافية مثل أعلام الميزات، سمات المستخدم، أو إعدادات per-tenant. المشكلة أن عوامل تشغيل JSONB المختلفة تتصرّف بطرق مختلفة، لذا الفهرس الأفضل يعتمد على كيفية الاستعلام.\n\nشكلان مهمان:\n\n- الاحتواء: "هل يحتوي هذا JSON على أزواج مفتاح-قيمة هذه؟" باستخدام @\u003e.\n- استخراج المسار: "ما قيمة هذا الحقل المحدد؟" باستخدام -\u003e / -\u003e\u003e (غالبًا مقارَن مع =).\n\n### متى يكون GIN خيارًا مناسبًا\n\nإذا كنت غالبًا ما تُصفّي بـ @\u003e، فعادةً ما يدفع فهرس GIN على عمود JSONB نفسه.\n\nsql\n-- Query shape: containment\nSELECT id\nFROM accounts\nWHERE tenant_id = $1\n AND metadata @\u003e '{\"region\":\"eu\",\"plan\":\"pro\"}';\n\n-- Index\nCREATE INDEX accounts_metadata_gin\nON accounts\nUSING GIN (metadata);\n\n\nإذا كان هيكل JSON متوقعًا وتستخدم غالبًا @\u003e على المفاتيح العلوية، jsonb_path_ops يمكن أن يكون أصغر وأسرع، لكنه يدعم أنواعًا أقل من المشغلات.\n\n### متى يكون فهرس التعبير أفضل\n\nإذا كانت واجهة المستخدم تُصفّي بشكل متكرر على حقل واحد (مثل plan)، فإن استخراج ذلك الحقل وفهرسته غالبًا ما يكون أسرع وأرخص من GIN واسع.\n\nsql\nSELECT id\nFROM accounts\nWHERE tenant_id = $1\n AND metadata-\u003e\u003e'plan' = 'pro';\n\nCREATE INDEX accounts_plan_expr\nON accounts ((metadata-\u003e\u003e'plan'));\n\n\nقاعدة عملية: احتفظ بـ JSONB للسمات المرنة والنادرة التصفية، لكن ارفَع الحقول المستقرة وشديدة الاستخدام (plan, status, created_at) إلى أعمدة حقيقية. إذا كنت تتكرّر بسرعة في تطبيق مُولَّد، فغالبًا ما تكون تعديلة مخطط بسيطة بمجرد أن ترى أي مرشحات تظهر على كل صفحة.\n\nمثال: إذا خزّنت {\"tags\":[\"beta\",\"finance\"],\"region\":\"us\"} في JSONB، استخدم GIN عندما تُصفّي بحزم من السمات (@\u003e)، وأضف فهارس تعبيرية للمفاتيح القليلة التي تقود معظم عروض القوائم (plan, region).\n\n## فهرسة المصفوفات: متى يتألق GIN\n\nالمصفوفات تبدو جذابة لأنها سهلة التخزين والقراءة. عمود users.roles text[] أو projects.labels text[] يمكن أن يعمل جيدًا عندما تسأل غالبًا سؤالًا واحدًا: هل يحتوي هذا الصف على قيمة؟ هذا هو المكان الذي يساعد فيه GIN.\n\nGIN الخيار الرئيسي لاستعلامات العضوية على المصفوفات. يقوم بتفكيك المصفوفة إلى عناصر فردية ويبني بحثًا سريعًا للصفوف التي تحتويها.\n\nأشكال استعلام المصفوفة التي غالبًا ما تستفيد:\n\n- تحتوي على قيمة أو مجموعة: @\u003e (المصفوفة تحتوي)\n- تتداخل مع مجموعة: \u0026\u0026 (المصفوفة تشترك بأي عناصر)\n- أحيانًا: = ANY(...)، لكن @\u003e أكثر توقعًا\n\nمثال شائع لتصفية المستخدمين حسب دور:\n\nsql\n-- Find users who have the \"admin\" role\nSELECT id, email\nFROM users\nWHERE roles @\u003e ARRAY['admin'];\n\nCREATE INDEX users_roles_gin ON users USING GIN (roles);\n\n\nوتصفية المشاريع حسب مجموعة تسميات (يجب أن تتضمن كلا الوسمين):\n\nsql\nSELECT id, name\nFROM projects\nWHERE labels @\u003e ARRAY['billing', 'urgent'];\n\nCREATE INDEX projects_labels_gin ON projects USING GIN (labels);\n\n\nما يفاجئ البعض: بعض الأنماط لا تستخدم الفهرس كما تتوقع. إذا حولت المصفوفة إلى سلسلة (array_to_string(labels, ',')) ثم شغّلت LIKE، فلن يساعد فهرس GIN. أيضًا، إذا احتجت "يبدأ بـ" أو مطابقات ضبابية داخل التسميات، فأنت في مجال البحث النصي، وليس عضوية المصفوفة.\n\nالمصفوفات يمكن أن تصبح صعبة الصيانة عندما تتحول إلى قاعدة بيانات صغيرة: تحديثات متكررة، حاجة إلى بيانات وصفية لكل عنصر (من أضاف التسمية ومتى ولماذا)، أو حاجة تحليلات لكل تسمية. في تلك المرحلة، جدول ربط مثل project_labels(project_id, label) عادةً ما يكون أسهل للتحقق والاستعلام والتطوّر.\n\n## فهرسة البحث: النص الكامل والمطابقة الضبابية (GIN و GiST)\n\nلمربعات البحث، يظهر نمطان مرارًا وتكرارًا: البحث النصي الكامل (إيجاد سجلات حول موضوع) والمطابقة الضبابية (التعامل مع الأخطاء الإملائية، الأسماء الجزئية، وأنماط ILIKE). الفهرس المناسب هو الفرق بين "فوري" و"انتهاء المهلة عند 10k مستخدم".\n\n### البحث النصي الكامل: tsvector + GIN\n\nاستخدم البحث النصي الكامل عندما يكتب المستخدمون كلمات حقيقية وتريد نتائج مرتبة حسب الصلة، مثل البحث في التذاكر بالموضوع والوصف. الإعداد المعتاد هو تخزين tsvector (غالبًا في عمود مولد) وفهرسته بـ GIN. تبحث باستخدام @@ و tsquery.\n\nsql\n-- Tickets: full-text search on subject + body\nALTER TABLE tickets\nADD COLUMN search_vec tsvector\nGENERATED ALWAYS AS (\n to_tsvector('simple', coalesce(subject,'') || ' ' || coalesce(body,''))\n) STORED;\n\nCREATE INDEX tickets_search_vec_gin\nON tickets USING GIN (search_vec);\n\n-- Query\nSELECT id, subject\nFROM tickets\nWHERE search_vec @@ plainto_tsquery('simple', 'invoice failed');\n\n-- Customers: fuzzy name search using trigrams\nCREATE INDEX customers_name_trgm\nON customers USING GIN (name gin_trgm_ops);\n\nSELECT id, name\nFROM customers\nWHERE name ILIKE '%jon smth%';\n\n\nما يجب تخزينه في المتجه: الحقول فقط التي تبحث فيها فعليًا. إذا ضمّنت كل شيء (ملاحظات، سجلات داخلية)، ستدفع ثمنًا في حجم الفهرس وتكلفة الكتابة.\n\n### المطابقة الضبابية: الترايغرام مع GIN أو GiST\n\nاستخدم تشابه الترايغرام عندما يبحث المستخدمون عن أسماء أو بريد إلكتروني أو عبارات قصيرة وتحتاج لمطابقات جزئية أو تحمّل الأخطاء. الترايغرام يساعد مع ILIKE '%term%' ومشغلات التشابه. GIN عادةً ما يكون أسرع لـ "هل يطابق؟"؛ GiST قد يكون أنسب عندما تهتم أيضًا بترتيب النتائج حسب التشابه.\n\nقواعد سهلة:\n\n- استخدم GIN + tsvector للبحث النصي بحسب الأهمية.\n- استخدم الترايغرام للـ ILIKE والبحث المرن ضد الأخطاء الإملائية.\n\nالمزالق التي تستحق المراقبة:\n\n- البدل الرائد بدون ترايغرام (ILIKE '%abc') يجبر المسح.\n- المصطلحات القصيرة جدًا (حرف أو اثنين) غالبًا لا تستخدم الترايغرام جيدًا.\n- الكلمات المحجوزة والاشتقاق قد تفاجئ المستخدمين في نتائج النص الكامل، لذا اختر تكوينًا يتناسب مع لغة المنتج.\n\nإذا كنت تطلق شاشات بحث بسرعة، اعتبر الفهرس جزءًا من الميزة: تجربة البحث واختيار الفهرس يجب أن يُصمَّما معًا.\n\n## خطوة بخطوة: من الاستعلام البطيء إلى الفهرس الصحيح\n\nابدأ بالاستعلام الدقيق الذي يشغّله تطبيقك، لا بتخمين. "الشاشة البطيئة" عادةً ما تكون عبارة SQL واحدة مع WHERE و ORDER BY محدّدين. انسخها من السجلات، إخراج تصحيح ORM، أو أي أداة التقاط استعلامات تستخدمها.\n\nسير عمل يصلح في التطبيقات الحقيقية:\n\n- التقط SQL الكامل، بما في ذلك WHERE و ORDER BY و LIMIT.\n- شغّل EXPLAIN (ANALYZE, BUFFERS) على نفس الاستعلام.\n- ركّز على المشغلات التي تقوم بالعمل (=, \u003e=, LIKE, @\u003e, @@)، وليس فقط أسماء الأعمدة.\n- أضف أصغر فهرس يطابق تلك المشغلات.\n- أعد تشغيل EXPLAIN (ANALYZE, BUFFERS) بحجم بيانات واقعي.\n\nإليك مثال ملموس. صفحة Customers تُصفّي حسب المستأجر والحالة، ترتّب بالأحدث، وتُرقّم:\n\nsql\nSELECT id, created_at, email\nFROM customers\nWHERE tenant_id = $1 AND status = $2\nORDER BY created_at DESC\nLIMIT 50;\n\n\nإذا أظهر EXPLAIN مسحًا تسلسليًا وفرزًا، ففهرس B-tree الذي يطابق الفلتر والفرز غالبًا ما يصلح المشكلة:\n\nsql\nCREATE INDEX ON customers (tenant_id, status, created_at DESC);\n\n\nإذا كان الجزء البطيء هو فلترة JSONB مثل metadata @\u003e '{\"plan\":\"pro\"}'، فهذا يشير إلى GIN. إذا كان بحثًا نصيًا مثل to_tsvector(...) @@ plainto_tsquery(...)، فهذا أيضًا يشير إلى فهرس بحث مدعوم بـ GIN. إذا كانت المشغلات من نوع "أقرب تطابق" أو تداخل، فهنا غالبًا ما يناسب GiST.\n\nبعد إضافة الفهرس، قِس المقايضة. تحقق من حجم الفهرس، وقت الإدراج والتحديث، وما إذا كان يساعد الاستعلامات البطيئة الثلاثة الأولى أم فقط حالة هامشية واحدة. في المشاريع سريعة الحركة (بما في ذلك المبنية على Koder.ai)، هذه المراجعة تمنع تراكم فهارس غير مستخدمة.\n\n## أخطاء شائعة في الفهرسة تضيع الوقت والمال\n\nمعظم مشاكل الفهرس ليست حول اختيار B-tree مقابل GIN مقابل GiST. إنها حول بناء فهرس يبدو صحيحًا، لكن لا يتطابق مع طريقة التطبيق في الاستعلام عن الجدول.\n\nأخطاء تؤذي غالبًا:\n\n- فهارس لا تُستخدم أبدًا. الاستعلام يستخدم مشغّلًا مختلفًا عن نوع الفهرس، أو فهرس مركب له ترتيب أعمدة خاطئ. إذا كان WHERE يبدأ بـ tenant_id و created_at، لكن الفهرس يبدأ بـ created_at، فقد يتجاهله المخطط.\n- فهرسة أعمدة قليلة التغاير وحدها. فهرس على status, is_active, أو boolean عادة يفيد قليلًا لأنّه يطابق كثيرًا من الصفوف. اقترن به بعمود انتقائي (مثل tenant_id أو created_at) أو تجاوزه.\n- فهارس متداخلة تزيد الحجم وتبطئ الكتابات. الفهارس المشابهة على نفس الجدول يمكن أن تضاعف التخزين وتبطئ الإدراجات والتحديثات.\n- ثغرات الترقيم. فهرسة فقط لدعم OFFSET فخ شائع. إذا تستخدم ترقيم keyset، تحتاج فهرسًا يطابق الفرز وفلتر "آخر مرّة شوهد".\n- إحصاءات قديمة وتناثر في الجدول. إذا لم يقم autovacuum بعمله، أو لم تُشغَّل ANALYZE مؤخرًا، يمكن أن يختار المخطط خططًا سيئة حتى عندما يوجد الفهرس الصحيح.\n\nمثال ملموس: شاشة Invoices تُصفّي بـ tenant_id و status، ثم ترتّب بـ created_at DESC. فهرس على status فقط لن يُفيد كثيرًا. الأنسب فهرس مركب يبدأ بـ tenant_id ثم status ثم created_at (فلتر أولًا، فرز آخرًا). هذا التغيير الواحد غالبًا يتفوق على إضافة ثلاثة فهارس منفصلة.\n\nعامل كل فهرس على أنه تكلفة. عليه أن يُبرّر وجوده في استعلامات حقيقية، لا في النظرية فقط.\n\n## قائمة تحقق سريعة قبل شحن تغييرات الفهارس\n\nتغييرات الفهارس سهلة الشحن ومزعجة لإلغائها إذا أضافت تكلفة كتابة أو قفلت جدولًا مزدحمًا. قبل الدمج، اعتبرها إصدارًا صغيرًا.\n\nابدأ بتحديد ما الذي تحسنه. استخرج ترتيبين قصيرين من سجلاتك أو مراقبتك: الاستعلامات التي تُشغّل أكثر، والاستعلامات التي تملك أعلى زمن استجابة. لكلٍّ منها، اكتب الشكل الدقيق: أعمدة الفلترة، ترتيب النتائج، الانضمامات، والمشغلات المستخدمة (equals, range, IN, ILIKE, مشغلات JSONB، احتواء المصفوفة). هذا يمنع التخمين ويساعدك في اختيار نوع الفهرس الصحيح.\n\nقائمة فحص قبل الشحن:\n\n- تأكد أن الاستعلام مقيد بالمستأجر حيث يجب أن يكون ذلك.\n- طابق الفهرس مع المشغّل: B-tree للمساواة/النطاق/الفرز، GIN للعضوية (المصفوفات، JSONB، النص الكامل)، GiST لحالات التداخل أو القرب.\n- فضّل فهرسًا مركبًا واحدًا يطابق الفلتر الشائع + الفرز بدلاً من عدة فهارس عمودية.\n- حافظ على الفهرس ضيقًا: أدرج فقط الأعمدة التي يستخدمها الاستعلام فعلاً.\n- خطط للطرح: هل إنشاء الفهرس سيقفل الكتابات، وهل تحتاج جدولته في وقت هادئ؟\n\nبعد إضافة الفهرس، تحقق أنه ساعد في الخطة الحقيقية. شغّل EXPLAIN (ANALYZE, BUFFERS) على الاستعلام الدقيق وقارن قبل وبعد. ثم راقب سلوك الإنتاج ليوم:\n\n- هل انخفض زمن القراءة للشاشات المستهدفة؟\n- هل زاد زمن الكتابة (إدراجات/تحديثات)؟\n- هل ارتفع CPU أو التخزين بسبب الفهرس الجديد؟\n- هل الفهرس مستخدم فعلًا أم أنه وزن ميت؟\n\nإذا كنت تبني مع Koder.ai، يجدر بك الاحتفاظ بـ SQL المُولَّد لشاشة أو شاشتين بطيئتين جنبًا إلى جنب مع التغيير، بحيث يطابق الفهرس ما يشغله التطبيق فعليًا.\n\n## مثال: فهرسة تدفّق عمل SaaS نموذجي وخطوات تالية\n\nتخيّل شاشة إدارة شائعة: قائمة Users مع تقييد مستأجر، بعض المرشحات، ترتيب حسب آخر نشاط، ومربع بحث. هنا تتحول الفهارس من نظرية إلى توفير وقت حقيقي.\n\nثلاث أشكال استعلام سترى عادة:\n\nsql\n-- 1) List page with tenant + status filter + sort\nSELECT id, email, last_active_at\nFROM users\nWHERE tenant_id = $1 AND status = $2\nORDER BY last_active_at DESC\nLIMIT 50;\n\n-- 2) Search box (full-text)\nSELECT id, email\nFROM users\nWHERE tenant_id = $1\n AND to_tsvector('simple', coalesce(name,'') || ' ' || coalesce(email,'')) @@ plainto_tsquery($2)\nORDER BY last_active_at DESC\nLIMIT 50;\n\n-- 3) Filter on JSON metadata (plan, flags)\nSELECT id\nFROM users\nWHERE tenant_id = $1\n AND metadata @\u003e '{\"plan\":\"pro\"}'::jsonb;\n\n\nمجموعة فهارس صغيرة لكنها متعمدة لهذه الشاشة:\n\n- B-tree مركب لعرض القائمة: (tenant_id, status, last_active_at DESC).\n- GIN للبحث: عمود tsvector مولَّد مع فهرس GIN.\n- فهرسة JSONB بناءً على الاستخدام: GIN (metadata) عندما تستخدم @\u003e كثيرًا، أو فهرس تعبيري B-tree مثل ((metadata-\u003e\u003e'plan')) عندما تُصفّي غالبًا على مفتاح واحد.\n\nالاحتياجات المختلطة عادية. إذا كانت صفحة تقوم بالفلاتر + البحث + JSON، تجنّب حشر كل شيء في فهرس عملاق. احتفظ بـ B-tree للفرز/الترقيم، ثم أضف فهرسًا متخصصًا واحدًا (غالبًا GIN) للجزء المكلف.\n\nخطوات قادمة: اختر شاشة بطيئة واحدة، اكتب أعلى 2-3 أشكال استعلام لها، وراجع كل فهرس حسب الغرض (فلترة، فرز، بحث، JSON). إذا لم يطابق الفهرس بصراحة استعلامًا حقيقيًا، أزله من الخطة. إذا كنت تتكرر بسرعة على koder.ai، فإن إجراء هذه المراجعة أثناء إضافة شاشات جديدة يمكن أن يمنع توسع الفهارس بينما مخططك لا يزال يتغير.فهرس يسمح لـ PostgreSQL بالعثور على الصفوف المطابقة دون قراءة معظم الجدول. لشاشات SaaS الشائعة مثل القوائم ولوحات المعلومات ومحركات البحث، الفهرس المناسب يمكن أن يحول عملية المسح المتسلسلة البطيئة إلى بحث سريع يتدرج أفضل مع نمو الجدول.
ابدأ بـ B-tree لمعظم استعلامات التطبيق لأنه الأفضل لـ =، ونطاقات القيم، والـ joins، وORDER BY. إذا كان استعلامك يدور حول الاحتواء (JSONB، مصفوفات) أو البحث النصي، فـ GIN عادةً ما يكون الخيار التالي؛ GiST مخصّص أكثر للحالات التي تتعلق بالتداخل أو البحث عن الأقرب/الأقرب تشابهًا.
ضع الأعمدة التي تُصفى باستخدام = أولاً، ثم ضع العمود الذي ترتّب به النتائج في النهاية. هذا الترتيب يتوافق مع طريقة سير عمل مخطط الاستعلام في المشي عبر الفهرس بكفاءة ليُصفّي ويعيد الصفوف بالترتيب الصحيح دون فرز إضافي.
إذا كان كل استعلام مقيدًا بـ tenant_id، فإن وضع tenant_id أولًا يجمع صفوف كل مستأجر داخل الفهرس مما يقلل كمية بيانات الفهرس والجدول التي يحتاج PostgreSQL لمسّها لشاشات القوائم اليومية.
INCLUDE يتيح إضافة أعمدة إضافية لدعم قراءات معتمدة على الفهرس فقط (index-only reads) لصفحات القوائم دون توسيع مفتاح الفهرس نفسه. يكون مفيدًا عندما تصفّي وتُرتّب بعدة أعمدة لكن تعرض أيضًا حقولًا إضافية على الشاشة.
استخدم فهرس جزئي عندما تهتم فقط بجزء من الصفوف، مثل "غير المحذوف" أو "نشط فقط". يبقي الفهرس أصغر وأرخص للصيانة، وهو مهم للجداول الساخنة التي تتلقى إدراجات وتحديثات كثيرة.
استخدم GIN على عمود JSONB عندما تستعلم كثيرًا باستخدام الاحتواء مثل metadata @\u003e '{"plan":"pro"}'. إذا كنت في معظم الحالات تُصفّي حسب مفتاح أو مفتاحين محددين داخل JSON، ففهرس تعبيري B-tree على (metadata-\u003e\u003e'plan') يكون أصغر وأسرع غالبًا.
GIN مناسب جدًا عندما يكون السؤال الرئيسي هو "هل تحتوي هذه المصفوفة على X؟" باستخدام مشغلات مثل @\u003e أو \u0026\u0026. إذا احتجت بيانات وصفية لكل عنصر، أو تحديثات متكررة، أو تحليلات لكل تسمية/دور، فجدول الربط (join table) عادةً ما يكون أسهل للصيانة والفهرسة.
للبحث النصي الكامل، خزّن tsvector (غالبًا كعمود مولد) وافهرسه بـ GIN، وابحث باستخدام @@ للحصول على نتائج مصنفة حسب الأهمية. للمطابقة الضبابية مثل ILIKE '%name%' وتحمل الأخطاء الإملائية، فمؤشرات الترايغرام (trigram) — غالبًا مع GIN — هي الأداة المناسبة.
انسخ SQL الدقيق الذي تشغّله تطبيقك وشغّل EXPLAIN (ANALYZE, BUFFERS) لترى أين تضيع الوقت وهل تقوم بمسح أو فرز أو تطبيق فلاتر مكلفة. أضف أصغر فهرس يطابق مشغلات الاستعلام وترتيب الفرز، ثم أعد تشغيل نفس EXPLAIN لتتأكد من أنه يُستخدم ويُحسن الخطة.