بنناجح
المرحلة 3: الـ Index في الشغل · الدرس 4 من 4 · 20 دقيقة

أسئلة إنترفيو وملخص

14 سؤال بمستوى إنترفيوهات الـ Backend في الشركات الكبيرة، من Mid لحد Staff. وفي الآخر ملخص للموضوع كله في كروت صغيرة.

في الدرس ده هتتعلم
  • تجاوب أسئلة الـ Indexes من Mid لحد Staff
  • تراجع الموضوع كله في ملخص واحد

1. أسئلة إنترفيو

أسئلة بمستوى إنترفيوهات الـ Backend في الشركات الكبيرة. فكّر في كل سؤال قبل ما تفتح الإجابة.

Mid

ليه الداتابيز بتستخدم B+Tree للـ Indexes، مش Hash Table ولا Binary Search Tree؟

  • Hash Table: ممتاز في التساوي O(1)، بس مفيش Range (>، BETWEEN) ولا ORDER BY ولا البحث ببداية النص.
  • Binary Search Tree: مفتاح واحد في كل Node، فمليون مفتاح ≈ 20 مستوى = 20 قراءة Disk عشوائية. وممكن تبقى غير متوازنة.
  • B+Tree: كل Node = Page فيها مئات المفاتيح (Fanout عالي)، فالعمق 3–4 حتى لمليارات الصفوف. متوازنة دايماً، والأوراق مربوطة فالـ Range Scan قراية متسلسلة.

الإجابة القوية بتربط ده بإن وحدة القراية من الـ Disk هي الـ Page، والهدف تقليل عدد الـ Pages.

Senior

جدول فيه مليار صف و Index على BIGINT. قدّر عمق الـ B+Tree وكام قراءة Disk هيحتاجها البحث بالتساوي.

  • Page = 16 KB، والمدخل الداخلي ≈ 15–20 byte، فالـ Fanout ≈ 800–1,000.
  • 1,000² = مليون، و1,000³ = مليار، يعني العمق حوالي 3 (4 لو الأوراق أقل امتلاءً).
  • الـ Root والمستوى التاني غالباً في الـ Buffer Pool دايماً، فعملياً 0–2 قراءة Disk.
  • في InnoDB، لو Secondary والكويري مش Covering: بحث تاني في الـ Clustered (3 مستويات تقريباً، والعليا في الـ RAM).

المحاور بيختبر التقدير بالأرقام، مش الحفظ.

Senior

إيه الفرق بين Clustered و Secondary Index في InnoDB؟ وليه الـ Covering Index بيفرق هناك أكتر من PostgreSQL؟

  • InnoDB: الجدول هو الـ Clustered Index (مترتب بالـ PK، والأوراق فيها الصف كامل).
  • الـ Secondary أوراقه فيها العمود + الـ PK، فأي بحث بيه محتاج Lookup تاني في الـ Clustered.
  • Covering ← البحث التاني بيتلغي (Using index). ده بيفرق أضعاف لو الكويري بترجع صفوف كتير، لأن كل Lookup عشوائي.
  • PostgreSQL: الـ Index بيشاور على الـ Heap بالـ TID مباشرة، والـ Index-Only Scan محتاج كمان Visibility Map محدّثة.
  • كمان في InnoDB: حجم الـ PK بيتكرر في كل Secondary.
Senior

فريقك عايز UUIDv4 كـ Primary Key في MySQL. إيه المشاكل المتوقعة؟ وإيه البديل؟

  • إضافات عشوائية في الـ Clustered Index ← Page Splits في كل مكان، وصفحات نص فاضية (Fragmentation)، والجدول والـ Indexes بيكبروا.
  • الـ Buffer Pool بيتضغط: الكتابة محتاجة الجدول كله تقريباً في الـ RAM، بدل آخر كام Page.
  • الحجم: CHAR(36) = 36 byte على الأقل، وبيتكرر في كل Secondary Index.
  • البدائل: UUIDv7 أو ULID كـ BINARY(16). أو PK رقمي داخلي + عمود UUID عليه Unique للاستخدام الخارجي.
Mid

Index على (a, b, c). أنهي كويري هتستفيد، وإزاي؟ a=1 AND c=3 · b=2 ORDER BY a · a=1 AND b>5 AND c=3 · a IN (1,2) AND b=4 · a=1 ORDER BY b, c

  • a = 1 AND c = 3: البحث بـ a بس، و c بيتفلتر (ICP في MySQL).
  • b = 2 ORDER BY a: مفيش a في الشرط. ممكن Full Index Scan للترتيب، أو Skip Scan في الداتابيز اللي بتدعمها.
  • a = 1 AND b > 5 AND c = 3: بحث بـ a + Range على b، و c فلتر بس.
  • a IN (1, 2) AND b = 4: الـ IN = مجموعة قيم بالتساوي ← بحثين (1,4) و (2,4)، والعمودين اتستخدموا.
  • a = 1 ORDER BY b, c: ممتاز. النتايج طالعة مترتبة، فمفيش Sort.
Senior

ضفت Index، والـ EXPLAIN لسه بيقول Full Table Scan. اذكر كل الأسباب المحتملة وإزاي تتأكد من كل واحد.

  1. Selectivity ضعيفة: الشرط بيرجع نسبة كبيرة، أو الصفوف متفرقة على الـ Disk.
  2. إحصائيات قديمة: ANALYZE TABLE، وقارن الـ rows المتوقعة بالحقيقية في EXPLAIN ANALYZE.
  3. الشرط مش Sargable: دالة، أو Implicit Conversion، أو Collation مختلفة في JOIN. راجع الـ SQL المتولد من الـ ORM، مش كود PHP.
  4. ترتيب الـ Composite غلط: مش على الـ Leftmost Prefix.
  5. الجدول صغير: الـ Scan أرخص فعلاً.
  6. OR، !=، LIKE '%x'.
  7. الـ Lookups غالية: فكّر في Covering.

FORCE INDEX للتجربة بس. الحل الدائم إنك تعالج السبب.

Senior

الكويري دي بطيئة على 100 مليون طلب. صمم الـ Index المناسب: WHERE tenant_id = ? AND status = 'pending' ORDER BY created_at DESC LIMIT 20

  • كل الشروط تساوي، وبعدها ترتيب. حسب ESR: (tenant_id, status, created_at).
  • الداتابيز بتنزل لمجموعة (tenant, pending) اللي مترتبة أصلاً بالوقت، وتقرا آخر 20 بالعكس (Backward Index Scan) وتقف. مفيش Sort، ومفيش صفوف زيادة.
  • Covering في MySQL: ضيف total في الآخر. والـ id موجود أصلاً.
  • PostgreSQL: ممكن Partial Index: ON orders (tenant_id, created_at DESC) WHERE status = 'pending'، أصغر بكتير.
  • tenant_id الأول لأن كل كويريز الـ Multi-tenant بتبدأ بيه.
Senior

ليه LIMIT 20 OFFSET 1000000 بطيئة حتى مع وجود Index؟ وإيه الحل؟

  • الـ OFFSET مش قفزة: الداتابيز بتمشي على أول مليون صف وترميهم، O(offset + limit). ولو مش Covering، ممكن تعمل Lookup لكل واحد فيهم كمان.
  • Keyset Pagination: WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT 20. تكلفة ثابتة مهما بعدت. وفي Laravel cursorPaginate().
  • العيب: مفيش “روح لصفحة 5000”. مقبول في الـ Infinite Scroll والـ APIs.
  • حل وسط: Deferred Join: هات الـ ids بكويري Covering بالـ OFFSET، وبعدين JOIN للـ 20 صف.
Staff

محتاج تضيف Index على جدول 2 TB في MySQL شغال 24/7 وعليه Replicas. اشرح خطتك.

  • قبل: اتأكد إنه مطلوب (EXPLAIN على Replica بحجم الإنتاج). احسب مساحة الـ Disk للبناء.
  • الطريقة: Online DDL ممكن، بس على الحجم ده: Replication Lag كبير، و Metadata Lock ممكن يقف ورا Transaction طويلة.
  • علشان كده: gh-ost (بيقرا الـ Binlog، فيه Throttling حسب الـ Lag والحمل، وتقدر توقفه وتكمّله) أو pt-online-schema-change (Triggers).
  • التنفيذ: وقت ضغط قليل، و lock_wait_timeout صغير، ومراقبة الـ Lag والـ IO، وخطة Rollback.
  • بعد: ANALYZE TABLE، واتأكد إن الكويريز بتستخدمه وإن الكتابة ما بطّأتش.
Senior

Background Job بيعمل UPDATE، وفجأة كل الـ Checkout وقف بـ Lock wait timeout. إيه علاقة ده بالـ Indexes؟

  • في InnoDB (REPEATABLE READ)، الـ UPDATE بيقفل كل الـ Index Records اللي بيعدّي عليها + الفراغات (Next-Key Locks).
  • الشرط على عمود من غير Index ← Full Scan ← الجدول كله تقريباً اتقفل.
  • الحل: Index على عمود الشرط + Batches صغيرة بالـ PK في Transactions قصيرة.
  • اختياري: READ COMMITTED بيقلل الـ Gap Locks لو مقبول منطقياً.
Senior

في PostgreSQL، عملت Covering Index بـ INCLUDE، والـ EXPLAIN بيوري Index Only Scan بس Heap Fetches عالية. ليه؟

  • الـ Index ملوش معلومات Visibility (MVCC)، فلازم يتأكد إن الصف مرئي.
  • لو الـ Page متعلّمة all-visible في الـ Visibility Map ← مفيش Heap. لو لأ ← Heap Fetch.
  • الجدول عليه تعديلات كتير، والـ Autovacuum مش ملاحق.
  • الحل: VACUUM، وظبط الـ Autovacuum للجدول ده (autovacuum_vacuum_scale_factor أصغر).
Staff

جدول Events بيستقبل 50,000 Insert في الثانية، والقراية قليلة وبالوقت. إزاي تفكر في الـ Indexes؟

  • كل Index = كتابة زيادة × 50,000 في الثانية ← أقل عدد ممكن.
  • PK متسلسل (BIGINT أو UUIDv7) علشان الإضافة في آخر الشجرة.
  • PostgreSQL: BRIN على created_at: صغير جداً ورخيص لأن الداتا مترتبة طبيعياً.
  • Partitioning بالوقت: Indexes صغيرة، ومسح القديم = DROP PARTITION بدل DELETE ضخم.
  • أكبر من كده: LSM-Tree (Cassandra، ScyllaDB، RocksDB/MyRocks): كتابة متسلسلة ودمج بعدين. بتدفع في القراية (Read Amplification) بدل الكتابة (Write Amplification).
  • أو فصل: Events في Kafka / ClickHouse، والعلائقية للـ Transactions.
Senior

إزاي تعمل LIKE '%keyword%' سريع على 50 مليون منتج؟

  • B-Tree مش هينفع: البداية مش معروفة، فمفيش نقطة تبدأ منها.
  • PostgreSQL: pg_trgm + GIN: بيقسم النص لـ Trigrams ويعمل Inverted Index. بيدعم LIKE '%x%' و ILIKE.
  • MySQL: FULLTEXT + MATCH … AGAINST، بس ده بحث بالكلمات. وفيه Parser اسمه ngram.
  • أكبر: Elasticsearch / OpenSearch / Meilisearch بـ Inverted Index + Ranking + Typos، والـ Sync بـ Laravel Scout أو CDC (Debezium).
  • للعربي: Normalization قبل الـ Indexing (أ/إ/آ ← ا، ة ← ه، شيل التشكيل)، وإلا “أحمد” و“احمد” كلمتين مختلفتين.
Staff

كـ Tech Lead، إزاي تمنع الـ Indexes في مشروعك تبقى فوضى مع الوقت؟

  • Code Review للـ Migrations: أي Index جديد معاه الكويري اللي بيخدمها + EXPLAIN قبل وبعد.
  • مراقبة مستمرة: Slow Query Log أو pg_stat_statements + Dashboard لأبطأ الكويريز وأكترها تكراراً.
  • مراجعة دورية للمش مستخدم والمكرر (والأرقام بتتصفّر مع الـ Restart).
  • Staging بحجم الإنتاج.
  • CI check: EXPLAIN على الكويريز الحرجة يفشل لو ظهر ALL على جدول كبير.
  • أسماء واضحة: idx_orders_tenant_status_created.

2. ملخص سريع

الفكرة

  • الداتابيز بتقرا Pages (InnoDB 16KB، PostgreSQL 8KB)، والتكلفة = عدد الـ Pages.
  • من غير Index: Full Scan O(N). بـ Index: B+Tree O(log_f N).
  • Fanout حوالي 1,000 ← مليار صف = 3 مستويات.
  • الأوراق مربوطة ببعض ← الـ Range Scans سريعة.

InnoDB و PostgreSQL

  • InnoDB: الجدول = Clustered Index بالـ PK. الـ Secondary فيه PK ← Lookup تاني.
  • PostgreSQL: Heap. كل الـ Indexes بتشاور بـ TID.
  • PK في InnoDB: متسلسل (BIGINT / UUIDv7 / ULID). بلاش UUIDv4.
  • PostgreSQL: HOT Updates لو مفيش عمود عليه Index اتغير.

Composite Index (a, b, c)

  • Leftmost Prefix: a · a,b · a,b,c ✓. و b لوحده ✗.
  • ترتيب الشروط في الـ WHERE مش مهم.
  • أول Range بيوقف استخدام اللي بعده في البحث.
  • رتّب بـ ESR: Equality ← Sort ← Range.
  • WHERE a = ? ORDER BY b ← من غير Sort.

حاجات بتعطّل الـ Index

  • دالة على العمود: DATE(col)، LOWER(col)، col + 1
  • رقم مع عمود VARCHAR (Implicit Conversion في MySQL)
  • LIKE '%x' (البداية مش معروفة)
  • OR على عواميد مختلفة، !=، NOT IN
  • Selectivity ضعيفة، أو إحصائيات قديمة
  • الحل: Range على العمود، Functional Index، Full-Text، Trigram

EXPLAIN

  • MySQL type: const ← eq_ref ← ref ← range ← index ← ALL ✗
  • Using index = Covering ✓ · Using filesort / Using temporary ⚠️
  • PostgreSQL: Index Only Scan ✓ · Bitmap Heap Scan · Seq Scan
  • EXPLAIN ANALYZE: قارن الـ rows المتوقعة بالحقيقية.

Laravel

  • $table->index(['user_id', 'created_at']);
  • PostgreSQL: foreignId('user_id')->index()->constrained()
  • whereDate() ✗ ← where('created_at', '>=', …) ✓
  • OFFSET العميق ✗ ← cursorPaginate() ✓
  • whereFullText() + $table->fullText()
  • CONCURRENTLY ← $withinTransaction = false

التمن

  • كل Index = كتابة إضافية مع كل INSERT / UPDATE / DELETE.
  • الـ Unique مش بيستفيد من الـ Change Buffer ← أغلى في الكتابة.
  • UPDATE من غير Index في InnoDB ← بيقفل الجدول تقريباً.
  • بينافس على الـ Buffer Pool.

الإنتاج

  • MySQL: ALGORITHM=INPLACE, LOCK=NONE · gh-ost · pt-osc
  • PostgreSQL: CREATE INDEX CONCURRENTLY (لو فشل ← INVALID)
  • مش مستخدم: sys.schema_unused_indexes · pg_stat_user_indexes
  • جرّب الحذف: ALTER INDEX … INVISIBLE (MySQL 8)
  • Bloat: REINDEX INDEX CONCURRENTLY (PostgreSQL 12+)
علّم عليه لما تقدر تجاوب أسئلة الإنترفيو من غير ما تبص.