> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أسئلة مقابلة مطور Backend من المبتدئ إلى الخبير
- URL: https://arabroot.io/articles/اسئلة-مقابلة-مطور-backend-من-المبتدئ-الى-الخبير/
- Published: 2026-10-13T05:00:00.000Z
- Updated: 2026-10-12T21:41:00.000Z
- Description: بنك أسئلة لمقابلة مطور Backend، لكل سؤال إجابة المبتدئ وإجابة الخبير وأسئلة متابعة، مع تمارين عملية شغلناها فعلاً.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الفرق والمنتج, استراتيجية تقنية, الاستضافة الذاتية

تسأل مرشحاً لوظيفة Backend عن الفهرس Index فيجيب بالتعريف الصحيح، وتسأله عن المعاملات Transactions فيعدد أحرف ACID الأربعة، وتخرج من المقابلة دون أن تعرف هل يستطيع أن يجد لماذا قبل النظام خمسين عملية بيع لعشر تذاكر، أو لماذا أصبحت صفحة الطلبات ترسل 201 استعلام بعد تعديل صغير. والسبب أن الأسئلة التي تقف عند التعريف تقيس الحفظ، والحفظ اليوم متاح لكل من يملك أداة ذكاء اصطناعي.

وفي [دليل إدارة المقابلة التقنية](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AF%D9%8A%D8%B1-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D8%AA%D9%82%D9%86%D9%8A%D8%A9-%D8%AA%D9%83%D8%B4%D9%81-%D9%85%D8%B3%D8%AA%D9%88%D9%89-%D8%A7%D9%84%D9%85%D8%B1%D8%B4%D8%AD/) شرحنا الطريقة: المستويات، وسلم أسئلة المتابعة، والتقييم بالدرجات، وهذا المقال هو بنك الأسئلة لمطور Backend، وفيه 47 سؤالاً في كل مجالات العمل اليومي، ولكل سؤال إجابة المبتدئ وإجابة الخبير وأسئلة متابعة وعلامات تحذير ورابط للمقال الذي يشرح الموضوع في عرب رووت، ثم ثلاثة تمارين عملية شغلناها فعلاً.

وسوف نناقش في هذا المقال ما يلي:

- كيف تستخدم بنك الأسئلة في مقابلة واحدة.
- الأسئلة مرتبة حسب المجال، من HTTP إلى تصميم الأنظمة ومراجعة كود الذكاء الاصطناعي.
- ثلاثة تمارين عملية: استعلامات N+1، وحالة سباق في بيع التذاكر، وتقسيم الصفحات في جدول بمليون صف.

## كيف تستخدم هذه الأسئلة؟

لا تسأل كل الأسئلة، فهذا بنك تختار منه، والقاعدة أن تأخذ لكل جلسة مدتها ساعة ثمانية أسئلة تقريباً من ثلاثة أو أربعة مجالات، مع تمرين عملي واحد، وتترك الوقت لأسئلة المتابعة لأنها ما يكشف المستوى. ولكل سؤال مستوى (مبتدئ أو متوسط أو خبير) يبين المستوى الذي يكون عنده السؤال في محله، وليس المستوى الوحيد الذي تسأله له، فسؤال المبتدئ عن N+1 يكشف خبيراً إذا صعدت فيه بأسئلة المتابعة.

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

والمجالات في هذا المقال:

- [ال HTTP وREST ورموز الحالة](#http) (4 أسئلة)
- [تصميم ال API والإصدارات](#api-design) (4 أسئلة)
- [قواعد البيانات وSQL والفهارس](#sql) (4 أسئلة)
- [المعاملات والعزل والأقفال](#transactions) (4 أسئلة)
- [التخزين المؤقت Caching](#caching) (3 أسئلة)
- [الطوابير والرسائل](#queues) (3 أسئلة)
- [التزامن والخيوط ومجمع الاتصالات](#concurrency) (3 أسئلة)
- [المصادقة والصلاحيات](#auth) (4 أسئلة)
- [أمن التطبيقات OWASP](#security) (3 أسئلة)
- [الاختبارات](#testing) (3 أسئلة)
- [المراقبة والسجلات Observability](#observability) (3 أسئلة)
- [تشخيص الأداء](#performance) (سؤالان)
- [تصميم الأنظمة: حالات قصيرة](#system-design) (3 أسئلة)
- [أعطال الإنتاج والنشر](#incidents) (سؤالان)
- [مراجعة كود الذكاء الاصطناعي](#ai-code) (سؤالان)
- [تمارين عملية للمقابلة](#exercises)
- [الخلاصة](#summary)

## ال HTTP وREST ورموز الحالة

### 1\. ما الفرق بين الرمز 401 والرمز 403، ومتى ترجع 404 بدلاً منهما؟

مبتدئ **ماذا يختبر:** هل يفهم معنى رموز الحالة Status Codes كعقد مع الكلاينت، وليس كأرقام يحفظها.

إجابة المبتدئ

401 يعني أن المستخدم لم يسجل الدخول أو أن التوكن غير صالح، و403 يعني أنه مسجل ولكن ليس لديه صلاحية، و404 عندما لا يوجد المورد.

إجابة الخبير

401 Unauthorized تعني في الحقيقة «لم تثبت هويتك»، ويجب أن يأتي معها `WWW-Authenticate` بحسب [مواصفة HTTP في RFC 9110](https://www.rfc-editor.org/rfc/rfc9110?ref=arabroot.io#name-401-unauthorized)، و403 Forbidden تعني «عرفناك ولكن لا يسمح لك». وفي بعض الموارد الحساسة نرجع 404 بدلاً من 403 عمداً، والسبب أن 403 تخبر المهاجم أن الطلب رقم 1234 موجود فعلاً، وهذا تسريب يساعد في تعداد الموارد Enumeration. والمهم أن يكون الاختيار متسقاً في كل ال API، لأن الكلاينت يبني منطقه عليه: 401 تعني «حدث التوكن أو أعد تسجيل الدخول»، و403 تعني «لا تحاول مرة أخرى».

أسئلة المتابعة
- التوكن منتهي الصلاحية، أي رمز ترجعه، وماذا يفعل تطبيق الجوال عندما يستلمه؟
- متى ترجع 409 ومتى 422؟
- خدمة خلفك لم ترد في الوقت المحدد، هل ترجع 500 أم 502 أم 504؟

**علامات تحذير:** يرجع 200 مع `{"error": ...}` في الجسم لكل الأخطاء، أو يعتبر 401 و403 نفس الشيء.

**مقالات تشرح الموضوع:** [HTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحة](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[كيف تصمم API لا تضطر إلى كسره بعد سنة](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/).

### 2\. أي طرق ال HTTP آمنة للتكرار، ولماذا يهم ذلك؟

مبتدئ **ماذا يختبر:** معنى Safe وIdempotent، وأثرهما على إعادة المحاولة Retry في الشبكات الحقيقية.

إجابة المبتدئ

GET للقراءة فقط، وPUT وDELETE إذا كررتها تعطي نفس النتيجة، أما POST فكل مرة تنشئ شيئاً جديداً.

إجابة الخبير

GET وHEAD آمنة Safe لأنها لا تغير شيئاً، وPUT وDELETE متساوية الأثر Idempotent لأن تكرارها يترك النظام في نفس الحالة، أما POST وPATCH فليستا كذلك بالضرورة. وهذا يهم لأن الشبكة تفقد الردود: الكلاينت أرسل الطلب ولم يصله الرد، فهل يعيد؟ ال Proxy والمكتبات تعيد طلبات GET تلقائياً، أما POST للدفع فلا يجوز إعادته إلا إذا جعلناه آمناً للتكرار بمفتاح Idempotency Key يرسله الكلاينت ويحفظه السيرفر مع النتيجة. ولاحظ أن Idempotent لا تعني أن الرد نفسه متطابق: DELETE الثانية قد ترجع 404 والحالة النهائية واحدة.

أسئلة المتابعة
- كيف تجعل POST الدفع آمناً للتكرار؟ أين تحفظ المفتاح وكم مدة؟
- طلبان بنفس المفتاح وصلا في نفس اللحظة، ماذا يحدث؟
- هل PATCH الذي يزيد عداداً بواحد Idempotent؟

**علامات تحذير:** يقول إن POST لا يمكن إعادته أبداً دون أن يعرف كيف يجعله آمناً، أو يعيد طلبات الدفع تلقائياً.

**مقالات تشرح الموضوع:** [Idempotency: كيف تجعل العملية آمنة عند تكرارها](https://arabroot.io/articles/idempotency-%D9%83%D9%8A%D9%81-%D8%AA%D8%AC%D8%B9%D9%84-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A9-%D8%A7%D9%85%D9%86%D8%A9-%D8%B9%D9%86%D8%AF-%D8%AA%D9%83%D8%B1%D8%A7%D8%B1%D9%87%D8%A7/)، و[HTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحة](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### 3\. كيف يعرف المتصفح أن نسخته المحفوظة ما زالت صالحة؟

متوسط **ماذا يختبر:** التخزين المؤقت في ال HTTP: `Cache-Control` و`ETag` والطلب المشروط.

إجابة المبتدئ

السيرفر يرسل `Cache-Control: max-age`، والمتصفح يحتفظ بالملف طوال هذه المدة ولا يطلبه مرة أخرى.

إجابة الخبير

هناك آليتان: الصلاحية بالوقت عبر `Cache-Control: max-age`، وخلالها لا يرسل المتصفح أي طلب، ثم التحقق Revalidation بعدها، حيث يرسل `If-None-Match` مع قيمة ال ETag التي استلمها، فإذا لم يتغير المورد يرد السيرفر 304 دون جسم. وللملفات الثابتة ذات الاسم الذي يحتوي الهاش مثل `app.3f9a.js` نستخدم `max-age` لسنة مع `immutable`، أما صفحة HTML فقصيرة أو `no-cache` (وهي تعني «تحقق كل مرة» وليس «لا تحفظ»). وانتبه لـ `private` في الردود التي تحتوي بيانات المستخدم حتى لا يحفظها CDN ويعرضها لمستخدم آخر.

أسئلة المتابعة
- ما الفرق بين `no-cache` و`no-store`؟
- نشرت إصداراً جديداً والمستخدمون ما زالوا يرون القديم، أين تبحث؟
- كيف تستخدم ETag لمنع تعارض التعديل بين مستخدمين؟

**علامات تحذير:** لا يعرف 304، أو يقترح إضافة `?v=random` لكل طلب كحل دائم.

**مقالات تشرح الموضوع:** [كيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظه](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[التخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%A4%D9%82%D8%AA-caching-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B1%D8%A7%D8%AA%D9%8A%D8%AC%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A7%D8%A8%D8%B7%D8%A7%D9%84/).

### 4\. تطبيقك خلف Reverse Proxy ويسجل عنواناً واحداً لكل الزوار، لماذا؟

متوسط **ماذا يختبر:** فهم ما يحدث للطلب بين الإنترنت والتطبيق، والثقة في الترويسات Headers.

إجابة المبتدئ

لأن الطلب يأتي من ال Proxy، والحل قراءة ترويسة `X-Forwarded-For`.

إجابة الخبير

التطبيق يرى عنوان آخر اتصال TCP وهو ال Proxy، والعنوان الحقيقي في `X-Forwarded-For` أو `Forwarded`، ولكن لا يجوز الوثوق بهذه الترويسة من أي مصدر، لأن المهاجم يستطيع إرسالها بنفسه وتزوير عنوانه لتجاوز Rate Limiting أو سجلات التدقيق. والصحيح أن تحدد للتطبيق قائمة ال Proxies الموثوقة، وتأخذ العنوان من يمين القائمة إلى يسارها حتى أول عنوان غير موثوق. ونفس الشيء مع `X-Forwarded-Proto`، فإذا لم يقرأه التطبيق فسوف يولد روابط http ويدخل في حلقة تحويل إلى https.

أسئلة المتابعة
- لديك Cloudflare ثم Nginx ثم التطبيق، أي عنوان في القائمة تأخذ؟
- كيف يستغل المهاجم ثقة التطبيق بكل ترويسة `X-Forwarded-For`؟

**علامات تحذير:** يأخذ أول عنوان في `X-Forwarded-For` دون أي تحقق.

**المقال الذي يشرح الموضوع:** [كيف يرى تطبيقك عنوان الزائر الحقيقي خلف ال Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/).

## تصميم ال API والإصدارات

### 5\. كيف تغير ال API دون أن تكسر التطبيقات القديمة؟

متوسط **ماذا يختبر:** التوافق مع الإصدارات السابقة Backward Compatibility، وفهم أن تطبيقات الجوال القديمة تبقى سنوات.

إجابة المبتدئ

نضع رقم الإصدار في الرابط مثل `/v1/` و`/v2/`، وعندما نغير شيئاً نعمل إصداراً جديداً.

إجابة الخبير

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

أسئلة المتابعة
- تريد تغيير اسم الحقل `name` إلى `full_name`، ما الخطوات بالترتيب؟
- كيف تعرف أن لا أحد يستخدم ال v1 قبل أن توقفه؟
- هل يحمي اختبار العقود Contract Testing من هذا؟ كيف؟

**علامات تحذير:** يرى أن تغيير اسم حقل أمر بسيط يكفيه إعلان في قناة الفريق.

**مقالات تشرح الموضوع:** [كيف تصمم API لا تضطر إلى كسره بعد سنة](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/)، و[اختبار العقود Contract Testing بين الخدمات مع Pact](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%AF-contract-testing-%D9%85%D8%B9-pact/).

### 6\. ما مشكلة تقسيم الصفحات بـ OFFSET في الجداول الكبيرة؟

متوسط **ماذا يختبر:** هل يعرف أن OFFSET يقرأ كل الصفوف التي يتخطاها، وهل يعرف البديل Keyset.

إجابة المبتدئ

نستخدم `LIMIT` و`OFFSET` ونرسل رقم الصفحة، والصفحات البعيدة قد تكون أبطأ قليلاً.

إجابة الخبير

قاعدة البيانات لا تقفز إلى الصف رقم 900000، وإنما تقرأه وترميه، لذلك يزيد الوقت مع رقم الصفحة، وتظهر مشكلة أخرى: إذا أضيف صف جديد أثناء التصفح فسوف يرى المستخدم صفاً مكرراً أو يفوته صف. والبديل التقسيم بالمفتاح Keyset أو Cursor Pagination: ترسل آخر قيمة رأيتها `WHERE (created_at, id) < ($1, $2)` مع فهرس على نفس الأعمدة ونفس الترتيب، فيكون الوقت ثابتاً. وثمنه أنك لا تستطيع القفز إلى «الصفحة 47» مباشرة، وهذا مقبول في أغلب الواجهات (التمرير اللانهائي، وزر التالي). وفي التمرين العملي أدناه قسنا الفرق فعلاً.

أسئلة المتابعة
- لماذا تضيف `id` إلى جانب `created_at` في المفتاح؟
- كيف تشفر ال Cursor حتى لا يعدله الكلاينت؟
- المستخدم يريد رقم الصفحة الأخيرة، كيف تعطيه عدد الصفوف دون `count(*)` على 50 مليون صف؟

**علامات تحذير:** يعتقد أن الفهرس يجعل OFFSET سريعاً في كل الحالات.

**مقالات تشرح الموضوع:** [الفهارس Indexes: لماذا تسرع الاستعلام وكيف تقرأ EXPLAIN](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D9%87%D8%A7%D8%B1%D8%B3-indexes-%D9%88%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-explain/)، و[PostgreSQL للجداول الضخمة: عشرات الملايين من الصفوف](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/).

### 7\. صمم نقطة دفع آمنة عندما يعيد التطبيق إرسال الطلب

خبير **ماذا يختبر:** تصميم Idempotency Key كاملاً: التخزين والتزامن والمدة والرد المحفوظ.

إجابة المبتدئ

نتحقق قبل الدفع هل يوجد طلب دفع لنفس الطلب، وإذا وجد لا ندفع مرة ثانية.

إجابة الخبير

الكلاينت يولد مفتاحاً UUID لكل محاولة دفع منطقية ويرسله في ترويسة `Idempotency-Key`، والسيرفر يحفظ المفتاح مع هاش جسم الطلب والحالة والرد في جدول عليه قيد فريد Unique Constraint. الطلب الأول يدخل المفتاح بحالة «قيد التنفيذ» في نفس المعاملة، والطلب المكرر إما يجد الرد المحفوظ فيعيده كما هو، أو يجد «قيد التنفيذ» فيرجع 409 ليعيد المحاولة لاحقاً، وإذا جاء نفس المفتاح بجسم مختلف فهذا خطأ 422\. والمفتاح يحفظ مدة محددة مثل 24 ساعة، والأهم أن الاستدعاء لبوابة الدفع الخارجية يحمل مفتاح Idempotency خاصاً به أيضاً، وإلا فقد يتكرر الخصم عندها حتى لو لم يتكرر عندنا.

أسئلة المتابعة
- السيرفر سقط بعد أن خصمت البوابة وقبل أن نحفظ الرد، ماذا يحدث عند إعادة المحاولة؟
- لماذا يولد الكلاينت المفتاح وليس السيرفر؟
- أين تحفظ المفاتيح: قاعدة البيانات أم Redis؟ وما الذي تخسره مع كل منهما؟

**علامات تحذير:** يعتمد على «تحقق ثم نفذ» بين طلبين متزامنين دون قيد فريد أو قفل.

**مقالات تشرح الموضوع:** [Idempotency: كيف تجعل العملية آمنة عند تكرارها](https://arabroot.io/articles/idempotency-%D9%83%D9%8A%D9%81-%D8%AA%D8%AC%D8%B9%D9%84-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A9-%D8%A7%D9%85%D9%86%D8%A9-%D8%B9%D9%86%D8%AF-%D8%AA%D9%83%D8%B1%D8%A7%D8%B1%D9%87%D8%A7/)، و[المحفظة الرقمية من الداخل: دفتر القيد المزدوج Ledger](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D8%AD%D9%81%D8%B8%D8%A9-%D8%B1%D9%82%D9%85%D9%8A%D8%A9-ledger/).

### 8\. كيف تحدد عدد الطلبات لكل مستخدم على عدة سيرفرات؟

متوسط **ماذا يختبر:** خوارزميات Rate Limiting، والحالة المشتركة بين السيرفرات، وتجربة الكلاينت.

إجابة المبتدئ

نحفظ عداداً لكل مستخدم ونرفض الطلب إذا تجاوز 100 في الدقيقة، ونرجع 429.

إجابة الخبير

العداد في ذاكرة كل سيرفر يعطي كل مستخدم الحد مضروباً في عدد السيرفرات، لذلك نضع الحالة في مكان مشترك مثل Redis مع عملية ذرية (سكربت Lua أو `INCR` مع مدة). والنافذة الثابتة Fixed Window تسمح بضعف الحد على حدود الدقيقة، لذلك نفضل Token Bucket أو النافذة المنزلقة Sliding Window. ونرجع 429 مع `Retry-After`، ونقرر مسبقاً ماذا يحدث إذا سقط Redis: هل نسمح بكل شيء Fail-open أم نرفض Fail-closed؟ والإجابة تختلف بين نقطة تسجيل الدخول ونقطة البحث.

أسئلة المتابعة
- لماذا يختلف الحد لنقطة تسجيل الدخول عن نقطة البحث؟
- المستخدمون خلف NAT واحد في شركة كبيرة يشتركون في نفس العنوان، كيف تحدد المفتاح؟

**علامات تحذير:** لا يفكر في وجود أكثر من سيرفر، أو لا يعرف الرمز 429.

**المقال الذي يشرح الموضوع:** [تحديد معدل الطلبات Rate Limiting وخوارزمياته](https://arabroot.io/articles/%D8%AA%D8%AD%D8%AF%D9%8A%D8%AF-%D9%85%D8%B9%D8%AF%D9%84-%D8%A7%D9%84%D8%B7%D9%84%D8%A8%D8%A7%D8%AA-rate-limiting-%D9%88%D8%AE%D9%88%D8%A7%D8%B1%D8%B2%D9%85%D9%8A%D8%A7%D8%AA%D9%87/).

## قواعد البيانات وSQL والفهارس

### 9\. لديك فهرس مركب على عمودين، متى لا يستخدمه الاستعلام؟

متوسط **ماذا يختبر:** ترتيب الأعمدة في الفهرس المركب Composite Index وقراءة خطة التنفيذ.

إجابة المبتدئ

إذا لم يكن العمود في شرط `WHERE`، فالفهرس يعمل على الأعمدة التي في الشرط.

إجابة الخبير

الفهرس على `(customer_id, created_at)` مرتب أولاً بالعميل ثم بالتاريخ، مثل دليل الهاتف المرتب بالعائلة ثم بالاسم الأول، فهو ممتاز لشرط على `customer_id` وحده أو على الاثنين، وممتاز لـ `ORDER BY created_at` داخل عميل واحد، ولكنه لا يفيد كثيراً لشرط على `created_at` وحده. والقاعدة: أعمدة المساواة أولاً ثم عمود المدى أو الترتيب. ولا أخمن، وإنما أقرأ `EXPLAIN (ANALYZE, BUFFERS)` وأقارن عدد الصفوف المتوقعة بالفعلية، لأن الفرق الكبير بينهما يعني إحصاءات قديمة وليس فهرساً ناقصاً.

أسئلة المتابعة
- لماذا قد يختار المخطط Sequential Scan مع وجود فهرس مناسب؟
- ما هو Index Only Scan ولماذا يحتاج VACUUM؟
- كيف تجد الفهارس التي لا يستخدمها أحد في PostgreSQL؟

**علامات تحذير:** يقترح فهرساً منفصلاً لكل عمود كحل عام.

**مقالات تشرح الموضوع:** [الفهارس Indexes: لماذا تسرع الاستعلام وكيف تقرأ EXPLAIN](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D9%87%D8%A7%D8%B1%D8%B3-indexes-%D9%88%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-explain/)، و[ضبط أداء PostgreSQL بالقياس وليس بالتخمين](https://arabroot.io/articles/%D8%B6%D8%A8%D8%B7-%D8%A7%D8%AF%D8%A7%D8%A1-postgresql-%D8%A8%D8%A7%D9%84%D9%82%D9%8A%D8%A7%D8%B3/).

### 10\. ما مشكلة N+1 في ال ORM، وكيف تكتشفها؟

مبتدئ **ماذا يختبر:** هل يعرف ما يولده ال ORM من استعلامات خلف الكود الذي يبدو بريئاً.

إجابة المبتدئ

عندما نجلب قائمة ثم نجلب علاقة كل عنصر في حلقة فيصبح عندنا استعلام لكل عنصر، والحل استخدام JOIN.

إجابة الخبير

الكود `for o in orders: o.customer.name` يبدو سطراً واحداً، ولكن التحميل الكسول Lazy Loading يرسل استعلاماً لكل طلب، فتصبح 200 طلب = 201 استعلام، وكل استعلام ذهاب وعودة Round Trip على الشبكة، فإذا كانت قاعدة البيانات على سيرفر آخر بزمن 1 ملي ثانية فقد خسرت 200 ملي ثانية قبل أي عمل حقيقي. والحل التحميل المسبق: `selectinload` يرسل استعلامين (القائمة ثم `IN (...)`)، و`joinedload` يرسل استعلاماً واحداً بـ JOIN، والاختيار بينهما يعتمد على شكل العلاقة: JOIN مع علاقة واحد إلى كثير يكرر صفوف الأب. وأكتشفه بعداد استعلامات في الاختبارات أو في السجل لكل طلب، وليس بالصدفة. وفي التمرين العملي الأول أدناه نرى الأرقام الحقيقية.

أسئلة المتابعة
- متى يكون `joinedload` أسوأ من `selectinload`؟
- كيف تكتب اختباراً يفشل إذا تجاوز طلب الصفحة 5 استعلامات؟
- كيف تظهر نفس المشكلة في GraphQL، وما هو DataLoader؟

**علامات تحذير:** لا يعرف ما يرسله ال ORM إلى قاعدة البيانات، ولم يفتح سجل الاستعلامات أبداً.

**مقالات تشرح الموضوع:** [تتبع الطلب البطيء من المتصفح إلى قاعدة البيانات](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/)، و[تعلم SQL (2): الربط JOIN والتجميع والاستعلامات الفرعية](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-2-%D8%A7%D9%84%D8%B1%D8%A8%D8%B7-join-%D9%88%D8%A7%D9%84%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D9%84%D8%A7%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D8%B9%D9%8A%D8%A9/).

### 11\. متى تكسر التطبيع Normalization عمداً؟

متوسط **ماذا يختبر:** الموازنة بين صحة البيانات وسرعة القراءة.

إجابة المبتدئ

التطبيع يمنع تكرار البيانات، ونكسره عندما تصبح الاستعلامات بطيئة بسبب كثرة JOIN.

إجابة الخبير

أبدأ دائماً بتصميم مطبع لأن تكرار البيانات يعني أن كل تعديل يجب أن يصل إلى كل النسخ، وأي نسخة تفوتها تصبح كذبة. ثم أكسره بقرار مقاس: عمود محسوب مثل `orders_count` يحدث في نفس المعاملة، أو جدول تقارير منفصل، أو Materialized View يحدث دورياً، أو أحفظ نسخة من البيانات التي يجب ألا تتغير أصلاً مثل سعر المنتج وعنوان الشحن وقت الطلب، وهذا ليس كسراً للتطبيع وإنما حفظ لحقيقة تاريخية. وفي كل حالة أكتب من يحدث النسخة ومتى، وماذا يحدث إذا اختلفت عن المصدر.

أسئلة المتابعة
- لماذا يحفظ جدول عناصر الطلب سعر المنتج بدلاً من قراءته من جدول المنتجات؟
- العداد المحسوب اختلف عن العدد الحقيقي، كيف تكتشف ذلك وتصلحه؟

**علامات تحذير:** يكرر البيانات في كل مكان «للسرعة» دون أن يقيس، أو لا يعرف ما هو التطبيع أصلاً.

**مقالات تشرح الموضوع:** [تصميم قواعد البيانات: التطبيع Normalization ومتى تكسره](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/)، و[متى تستخدم العروض المحفوظة Materialized Views في تطبيقك؟](https://arabroot.io/articles/%D8%A7%D9%84%D8%B9%D8%B1%D9%88%D8%B6-%D8%A7%D9%84%D9%85%D8%AD%D9%81%D9%88%D8%B8%D8%A9-materialized-views/).

### 12\. متى تختار MongoDB بدلاً من PostgreSQL لمشروع جديد؟

متوسط **ماذا يختبر:** اختيار قاعدة البيانات من شكل البيانات والاستعلامات، وليس من الشهرة.

إجابة المبتدئ

MongoDB أسرع وأسهل لأنها لا تحتاج Schema، فنستخدمها عندما تكون البيانات متغيرة.

إجابة الخبير

أسأل عن شكل الاستعلامات قبل شكل البيانات: إذا كانت البيانات تقرأ دائماً كوثيقة واحدة كاملة (ملف منتج بخصائص تختلف من فئة لأخرى) فنموذج المستندات مريح، أما إذا كانت هناك علاقات كثيرة ومعاملات بين كيانات وتقارير بـ JOIN فقاعدة علائقية أنسب. ولا يوجد شيء اسمه «بدون Schema»، وإنما Schema في الكود بدلاً من قاعدة البيانات، ومسؤوليتها على الفريق. وغالباً أبدأ بـ PostgreSQL مع عمود `jsonb` للخصائص المتغيرة، لأنه يعطي المرونة مع المعاملات والقيود، وأنتقل فقط عندما يظهر سبب مقاس.

أسئلة المتابعة
- ما الذي تخسره عندما تضع كل شيء في `jsonb`؟
- كيف تعمل المعاملات بين عدة مستندات في MongoDB، وما ثمنها؟

**علامات تحذير:** يختار قاعدة البيانات لأنها «أسرع» دون تعريف لما هو أسرع.

**مقالات تشرح الموضوع:** [PostgreSQL أم MySQL أم MongoDB؟](https://arabroot.io/articles/postgresql-%D8%A7%D9%85-mysql-%D8%A7%D9%85-mongodb/)، و[أنواع قواعد البيانات ومتى تختار كل نوع](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%83%D9%84-%D9%86%D9%88%D8%B9/).

## المعاملات والعزل والأقفال

### 13\. ماذا تعني المعاملة Transaction، ومتى تحتاجها؟

مبتدئ **ماذا يختبر:** فهم ACID بمثال حقيقي، وليس بحفظ الأحرف الأربعة.

إجابة المبتدئ

المعاملة مجموعة عمليات تنجح كلها أو تفشل كلها، مثل تحويل مبلغ: خصم من حساب وإضافة لآخر.

إجابة الخبير

الذرية Atomicity تعني الكل أو لا شيء، والاتساق Consistency أن القيود تبقى صحيحة، والعزل Isolation أن المعاملات المتزامنة لا ترى بعضها بشكل خاطئ، والديمومة Durability أن ما تم تأكيده لا يضيع بعد انقطاع الكهرباء (لأنه كتب في ال WAL). والمهم عملياً: المعاملة تحمي ما داخل قاعدة البيانات فقط، فإذا أرسلت بريداً أو استدعيت بوابة دفع داخلها ثم فشلت المعاملة فالبريد قد خرج ولن يعود، لذلك أبقي المعاملات قصيرة وأخرج الاستدعاءات الخارجية منها، وأستخدم نمط Outbox عندما أحتاج أن يتبع الحدث المعاملة.

أسئلة المتابعة
- ماذا يحدث لمعاملة مفتوحة لعشر دقائق في PostgreSQL؟
- لماذا لا تضع استدعاء HTTP داخل المعاملة؟

**علامات تحذير:** يفتح معاملة لكل الطلب بما فيه استدعاءات خارجية، أو لا يعرف أن ال ORM يفتح معاملة ضمنية.

**مقالات تشرح الموضوع:** [تعلم SQL (3): CTE ودوال النوافذ والمعاملات](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/)، و[المعاملات الموزعة: Saga ونمط Outbox بدل 2PC](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-saga-%D9%88%D9%86%D9%85%D8%B7-outbox/).

### 14\. ما مستوى العزل الافتراضي في PostgreSQL، وما الذي لا يحميك منه؟

خبير **ماذا يختبر:** مستويات العزل Isolation Levels والظواهر التي يسمح بها كل مستوى.

إجابة المبتدئ

المستوى الافتراضي Read Committed، وهو يمنع قراءة بيانات لم تؤكد بعد.

إجابة الخبير

الافتراضي Read Committed، وكل جملة فيه ترى ما تم تأكيده قبل بدايتها، لذلك جملتان في نفس المعاملة قد تريان قيماً مختلفة، والأخطر أنه لا يمنع التحديث المفقود Lost Update في نمط «اقرأ ثم احسب في التطبيق ثم اكتب». و Repeatable Read في PostgreSQL يعطي لقطة Snapshot ثابتة للمعاملة ويرفض التحديث المتعارض بخطأ تسلسل، وSerializable يمنع حتى Write Skew (طبيبان يلغيان مناوبتهما معاً لأن كلاً منهما رأى الآخر موجوداً). والمستويات الأعلى تعني أن التطبيق يجب أن يعيد المعاملة عند خطأ `40001`، وهذا ما ينساه الناس.

أسئلة المتابعة
- اشرح Write Skew بمثال من نظام حجز.
- ماذا يفعل الكود عندما يستلم خطأ `serialization_failure`؟
- هل MySQL InnoDB يتصرف بنفس الطريقة في Repeatable Read؟

**علامات تحذير:** يظن أن وضع الكود داخل معاملة يمنع كل مشكلات التزامن.

**مقالات تشرح الموضوع:** [تعلم SQL (3): CTE ودوال النوافذ والمعاملات](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/)، و[الأقفال Locks والجمود Deadlock في قواعد البيانات](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%82%D9%81%D8%A7%D9%84-locks-%D9%88%D8%A7%D9%84%D8%AC%D9%85%D9%88%D8%AF-deadlock-%D9%81%D9%8A-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/).

### 15\. خمسون مستخدماً اشتروا عشر تذاكر، أين ذهبت التذاكر الإضافية؟

متوسط **ماذا يختبر:** التحديث المفقود Lost Update وطرق منعه، وهو التمرين العملي الثاني في هذا المقال.

إجابة المبتدئ

هناك مشكلة تزامن، فطلبان قرآ نفس العدد في نفس الوقت، والحل وضع الكود في معاملة.

إجابة الخبير

كل طلب قرأ `stock = 10`، ثم كتب `stock = 9`، فضاعت التحديثات وبقي المخزون شبه ممتلئ مع خمسين عملية بيع، والمعاملة وحدها في Read Committed لا تمنع ذلك. والحلول بالترتيب: جملة واحدة ذرية تقرر وتكتب `UPDATE ... SET stock = stock - 1 WHERE id = $1 AND stock > 0 RETURNING stock` وهي الأبسط والأسرع، أو قفل الصف `SELECT ... FOR UPDATE` عندما يحتاج القرار منطقاً في التطبيق، أو قفل متفائل Optimistic Locking بعمود `version` عندما يكون التعارض نادراً. ولاحظ أن أي قفل يجب ألا يمتد فوق استدعاء بوابة الدفع، وإلا أصبحت سرعة البيع مرتبطة بسرعة البوابة.

أسئلة المتابعة
- متى تفضل القفل المتفائل على `FOR UPDATE`؟
- ماذا يفعل `SKIP LOCKED` ومتى يفيد؟
- كيف تحجز المقعد لعشر دقائق أثناء الدفع دون قفل مفتوح؟

**علامات تحذير:** يقترح قفلاً في ذاكرة التطبيق (mutex) وهو يعمل على عدة سيرفرات.

**مقالات تشرح الموضوع:** [كيف تبني نظام حجز تذاكر يتحمل لحظة فتح البيع](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%86%D8%B8%D8%A7%D9%85-%D8%AD%D8%AC%D8%B2-%D8%AA%D8%B0%D8%A7%D9%83%D8%B1-%D9%8A%D8%AA%D8%AD%D9%85%D9%84-%D9%84%D8%AD%D8%B8%D8%A9-%D9%81%D8%AA%D8%AD-%D8%A7%D9%84%D8%A8%D9%8A%D8%B9/)، و[الأقفال Locks والجمود Deadlock في قواعد البيانات](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%82%D9%81%D8%A7%D9%84-locks-%D9%88%D8%A7%D9%84%D8%AC%D9%85%D9%88%D8%AF-deadlock-%D9%81%D9%8A-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/).

### 16\. ظهر في السجل Deadlock detected، كيف تصل إلى السبب؟

خبير **ماذا يختبر:** فهم الجمود Deadlock وترتيب الأقفال وقراءة سجل قاعدة البيانات.

إجابة المبتدئ

معاملتان تنتظر كل منهما الأخرى، وقاعدة البيانات تلغي واحدة، فنعيد المحاولة.

إجابة الخبير

PostgreSQL يكتب في السجل الجملتين والعمليتين، فأبدأ من هناك، وغالباً أجد معاملتين تقفلان نفس الصفوف بترتيب مختلف: الأولى تحدث الحساب A ثم B، والثانية B ثم A. والحل الأساسي ترتيب ثابت للأقفال (مثلاً بالمعرف تصاعدياً)، ثم تقصير المعاملات، ثم إعادة المحاولة للمعاملة الملغاة لأنها آمنة بطبيعتها. وأنتبه للأقفال التي لا أراها: المفاتيح الأجنبية Foreign Keys تأخذ قفلاً على الصف الأب، والتحديثات الجماعية بدون `ORDER BY` تمر على الصفوف بترتيب غير محدد.

أسئلة المتابعة
- كيف تعيد إنتاج الجمود في اختبار؟
- ما الفرق بين الجمود وانتظار قفل طويل، وكيف تكتشف الثاني؟

**علامات تحذير:** يقترح زيادة المهلة Timeout أو إعادة المحاولة دون أن يبحث عن ترتيب الأقفال.

**المقال الذي يشرح الموضوع:** [الأقفال Locks والجمود Deadlock في قواعد البيانات](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%82%D9%81%D8%A7%D9%84-locks-%D9%88%D8%A7%D9%84%D8%AC%D9%85%D9%88%D8%AF-deadlock-%D9%81%D9%8A-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/).

## التخزين المؤقت Caching

### 17\. اشرح نمط Cache-Aside، وماذا يحدث عند تعديل البيانات؟

مبتدئ **ماذا يختبر:** أشهر نمط كاش، ومشكلة الإبطال Invalidation التي تأتي معه.

إجابة المبتدئ

نقرأ من Redis أولاً، فإذا لم نجد نقرأ من قاعدة البيانات ونحفظ في Redis، وعند التعديل نحذف المفتاح.

إجابة الخبير

هذا صحيح، والتفاصيل هي ما يفرق: عند التعديل نحذف المفتاح ولا نكتب القيمة الجديدة، لأن كتابتين متزامنتين قد تصلان إلى Redis بترتيب معاكس فتبقى القيمة القديمة. ونحذف بعد تأكيد المعاملة وليس قبله، وإلا قرأ طلب آخر القيمة القديمة من قاعدة البيانات وأعادها إلى الكاش. ويبقى سباق صغير بين القراءة والحذف، لذلك نضع دائماً TTL كحد أعلى لعمر الخطأ. وإذا كانت عدة خدمات تعدل نفس البيانات فالإبطال من مكان واحد (أحداث CDC مثلاً) أوثق من تذكره في كل كود.

أسئلة المتابعة
- لماذا الحذف أفضل من الكتابة عند التعديل؟
- الكاش يعيد بيانات مستخدم لمستخدم آخر، ما الأسباب المحتملة؟

**علامات تحذير:** لا يذكر الإبطال أبداً، أو يضع TTL طويلاً دون أي خطة للتعديل.

**مقالات تشرح الموضوع:** [التخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%A4%D9%82%D8%AA-caching-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B1%D8%A7%D8%AA%D9%8A%D8%AC%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A7%D8%A8%D8%B7%D8%A7%D9%84/)، و[Redis وبديله المفتوح Valkey: التخزين المؤقت والجلسات](https://arabroot.io/articles/redis-%D9%88-valkey-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%A4%D9%82%D8%AA-%D9%88%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA/).

### 18\. انتهت صلاحية مفتاح مشهور فسقطت قاعدة البيانات، ماذا حدث؟

خبير **ماذا يختبر:** Cache Stampede وطرق منعه.

إجابة المبتدئ

كل الطلبات لم تجد المفتاح فذهبت إلى قاعدة البيانات في نفس الوقت، والحل زيادة مدة الصلاحية.

إجابة الخبير

هذا Cache Stampede: آلاف الطلبات في الثانية تفقد المفتاح في نفس اللحظة وكل منها يرسل نفس الاستعلام الثقيل. والحلول: قفل أو Single-flight حتى يحسب طلب واحد القيمة والبقية تنتظر أو تأخذ القيمة القديمة، أو التحديث المبكر الاحتمالي قبل الانتهاء، أو تقديم القيمة القديمة مع تحديثها في الخلفية Stale-While-Revalidate، وإضافة عشوائية Jitter لمدد الصلاحية حتى لا تنتهي مفاتيح كثيرة معاً بعد إعادة تشغيل. وزيادة المدة تؤخر المشكلة فقط.

أسئلة المتابعة
- كيف تنفذ قفل Single-flight على عدة سيرفرات؟
- ماذا يحدث عند إعادة تشغيل Redis بالكامل في ساعة الذروة؟

**علامات تحذير:** لا يتخيل أن الكاش نفسه قد يسبب سقوط قاعدة البيانات.

**مقالات تشرح الموضوع:** [التخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%A4%D9%82%D8%AA-caching-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B1%D8%A7%D8%AA%D9%8A%D8%AC%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A7%D8%A8%D8%B7%D8%A7%D9%84/)، و[كيف تمنع خدمة بطيئة من إسقاط نظامك كله؟](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D9%85%D9%86%D8%B9-%D8%AE%D8%AF%D9%85%D8%A9-%D8%A8%D8%B7%D9%8A%D8%A6%D8%A9-%D9%85%D9%86-%D8%A7%D8%B3%D9%82%D8%A7%D8%B7-%D9%86%D8%B8%D8%A7%D9%85%D9%83-%D9%83%D9%84%D9%87/).

### 19\. أين يمكن أن تضع الكاش بين المستخدم وقاعدة البيانات؟

متوسط **ماذا يختبر:** رؤية الطبقات كلها، وليس Redis وحده.

إجابة المبتدئ

في Redis بين التطبيق وقاعدة البيانات، وأيضاً في المتصفح.

إجابة الخبير

في المتصفح بترويسات HTTP، وفي CDN على الحافة للمحتوى العام، وفي ال Reverse Proxy، وفي ذاكرة التطبيق نفسه (أسرع شيء ولكنه مختلف بين السيرفرات)، وفي Redis المشترك، وداخل قاعدة البيانات نفسها (ال Buffer Cache وMaterialized Views). وكل طبقة أقرب للمستخدم أسرع وأصعب في الإبطال، لذلك أسأل: هل البيانات عامة أم خاصة بالمستخدم، وما الضرر من نسخة قديمة، ومن يستطيع إبطالها؟ والكاش المحلي في الذاكرة مع Redis معاً يحتاج إبطالاً بين السيرفرات عبر Pub/Sub مثلاً.

أسئلة المتابعة
- لماذا قد يكون الكاش في ذاكرة التطبيق أخطر من Redis؟
- كيف تمنع CDN من حفظ صفحة فيها اسم المستخدم؟

**علامات تحذير:** يرى الكاش طبقة واحدة فقط.

**مقالات تشرح الموضوع:** [كيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظه](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[التخزين المؤقت Caching بالتفصيل: الاستراتيجيات والإبطال](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%A4%D9%82%D8%AA-caching-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B1%D8%A7%D8%AA%D9%8A%D8%AC%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A7%D8%A8%D8%B7%D8%A7%D9%84/).

## الطوابير والرسائل

### 20\. متى تضع طابوراً Queue بين خدمتين بدلاً من استدعاء مباشر؟

مبتدئ **ماذا يختبر:** الفرق بين العمل المتزامن وغير المتزامن، وثمن الطابور.

إجابة المبتدئ

عندما يكون العمل طويلاً مثل إرسال البريد أو معالجة الصور، فنضعه في طابور حتى لا ينتظر المستخدم.

إجابة الخبير

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

أسئلة المتابعة
- ما المقياس الذي تراقبه لتعرف أن المستهلكين لا يلحقون؟
- متى تختار Kafka بدلاً من RabbitMQ؟

**علامات تحذير:** يضع طابوراً لكل شيء دون أن يذكر المراقبة أو تجربة المستخدم.

**مقالات تشرح الموضوع:** [الطابور Queue للمبتدئين: لماذا تحتاجه وكيف يعمل](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D8%B7%D9%88%D8%A7%D8%A8%D9%8A%D8%B1-queue-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[RabbitMQ أم Kafka أم NATS؟ كيف تختار وسيط الرسائل](https://arabroot.io/articles/rabbitmq-%D8%A7%D9%85-kafka-%D8%A7%D9%85-nats-%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%88%D8%B3%D9%8A%D8%B7-%D8%A7%D9%84%D8%B1%D8%B3%D8%A7%D8%A6%D9%84/).

### 21\. لماذا تصل الرسالة نفسها مرتين، وكيف تكتب مستهلكاً يتحمل ذلك؟

متوسط **ماذا يختبر:** التسليم مرة على الأقل At-least-once والمستهلك الآمن للتكرار Idempotent Consumer.

إجابة المبتدئ

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

إجابة الخبير

المستهلك عالج الرسالة ثم سقط قبل أن يرسل التأكيد ACK، فيعيد الوسيط تسليمها، وهذا سلوك صحيح وليس خطأ، لأن البديل هو أن تضيع الرسائل. و«مرة واحدة بالضبط» بين نظامين مختلفين غير ممكنة عملياً، لذلك نجعل الأثر مرة واحدة: كل رسالة تحمل معرفاً، والمستهلك يحفظ المعرفات المعالجة في نفس المعاملة التي يكتب فيها النتيجة (جدول عليه قيد فريد)، أو تكون العملية نفسها آمنة للتكرار مثل «اجعل الحالة مدفوع» بدلاً من «أضف 100».

أسئلة المتابعة
- رسالة تفشل كل مرة بسبب بيانات خاطئة، ماذا يحدث للطابور؟ (Poison Message)
- هل الترتيب مضمون؟ ومتى يهمك؟

**علامات تحذير:** يعتقد أن الوسيط يضمن «مرة واحدة بالضبط» فلا يحتاج أي حماية.

**مقالات تشرح الموضوع:** [طوابير المهام فوق Redis وValkey: متى تضيع المهمة؟](https://arabroot.io/articles/%D8%B7%D9%88%D8%A7%D8%A8%D9%8A%D8%B1-%D8%A7%D9%84%D9%85%D9%87%D8%A7%D9%85-%D9%81%D9%88%D9%82-redis-%D9%88-valkey/)، و[Idempotency: كيف تجعل العملية آمنة عند تكرارها](https://arabroot.io/articles/idempotency-%D9%83%D9%8A%D9%81-%D8%AA%D8%AC%D8%B9%D9%84-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A9-%D8%A7%D9%85%D9%86%D8%A9-%D8%B9%D9%86%D8%AF-%D8%AA%D9%83%D8%B1%D8%A7%D8%B1%D9%87%D8%A7/).

### 22\. حفظت الطلب وأرسلت حدثاً إلى Kafka، فنجح الحفظ وفشل الإرسال، ما الحل؟

خبير **ماذا يختبر:** مشكلة الكتابة المزدوجة Dual Write ونمط Outbox.

إجابة المبتدئ

نعيد محاولة الإرسال عدة مرات، وإذا فشل نسجل الخطأ.

إجابة الخبير

هذه الكتابة المزدوجة: نظامان دون معاملة مشتركة، فأي ترتيب تختاره له حالة فشل (حفظ دون حدث، أو حدث عن طلب لم يحفظ). والحل نمط Outbox: نكتب الحدث في جدول `outbox` في نفس معاملة الطلب، ثم عملية منفصلة تقرأ الجدول وترسل إلى الوسيط وتعلم الصف كمرسل، أو تقرأه من سجل قاعدة البيانات عبر CDC مثل Debezium. والنتيجة تسليم مرة على الأقل، لذلك المستهلك يجب أن يكون آمناً للتكرار كما في السؤال السابق.

أسئلة المتابعة
- ما الفرق بين قراءة جدول ال Outbox دورياً وقراءته عبر CDC؟
- كيف تحافظ على ترتيب أحداث نفس الطلب؟

**علامات تحذير:** يقترح 2PC بين قاعدة البيانات وKafka دون أن يعرف ثمنه، أو يتجاهل حالة الفشل.

**مقالات تشرح الموضوع:** [المعاملات الموزعة: Saga ونمط Outbox بدل 2PC](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-saga-%D9%88%D9%86%D9%85%D8%B7-outbox/)، و[نقل تغييرات قاعدة البيانات CDC مع Debezium](https://arabroot.io/articles/%D9%86%D9%82%D9%84-%D8%AA%D8%BA%D9%8A%D9%8A%D8%B1%D8%A7%D8%AA-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-cdc-%D9%85%D8%B9-debezium/).

## التزامن والخيوط ومجمع الاتصالات

### 23\. ما الفرق بين الخيوط Threads وasync/await في سيرفر الويب؟

متوسط **ماذا يختبر:** نموذج التزامن في اللغة التي يعمل بها، ومتى يحجب الكود الخيط.

إجابة المبتدئ

async/await يجعل الكود لا ينتظر العمليات البطيئة، والخيوط تشغل عدة أشياء في نفس الوقت.

إجابة الخبير

سيرفر الخيوط يعطي كل طلب خيطاً ينتظر عليه أثناء I/O، وهذا بسيط ولكن الخيوط مكلفة في الذاكرة وتبديلها. وasync يسمح لخيط واحد أو قليل بإدارة آلاف الطلبات المنتظرة على الشبكة، وهذا ممتاز للعمل الذي ينتظر I/O، ولا يفيد شيئاً للعمل الذي يستهلك المعالج، بل يضر: حلقة حسابية ثقيلة في Node.js أو داخل `async def` في Python تحجب كل الطلبات الأخرى. وكذلك استدعاء مكتبة متزامنة مثل `requests` داخل كود async يحجب الحلقة. وفي Java 21 جاءت الخيوط الافتراضية Virtual Threads لتعطي أسلوب الخيوط بتكلفة قريبة من async.

أسئلة المتابعة
- طلب واحد يحسب تقريراً لثانيتين في Node.js، ماذا يحدث لبقية الطلبات؟ وما الحل؟
- كيف تكتشف أن الحلقة محجوبة في الإنتاج؟

**علامات تحذير:** يعتقد أن async يجعل الكود أسرع في كل الحالات.

**مقالات تشرح الموضوع:** [Threads وasync/await: كود لا يتجمد ولا يضيع بياناته](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-threads-%D9%88%D8%A7%D9%84%D8%A3%D9%82%D9%81%D8%A7%D9%84-%D9%88-async-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)، و[التزامن Concurrency والخيوط: ماذا يعني لسيرفرك وتطبيقاتك](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%88%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### 24\. لماذا لا يكفي رفع حجم مجمع الاتصالات Connection Pool إلى 500؟

متوسط **ماذا يختبر:** فهم أن قاعدة البيانات لها حد، وأن الاتصالات الكثيرة تبطئها.

إجابة المبتدئ

لأن قاعدة البيانات لها حد أقصى للاتصالات، فإذا تجاوزناه ترفض الاتصالات الجديدة.

إجابة الخبير

كل اتصال في PostgreSQL عملية Process كاملة بذاكرتها، والمعالج لا ينفذ أكثر من عدد أنويته في نفس اللحظة، فخمسمئة اتصال نشط على سيرفر بثماني أنوية تعني تبديلاً وتنافساً على الأقفال، والنتيجة أن الإنتاجية تنخفض. وعشرة سيرفرات تطبيق في كل منها مجمع بـ 50 تعني 500 اتصال. والصحيح مجمع صغير لكل نسخة، وطابور انتظار قصير مع مهلة، وإذا كثرت النسخ (Serverless أو Kubernetes) فمجمع مركزي مثل PgBouncer. وإذا كان المجمع ممتلئاً دائماً فالسبب غالباً معاملات طويلة أو استعلامات بطيئة، وليس حجم المجمع.

أسئلة المتابعة
- ما الفرق بين وضع Session ووضع Transaction في PgBouncer، وما الذي ينكسر في الثاني؟
- كيف تعرف أن الطلبات تنتظر المجمع وليس قاعدة البيانات؟

**علامات تحذير:** يحل كل مشكلة اتصال برفع الأرقام.

**المقال الذي يشرح الموضوع:** [Connection Pooling: لماذا الاتصال بقاعدة البيانات مكلف](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### 25\. مهمة مجدولة تعمل على ثلاثة سيرفرات، كيف تضمن أنها تعمل مرة واحدة؟

خبير **ماذا يختبر:** الأقفال الموزعة Distributed Locks وحدودها.

إجابة المبتدئ

نضع قفلاً في Redis بـ `SET NX` مع مدة، والسيرفر الذي يأخذه ينفذ المهمة.

إجابة الخبير

هذا يعمل غالباً، ولكن القفل بمدة له مشكلة: إذا توقف السيرفر مؤقتاً (Garbage Collection طويل أو شبكة) وانتهت المدة، يأخذ سيرفر آخر القفل ويعمل الاثنان معاً. لذلك العمل نفسه يجب أن يتحمل التنفيذ المزدوج، أو نستخدم رقماً متزايداً Fencing Token يرفض به المورد الكتابة القديمة. والأبسط في كثير من الحالات قفل استشاري في PostgreSQL `pg_try_advisory_lock` مرتبط بالاتصال، أو جدول مهام بـ `SELECT ... FOR UPDATE SKIP LOCKED`، أو تشغيل المجدول في مكان واحد أصلاً.

أسئلة المتابعة
- ما هو Fencing Token ولماذا يحتاجه القفل الموزع؟
- متى يكون جدول المهام في قاعدة البيانات أفضل من طابور منفصل؟

**علامات تحذير:** يعتبر قفل Redis بمدة ضماناً كاملاً.

**مقالات تشرح الموضوع:** [الأنظمة الموزعة للمبتدئين: ماذا يحدث عندما تنقطع الشبكة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-cap-%D9%88%D8%A7%D9%84%D8%A7%D8%AA%D8%B3%D8%A7%D9%82-%D9%88%D8%A7%D9%84%D8%A7%D8%AC%D9%85%D8%A7%D8%B9/)، و[طوابير المهام فوق Redis وValkey: متى تضيع المهمة؟](https://arabroot.io/articles/%D8%B7%D9%88%D8%A7%D8%A8%D9%8A%D8%B1-%D8%A7%D9%84%D9%85%D9%87%D8%A7%D9%85-%D9%81%D9%88%D9%82-redis-%D9%88-valkey/).

## المصادقة والصلاحيات

### 26\. الجلسة في السيرفر أم JWT؟ وما الذي تخسره مع كل منهما؟

متوسط **ماذا يختبر:** فهم حقيقي لما يحفظ وأين، ومشكلة إلغاء التوكن.

إجابة المبتدئ

الجلسة تحفظ في السيرفر، وJWT يحفظ في الكلاينت ولا يحتاج قاعدة بيانات، لذلك JWT أفضل للتوسع.

إجابة الخبير

الجلسة معرف عشوائي في Cookie والحالة في السيرفر (Redis مثلاً)، فإلغاؤها فوري، وثمنها قراءة في كل طلب. وJWT موقع يحمل البيانات فلا يحتاج قراءة، ولكن لا يمكن إلغاؤه قبل انتهائه إلا بقائمة سوداء، وهذه قراءة في كل طلب مرة أخرى. لذلك الشائع توكن وصول قصير (5 إلى 15 دقيقة) مع توكن تجديد Refresh Token يحفظ في السيرفر ويمكن إلغاؤه ويدور مع كل استخدام. وللمتصفح أفضل مكان هو Cookie بخصائص `HttpOnly` و`Secure` و`SameSite`، وليس `localStorage` الذي يقرأه أي سكربت ينجح في حقن XSS.

أسئلة المتابعة
- المستخدم غير كلمة المرور، كيف تخرج كل أجهزته الأخرى؟
- ما هجوم `alg: none`، وكيف تمنعه المكتبات الحديثة؟
- لماذا يدور توكن التجديد مع كل استخدام؟

**علامات تحذير:** يضع بيانات حساسة في ال JWT ظناً أنه مشفر، أو لا يعرف أن الإلغاء مشكلة.

**مقالات تشرح الموضوع:** [كيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[شرح OAuth وOpenID Connect للمبتدئين](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### 27\. كيف تحفظ كلمات مرور المستخدمين؟

مبتدئ **ماذا يختبر:** الهاش البطيء والملح Salt، وعدم اختراع التشفير.

إجابة المبتدئ

لا نحفظها نصاً، وإنما نحفظ الهاش، مثل SHA-256 مع ملح Salt.

إجابة الخبير

SHA-256 سريع جداً، وهذا ما يجعله سيئاً لكلمات المرور، لأن المهاجم الذي سرق الجدول يجرب مليارات الكلمات في الثانية على GPU. والصحيح دالة بطيئة مصممة لهذا مثل Argon2id أو bcrypt أو scrypt، مع ملح عشوائي لكل مستخدم (تضيفه المكتبة تلقائياً) وتكلفة نرفعها مع الوقت، ونعيد الهاش بالتكلفة الجديدة عند الدخول التالي. ولا أكتب أي جزء من هذا بنفسي، وأضيف بجانبه حماية تسجيل الدخول نفسه: Rate Limiting والتحقق بخطوتين، ورفض الكلمات المسربة المعروفة.

أسئلة المتابعة
- لماذا يضاف ملح مختلف لكل مستخدم؟
- كيف تنقل مليون مستخدم من MD5 إلى Argon2 دون أن تطلب منهم تغيير كلمة المرور؟

**علامات تحذير:** يقترح التشفير Encryption بدلاً من الهاش، أو MD5 أو SHA-1.

**مقالات تشرح الموضوع:** [التشفير للمطورين: ماذا تستخدم ولماذا لا تخترع خوارزميتك](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B4%D9%81%D9%8A%D8%B1-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D9%87%D8%A7%D8%B4-%D9%88%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD/)، و[الدخول دون كلمة مرور: شرح Passkeys وWebAuthn](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%AF%D9%88%D9%86-%D9%83%D9%84%D9%85%D8%A9-%D9%85%D8%B1%D9%88%D8%B1-passkeys-%D9%88-fido2/).

### 28\. المستخدم غير الرقم في الرابط فرأى فاتورة شخص آخر، ما الخطأ؟

متوسط **ماذا يختبر:** أشهر ثغرة في ال API: BOLA أو IDOR، والفرق بين المصادقة والتفويض.

إجابة المبتدئ

لم نتحقق أن الفاتورة تخص المستخدم، فيجب أن نضيف شرطاً على رقم المستخدم.

إجابة الخبير

هذه ثغرة BOLA (Broken Object Level Authorization) وهي الأولى في OWASP API Security Top 10: المصادقة نجحت (نعرف من هو)، والتفويض لكل كائن غائب. والإصلاح ليس في هذه النقطة وحدها، وإنما في نمط يجعل النسيان صعباً: كل استعلام يمر من طبقة تضيف شرط المالك أو المستأجر تلقائياً، أو Row-Level Security في قاعدة البيانات، أو خدمة صلاحيات مركزية، مع اختبار آلي يحاول الوصول بمستخدم آخر لكل نقطة. واستخدام UUID بدلاً من الرقم المتسلسل يصعب التخمين ولكنه ليس إصلاحاً.

أسئلة المتابعة
- كيف تكتب اختباراً يغطي هذه الثغرة لكل نقاط ال API؟
- ما الفرق بين RBAC وABAC وReBAC، ومتى تحتاج الأخير؟

**علامات تحذير:** يقترح UUID أو إخفاء الرقم كحل كامل.

**مقالات تشرح الموضوع:** [أمن ال API بالأمثلة مع OWASP API Security Top 10](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-%D9%85%D8%B9-owasp-api-security-top-10/)، و[Authorization: كيف تجمع قرارات الصلاحيات في مكان واحد](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/).

### 29\. اشرح تسجيل الدخول بحساب Google من البداية حتى الجلسة

خبير **ماذا يختبر:** تدفق Authorization Code مع PKCE وOpenID Connect، والتحقق من التوكن.

إجابة المبتدئ

نحول المستخدم إلى Google، فيسجل الدخول ويعطينا توكن، ونستخدمه لنعرف بريده.

إجابة الخبير

نحول المستخدم إلى Google مع `client_id` و`redirect_uri` و`state` عشوائي و`code_challenge` من PKCE، فيعود بكود قصير العمر، ونستبدله من السيرفر بتوكنات مع `code_verifier`. وفي OpenID Connect نتحقق من `id_token`: التوقيع بمفاتيح Google العامة، و`iss` و`aud` وتاريخ الانتهاء و`nonce`، ثم نربط المستخدم بالمعرف `sub` وليس بالبريد، لأن البريد قد يتغير أو لا يكون موثقاً. ثم ننشئ جلستنا نحن، ولا نستخدم توكن Google كجلسة لتطبيقنا.

أسئلة المتابعة
- ما الهجوم الذي يمنعه ال `state`؟ وما الذي يمنعه PKCE؟
- لماذا تربط الحساب بـ `sub` وليس بالبريد؟

**علامات تحذير:** يخلط بين OAuth للتفويض وOpenID Connect للهوية، أو يقبل ال `id_token` دون تحقق.

**مقالات تشرح الموضوع:** [شرح OAuth وOpenID Connect للمبتدئين](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[تسجيل الدخول بحساب Google في authentik وقصره على نطاق مؤسستك](https://arabroot.io/articles/%D8%AA%D8%B3%D8%AC%D9%8A%D9%84-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A8%D8%AD%D8%B3%D8%A7%D8%A8-google-%D9%81%D9%8A-authentik/).

## أمن التطبيقات OWASP

### 30\. ما هو حقن SQL، ولماذا لا يكفي تنظيف المدخلات؟

مبتدئ **ماذا يختبر:** أساس أمني لا يقبل الخطأ فيه في أي مستوى.

إجابة المبتدئ

المهاجم يكتب SQL داخل حقل الإدخال فينفذ، والحل استخدام Prepared Statements.

إجابة الخبير

الحقن يحدث عندما يختلط الكود بالبيانات في نفس النص، والمعاملات Parameters تفصلهما فترسل قاعدة البيانات الاستعلام والقيم منفصلة، فلا يمكن لأي قيمة أن تصبح كوداً مهما احتوت. أما التنظيف والهروب Escaping فيعتمد على أن تتذكره في كل مكان وأن تعرف كل الحالات. وتبقى أماكن لا تقبل معاملات مثل اسم العمود في `ORDER BY`، وهنا نستخدم قائمة مسموحة Allowlist. وال ORM يحمي ما دمت لا تستخدم دوال النص الخام مثل `raw()` مع f-string.

أسئلة المتابعة
- المستخدم يختار عمود الترتيب من القائمة، كيف تبنيه بأمان؟
- كيف يجد فحص SAST هذه المشكلة؟

**علامات تحذير:** يقترح حذف علامات الاقتباس من المدخلات كحل.

**مقالات تشرح الموضوع:** [OWASP Top 10 للمبتدئين: أخطر عشر مشكلات في تطبيقات الويب](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[ثغرات الويب بالتجربة: تطبيق نكسره ثم نصلحه](https://arabroot.io/articles/%D8%AB%D8%BA%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%88%D9%8A%D8%A8-%D8%A8%D8%A7%D9%84%D8%AA%D8%AC%D8%B1%D8%A8%D8%A9-sql-injection-%D9%88-xss-%D9%88-ssrf/).

### 31\. ميزة «أضف صورة من رابط» في تطبيقك، ما الخطر فيها؟

خبير **ماذا يختبر:** تزوير الطلب من جهة السيرفر SSRF.

إجابة المبتدئ

قد يكون الرابط لملف ضار، فنتحقق من نوع الملف وحجمه.

إجابة الخبير

الخطر الأكبر SSRF: المهاجم يعطي رابطاً داخلياً مثل `http://169.254.169.254/` (خدمة بيانات السحابة التي تعطي مفاتيح الوصول) أو `http://localhost:6379`، فيطلبه السيرفر نيابة عنه من داخل الشبكة. والحماية: نحل الاسم إلى عنوان ونرفض العناوين الخاصة والمحلية، ونتصل بنفس العنوان الذي تحققنا منه (حتى لا يتغير بين التحقق والاتصال DNS Rebinding)، ونمنع التحويلات Redirects أو نتحقق من كل واحدة، ونحدد البروتوكولات بـ http وhttps فقط، والأفضل تمرير هذه الطلبات عبر Proxy خروج Egress معزول.

أسئلة المتابعة
- لماذا لا يكفي التحقق من اسم النطاق في الرابط؟
- ما الذي غيره IMDSv2 في AWS لتقليل هذا الخطر؟

**علامات تحذير:** يركز على الملف وحده ولا يفكر أن السيرفر نفسه سيطلب الرابط.

**مقالات تشرح الموضوع:** [أمن ال API بالأمثلة مع OWASP API Security Top 10](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-%D9%85%D8%B9-owasp-api-security-top-10/)، و[لماذا تفصل مدخل منصتك عن مخرجها؟ Ingress وEgress](https://arabroot.io/articles/%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D9%81%D8%B5%D9%84-%D9%85%D8%AF%D8%AE%D9%84-%D9%85%D9%86%D8%B5%D8%AA%D9%83-%D8%B9%D9%86-%D9%85%D8%AE%D8%B1%D8%AC%D9%87%D8%A7/).

### 32\. أين تضع كلمة مرور قاعدة البيانات ومفاتيح ال API؟

متوسط **ماذا يختبر:** إدارة الأسرار Secrets، وما يحدث بعد التسريب.

إجابة المبتدئ

في ملف `.env` لا ندخله في Git، وفي الإنتاج في متغيرات البيئة.

إجابة الخبير

خارج المستودع دائماً، وفي الإنتاج من مدير أسرار (Vault أو خدمة السحابة أو أسرار المنصة) بصلاحيات لكل خدمة، ومع فحص للأسرار في CI وعلى ال Commits قبل الدفع. والأهم ما بعد التسريب: إذا دخل سر إلى Git فحذفه من ال Commit التالي لا يكفي لأنه في التاريخ وفي كل نسخة، والإجراء الصحيح هو إلغاء السر وإصدار سر جديد فوراً، ثم تنظيف التاريخ إذا لزم. ولهذا نفضل أسراراً قصيرة العمر تصدر تلقائياً على مفاتيح ثابتة لسنوات.

أسئلة المتابعة
- دخل مفتاح AWS إلى مستودع عام قبل ساعة، ما الخطوات بالترتيب؟
- لماذا قد تكون متغيرات البيئة مكاناً غير آمن تماماً؟

**علامات تحذير:** يعتبر حذف الملف من ال Commit التالي كافياً.

**مقالات تشرح الموضوع:** [Secrets Scanning: كيف تكتشف الأسرار المسربة في الكود](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/)، و[البرمجة الآمنة Secure Coding بأمثلة من أربع لغات](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D9%85%D9%86%D8%A9-secure-coding-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/).

## الاختبارات

### 33\. ما الفرق بين اختبار الوحدة واختبار التكامل، وكم تكتب من كل نوع؟

مبتدئ **ماذا يختبر:** أنواع الاختبارات وما يكشفه كل نوع.

إجابة المبتدئ

اختبار الوحدة Unit يختبر دالة وحدها، واختبار التكامل Integration يختبر عدة أجزاء معاً، ونكتب كثيراً من الأول وقليلاً من الثاني.

إجابة الخبير

اختبار الوحدة سريع ويحدد مكان الخطأ بدقة، ولكنه لا يكتشف أخطاء الربط: استعلام SQL خاطئ، أو ترحيل Migration ناقص، أو إعداد خاطئ. واختبار التكامل مع قاعدة بيانات حقيقية في Container يكتشف هذه، وأصبح سريعاً بما يكفي ليكون جزءاً كبيراً من الاختبارات في خدمات Backend التي أغلب منطقها حول البيانات. لذلك لا أعد الاختبارات بالشكل الهرمي، وإنما أسأل: أين تحدث أخطاؤنا فعلاً؟ وأضع الاختبارات هناك.

أسئلة المتابعة
- لماذا قد يكون Mock لقاعدة البيانات خطيراً؟
- كيف تعرف أن اختباراتك تكشف الأخطاء فعلاً، وليس فقط تغطي الأسطر؟

**علامات تحذير:** يرى تغطية 100% هدفاً بذاته، أو لا يكتب اختبارات تكامل أبداً.

**مقالات تشرح الموضوع:** [أنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwright](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)، و[اختبار الطفرات Mutation Testing: هل تكشف اختباراتك الأخطاء؟](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%B7%D9%81%D8%B1%D8%A7%D8%AA-mutation-testing/).

### 34\. اختبار يفشل مرة كل عشرين تشغيلاً، ماذا تفعل؟

متوسط **ماذا يختبر:** التعامل مع الاختبارات المتقلبة Flaky Tests بدلاً من تجاهلها.

إجابة المبتدئ

نعيد تشغيله، وإذا نجح نكمل، وإذا تكرر نبحث عن السبب.

إجابة الخبير

الاختبار المتقلب يكشف شيئاً حقيقياً في أغلب الأحيان: ترتيب غير مضمون (اختبار يعتمد على ترتيب صفوف دون `ORDER BY`)، أو وقت (منتصف الليل، أو منطقة زمنية)، أو حالة مشتركة بين الاختبارات، أو سباق في الكود نفسه. أعزله أولاً حتى لا يعلم الفريق أن الأحمر طبيعي، ثم أشغله مئة مرة لأعيد إنتاجه، مع بذرة عشوائية ثابتة. والسباق في الاختبار قد يكون سباقاً في الإنتاج يحدث مرة كل ألف طلب.

أسئلة المتابعة
- كيف تختبر كوداً يعتمد على الوقت الحالي؟
- متى تقبل إعادة التشغيل التلقائي في CI؟

**علامات تحذير:** يضيف `sleep` حتى ينجح الاختبار.

**مقالات تشرح الموضوع:** [أنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwright](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)، و[التاريخ والوقت للمطورين ومديري الأنظمة](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%A7%D8%B1%D9%8A%D8%AE-%D9%88%D8%A7%D9%84%D9%88%D9%82%D8%AA-utc-%D9%88%D8%A7%D9%84%D9%85%D9%86%D8%A7%D8%B7%D9%82-%D8%A7%D9%84%D8%B2%D9%85%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D8%A7%D9%84%D9%87%D8%AC%D8%B1%D9%8A/).

### 35\. كيف تتأكد أن تعديلك لن يكسر خدمة أخرى تستهلك ال API؟

خبير **ماذا يختبر:** اختبار العقود Contract Testing والنشر الآمن بين الفرق.

إجابة المبتدئ

نكتب اختبار تكامل يشغل الخدمتين معاً ونتأكد أن الطلبات تعمل.

إجابة الخبير

تشغيل كل الخدمات معاً في بيئة واحدة بطيء وهش، ولا يكتشف إلا ما تغطيه السيناريوهات. والبديل اختبار العقود الذي يقوده المستهلك: كل مستهلك يكتب ما يحتاجه فعلاً من ال API في عقد (Pact مثلاً)، والمنتج يتحقق من كل العقود في خطه قبل الدمج، وأداة مثل `can-i-deploy` تمنع النشر إذا كانت النسخة الجديدة تكسر مستهلكاً منشوراً في الإنتاج. وهذا يحول «نسيت أن تطبيق الجوال يستخدم هذا الحقل» من عطل في الإنتاج إلى فشل في CI.

أسئلة المتابعة
- ما الفرق بين اختبار العقود والتحقق من ال OpenAPI Schema؟
- من يملك العقد: المنتج أم المستهلك؟

**علامات تحذير:** يعتمد على بيئة Staging مشتركة يكسرها الجميع.

**المقال الذي يشرح الموضوع:** [اختبار العقود Contract Testing بين الخدمات مع Pact](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%AF-contract-testing-%D9%85%D8%B9-pact/).

## المراقبة والسجلات Observability

### 36\. ماذا تسجل في السجلات، وماذا لا تسجل أبداً؟

مبتدئ **ماذا يختبر:** سجلات مفيدة للتشخيص وآمنة.

إجابة المبتدئ

نسجل الأخطاء والطلبات المهمة، ولا نسجل كلمات المرور.

إجابة الخبير

أسجل بشكل منظم Structured (JSON) مع معرف الطلب Request ID أو Trace ID في كل سطر، حتى أجمع كل ما حدث في طلب واحد عبر عدة خدمات، ومع المستوى والخدمة والمستخدم (بمعرفه وليس باسمه). ولا أسجل كلمات المرور ولا التوكنات ولا أرقام البطاقات ولا بيانات شخصية حساسة، وأنتبه لتسجيل جسم الطلب كاملاً لأنه يحتويها دون أن ينتبه أحد. والخطأ يسجل مرة واحدة في المكان الذي يعالج فيه، وليس في كل طبقة يمر بها، وإلا أصبح الخطأ الواحد عشرة أسطر.

أسئلة المتابعة
- كيف تمنع تسجيل التوكن عن طريق الخطأ في مكتبة لا تتحكم بها؟
- متى تستخدم مستوى DEBUG في الإنتاج؟

**علامات تحذير:** يسجل الطلب والرد كاملين دائماً، أو يعتمد على `print`.

**مقالات تشرح الموضوع:** [السجلات Logging للمطورين: ماذا تسجل وماذا لا تسجل](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)، و[Audit Trail: كيف تبني سجل تدقيق لا يقبل العبث](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82-audit-trail-%D9%85%D9%86-%D9%81%D8%B9%D9%84-%D9%85%D8%A7%D8%B0%D8%A7-%D9%88%D9%85%D8%AA%D9%89/).

### 37\. متوسط زمن الاستجابة 80 ملي ثانية والمستخدمون يشتكون، كيف؟

متوسط **ماذا يختبر:** النسب المئوية Percentiles والتنبيهات المبنية على تجربة المستخدم.

إجابة المبتدئ

ربما بعض الطلبات بطيئة، فننظر إلى أبطأ الطلبات.

إجابة الخبير

المتوسط يخفي الذيل: إذا كان 95% من الطلبات 50 ملي ثانية و5% منها ثلاث ثوان فالمتوسط يبدو جيداً، و5% من الطلبات تعني أن مستخدماً يفتح عشرين طلباً في الصفحة يرى البطء غالباً في كل زيارة. لذلك أراقب P95 وP99 لكل نقطة، وأبني التنبيه على هدف مستوى الخدمة SLO مثل «99% من الطلبات أقل من 300 ملي ثانية خلال 30 يوماً»، وأنبه على سرعة استهلاك ميزانية الخطأ Error Budget وليس على كل قفزة صغيرة، حتى لا يتعود الفريق تجاهل التنبيهات.

أسئلة المتابعة
- لماذا لا يمكن حساب P99 لعدة سيرفرات بمتوسط P99 لكل سيرفر؟
- ما الفرق بين SLI وSLO وSLA؟

**علامات تحذير:** يراقب المتوسط وحده، أو ينبه على استهلاك المعالج بدلاً من تجربة المستخدم.

**مقالات تشرح الموضوع:** [تتبع الطلب البطيء من المتصفح إلى قاعدة البيانات](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/)، و[DevOps وSRE وهندسة المنصات: أين يلتقي كل منها؟](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D8%B1%D9%82-%D8%A8%D9%8A%D9%86-devops-%D9%88-sre-%D9%88-platform-engineering/).

### 38\. طلب يمر بخمس خدمات وأحدها بطيء، كيف تعرف أيها؟

خبير **ماذا يختبر:** التتبع الموزع Distributed Tracing ونقل السياق.

إجابة المبتدئ

نبحث في سجلات كل خدمة عن الطلب ونقارن الأوقات.

إجابة الخبير

التتبع الموزع يعطي كل طلب Trace ID ينتقل بين الخدمات في ترويسة `traceparent` (معيار W3C Trace Context)، وكل خدمة تسجل Spans لعملياتها، فترى الطلب كشجرة زمنية وتعرف أين ذهب الوقت، بما فيه الاستعلامات والانتظار في الطوابير. وأستخدم OpenTelemetry حتى لا أرتبط بأداة واحدة، وأحدد نسبة العينات Sampling لأن تسجيل كل طلب مكلف، مع حفظ كل الطلبات البطيئة والفاشلة (Tail Sampling). وأربط ال Trace ID في السجلات أيضاً حتى أنتقل من التتبع إلى السطر.

أسئلة المتابعة
- كيف ينتقل السياق عبر طابور رسائل؟
- ما الفرق بين Head Sampling وTail Sampling؟

**علامات تحذير:** لم يسمع بالتتبع ويعتمد على مقارنة الأوقات يدوياً في السجلات.

**المقال الذي يشرح الموضوع:** [التتبع الموزع Distributed Tracing مع OpenTelemetry](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9-distributed-tracing-%D9%85%D8%B9-opentelemetry/).

## تشخيص الأداء

### 39\. نقطة واحدة في ال API أصبحت بطيئة، ما خطواتك بالترتيب؟

متوسط **ماذا يختبر:** طريقة منهجية في التشخيص، من القياس إلى السبب.

إجابة المبتدئ

أشغل الطلب محلياً وأضع Logs بالوقت في كل خطوة لأعرف أين البطء.

إجابة الخبير

أبدأ بالقياس في الإنتاج: منذ متى، وهل البطء لكل المستخدمين أم لبعضهم (مستأجر كبير، أو بيانات معينة)، وP99 مقابل P50\. ثم أفتح Trace لطلب بطيء لأرى أين الوقت: قاعدة البيانات أم خدمة خارجية أم المعالج أم انتظار المجمع. فإذا كانت قاعدة البيانات فـ `pg_stat_statements` و`EXPLAIN ANALYZE` بنفس المعاملات الحقيقية، لأن الاستعلام قد يكون سريعاً لمستخدم وبطيئاً لآخر عنده مليون صف. وإذا كان المعالج فـ Profiler. ولا أغير شيئاً قبل أن أعرف أين ذهب الوقت، ثم أقيس بعد التغيير.

أسئلة المتابعة
- الاستعلام سريع عندما تشغله يدوياً وبطيء من التطبيق، لماذا قد يحدث ذلك؟
- كيف تجد أبطأ الاستعلامات في PostgreSQL خلال آخر ساعة؟

**علامات تحذير:** يقترح زيادة السيرفرات أو إضافة كاش قبل القياس.

**مقالات تشرح الموضوع:** [تتبع الطلب البطيء من المتصفح إلى قاعدة البيانات](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%B7%D9%84%D8%A8-%D8%A7%D9%84%D8%A8%D8%B7%D9%8A%D8%A1-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D8%A7%D9%84%D9%89-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/)، و[استكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السبب](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، و[ضبط أداء PostgreSQL بالقياس وليس بالتخمين](https://arabroot.io/articles/%D8%B6%D8%A8%D8%B7-%D8%A7%D8%AF%D8%A7%D8%A1-postgresql-%D8%A8%D8%A7%D9%84%D9%82%D9%8A%D8%A7%D8%B3/).

### 40\. كيف تعرف كم مستخدماً يتحمل النظام قبل حملة تسويقية؟

خبير **ماذا يختبر:** اختبار الأحمال Load Testing بشكل يعطي أرقاماً يمكن الوثوق بها.

إجابة المبتدئ

نستخدم أداة مثل k6 أو JMeter ونرسل عدداً كبيراً من الطلبات ونرى متى يسقط.

إجابة الخبير

أبدأ بتحديد السؤال: كم طلباً في الثانية متوقع، وما زمن الاستجابة المقبول، ثم أبني سيناريو يشبه سلوك المستخدمين الحقيقي (نسبة التصفح إلى الشراء، وبيانات متنوعة حتى لا يخدم الكاش كل شيء)، وأختبر على بيئة تشبه الإنتاج في الحجم والبيانات. وأزيد الحمل تدريجياً وأراقب أين ينكسر أولاً (قاعدة البيانات، المجمع، خدمة خارجية لها حد)، لأن الهدف معرفة عنق الزجاجة وليس رقماً واحداً. وأنتبه لأداة الحمل نفسها: إذا كانت هي التي تصل لحدها فالأرقام كاذبة، وانتبه لنمط الحلقة المغلقة Closed Model الذي يخفي البطء.

أسئلة المتابعة
- ما الفرق بين Load وStress وSoak Testing؟
- لماذا قد يعطيك اختبار بمستخدم واحد مكرر نتائج متفائلة جداً؟

**علامات تحذير:** يختبر على جهازه بمستخدم واحد مكرر ويعمم النتيجة.

**المقال الذي يشرح الموضوع:** [اختبار الأحمال والضغط Load Testing للمبتدئين](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%AD%D9%85%D8%A7%D9%84-%D9%88%D8%A7%D9%84%D8%B6%D8%BA%D8%B7-load-%D9%88-stress-testing-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

## تصميم الأنظمة: حالات قصيرة

### 41\. صمم خدمة لاختصار الروابط تتحمل 5000 تحويل في الثانية

متوسط **ماذا يختبر:** التقدير والمعرفات والكاش في مسألة صغيرة واضحة.

إجابة المبتدئ

جدول فيه الرابط الأصلي والكود القصير، ونولد الكود عشوائياً، وعند الطلب نبحث عنه ونحول المستخدم.

إجابة الخبير

أبدأ بالأرقام: القراءة أكثر من الكتابة بمئة مرة غالباً، و5000 قراءة في الثانية على مفتاح أساسي سهلة لقاعدة بيانات واحدة مع كاش. والكود: عداد متسلسل مشفر بـ Base62 قصير ولكنه قابل للتخمين ويكشف عدد الروابط، أو عشوائي بسبعة أحرف (62 أس 7 تقريباً 3.5 تريليون) مع التحقق من التكرار بقيد فريد. والتحويل 301 يحفظه المتصفح فلا نرى الزيارات التالية، و302 يمر علينا كل مرة فنحسب الإحصاءات. ثم الأمور التي يسأل عنها الخبير: إساءة الاستخدام (روابط تصيد)، وانتهاء الروابط، وأين نحفظ الإحصاءات دون أن نبطئ التحويل (طابور أو سجل يجمع لاحقاً).

أسئلة المتابعة
- كيف تحسب عدد النقرات لكل رابط دون أن تكتب في قاعدة البيانات مع كل تحويل؟
- ماذا يتغير إذا أصبح الحمل 500 ألف تحويل في الثانية؟

**علامات تحذير:** يبدأ بـ Kafka وCassandra وعشر خدمات قبل أي تقدير.

**مقالات تشرح الموضوع:** [مدخل إلى تصميم الأنظمة: من سيرفر واحد إلى ملايين المستخدمين](https://arabroot.io/articles/%D9%85%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D9%85%D9%86-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-%D9%88%D8%A7%D8%AD%D8%AF-%D8%A7%D9%84%D9%89-%D9%85%D9%84%D8%A7%D9%8A%D9%8A%D9%86/)، و[الرقم المتسلسل أم UUID؟ كيف تختار المعرف الفريد](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%B1%D9%81%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D9%8A%D8%AF%D8%A9-uuid-%D9%88-ulid-%D9%88-snowflake/).

### 42\. صمم إرسال إشعار لمليون مستخدم خلال دقيقة دون تكرار

خبير **ماذا يختبر:** التوزيع والطوابير وحدود المزودين ومنع التكرار.

إجابة المبتدئ

نضع الإشعارات في طابور، وعدة Workers ترسلها إلى Firebase.

إجابة الخبير

التقسيم أولاً: الحملة تتحول إلى دفعات من المستخدمين (ألف لكل رسالة في الطابور مثلاً)، والـ Workers يتوسعون أفقياً، والقيد الحقيقي هو حدود المزود (FCM وAPNs) وليس سرعتنا، لذلك نحتاج Rate Limiting من جهتنا وإعادة محاولة مع تأخير متزايد. ومنع التكرار بمفتاح لكل (حملة، جهاز) يحفظ قبل الإرسال، لأن الإرسال مرة على الأقل. ثم التفاصيل التي يعيشها المستخدم: التوكنات المنتهية التي يرجعها المزود ونحذفها، وأوقات الهدوء، والمنطقة الزمنية، وتفضيلات المستخدم، وقياس الوصول والفتح.

أسئلة المتابعة
- المزود رجع خطأ لنصف الدفعة، كيف تعيد دون أن تكرر للنصف الآخر؟
- كيف ترسل حسب المنطقة الزمنية لكل مستخدم في التاسعة صباحاً بتوقيته؟

**علامات تحذير:** لا يذكر حدود المزود أو التكرار.

**مقالات تشرح الموضوع:** [كيف تبني نظام إشعارات يصل إلى ملايين المستخدمين](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%86%D8%B8%D8%A7%D9%85-%D8%A7%D8%B4%D8%B9%D8%A7%D8%B1%D8%A7%D8%AA-%D9%8A%D8%B5%D9%84-%D8%A7%D9%84%D9%89-%D9%85%D9%84%D8%A7%D9%8A%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85%D9%8A%D9%86/)، و[كيف تعمل الإشعارات الفورية Push Notifications؟](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A7%D8%B4%D8%B9%D8%A7%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D9%88%D8%B1%D9%8A%D8%A9-push-notifications/).

### 43\. الفريق يريد تقسيم التطبيق إلى Microservices، هل توافق؟

خبير **ماذا يختبر:** الحكم المعماري: المشكلة قبل الحل، وثمن التوزيع.

إجابة المبتدئ

نعم، لأن Microservices أسهل في التوسع، وكل فريق يعمل على خدمته.

إجابة الخبير

أسأل أولاً: ما المشكلة التي نحلها؟ إذا كانت النشر البطيء وتعارض الفرق فقد يحلها Modular Monolith بحدود واضحة بين الوحدات وخط نشر أفضل، دون أن ندفع ثمن التوزيع: الشبكة بين كل استدعاء، والمعاملات الموزعة، والتتبع، وعشرة خطوط نشر، وفريق منصة. وإذا كانت هناك أجزاء تحتاج توسعاً أو نشراً مستقلاً فعلاً، أفصلها واحدة واحدة على حدود المجال Bounded Context، ولا أقسم حسب الطبقات التقنية. والقاعدة أن حجم الفريق وشكله يقرر أكثر من التقنية.

أسئلة المتابعة
- ما علامات أن الحدود بين خدماتك خاطئة؟
- كيف تفصل أول خدمة من Monolith دون توقف (Strangler Fig)؟

**علامات تحذير:** يرى Microservices ترقية طبيعية لكل نظام.

**مقالات تشرح الموضوع:** [Monolith أم Microservices؟ قرار صادق للفرق الصغيرة والكبيرة](https://arabroot.io/articles/monolith-%D8%A7%D9%85-microservices-%D9%82%D8%B1%D8%A7%D8%B1-%D8%B5%D8%A7%D8%AF%D9%82-%D9%84%D9%84%D9%81%D8%B1%D9%82-%D8%A7%D9%84%D8%B5%D8%BA%D9%8A%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D9%83%D8%A8%D9%8A%D8%B1%D8%A9/)، و[التصميم الموجه بالمجال Domain-Driven Design ببساطة](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D9%85%D9%88%D8%AC%D9%87-%D8%A8%D8%A7%D9%84%D9%85%D8%AC%D8%A7%D9%84-ddd-%D8%A8%D8%A8%D8%B3%D8%A7%D8%B7%D8%A9/).

## أعطال الإنتاج والنشر

### 44\. كيف تنشر تعديلاً على قاعدة البيانات دون توقف الخدمة؟

متوسط **ماذا يختبر:** ترحيل البيانات Migrations المتوافق، والنشر التدريجي.

إجابة المبتدئ

نشغل ال Migration قبل نشر الكود الجديد، ونختار وقتاً قليل الاستخدام.

إجابة الخبير

أثناء النشر يعمل الكود القديم والجديد معاً، لذلك كل ترحيل يجب أن يتوافق مع النسختين: نمط التوسع ثم الانكماش Expand and Contract. لتغيير اسم عمود: أضف العمود الجديد، واكتب في الاثنين، وانقل البيانات القديمة على دفعات، ثم اقرأ من الجديد، ثم بعد نشر آخر احذف القديم. وأنتبه لعمليات تأخذ قفلاً على الجدول كله: إضافة عمود بقيمة افتراضية متغيرة، أو فهرس دون `CONCURRENTLY`، أو تغيير نوع عمود، وأضع `lock_timeout` حتى لا يتوقف كل شيء خلف ترحيل ينتظر قفلاً.

أسئلة المتابعة
- لماذا قد يوقف `ALTER TABLE` سريع كل الطلبات لعدة دقائق؟
- كيف تتراجع عن نشر فيه ترحيل حذف عموداً؟

**علامات تحذير:** يعتمد على «وقت قليل الاستخدام» بدلاً من ترحيل متوافق.

**مقالات تشرح الموضوع:** [PostgreSQL للجداول الضخمة: عشرات الملايين من الصفوف](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/)، و[Feature Flags: انشر الكود وفعل الميزة لاحقاً](https://arabroot.io/articles/%D9%85%D9%81%D9%87%D9%88%D9%85-feature-flags-%D9%88%D8%A7%D9%84%D8%A7%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D8%AA%D8%AF%D8%B1%D9%8A%D8%AC%D9%8A-gradual-rollout/).

### 45\. حذف أحدهم جدولاً في الإنتاج بالخطأ، ماذا تفعل؟

متوسط **ماذا يختبر:** النسخ الاحتياطي الذي اختبر فعلاً، والاستعادة إلى لحظة.

إجابة المبتدئ

نستعيد آخر نسخة احتياطية.

إجابة الخبير

أوقف الكتابة التي قد تزيد الضرر أولاً، ثم أحدد اللحظة قبل الحذف من السجلات. والنسخة الليلية تعني خسارة يوم كامل، لذلك نحتاج الاستعادة إلى لحظة PITR من النسخة الأساسية مع ال WAL، والأفضل أن أستعيدها على سيرفر منفصل وأنسخ الجدول المحذوف فقط إلى الإنتاج، بدلاً من إرجاع قاعدة البيانات كلها وخسارة كل ما كتب بعد الحذف. والسؤال الذي يكشف الخبرة: متى آخر مرة اختبرتم الاستعادة، وكم أخذت من الوقت؟ لأن النسخة التي لم تستعد أبداً هي أمل وليست نسخة.

أسئلة المتابعة
- ما الفرق بين RPO وRTO، وكم هما في نظامكم؟
- لماذا لا تحمي النسخة المتماثلة Replica من هذا الخطأ؟

**علامات تحذير:** يعتبر ال Replica نسخة احتياطية، أو لم يختبر استعادة أبداً.

**مقالات تشرح الموضوع:** [استعادة PostgreSQL إلى أي لحظة مع pgBackRest](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9-postgresql-%D8%A7%D9%84%D9%89-%D8%A7%D9%8A-%D9%84%D8%AD%D8%B8%D8%A9-%D9%85%D8%B9-pgbackrest/)، و[النسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادة](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/).

## مراجعة كود الذكاء الاصطناعي

### 46\. أداة الذكاء الاصطناعي كتبت ميزة كاملة مع اختباراتها، كيف تراجعها؟

متوسط **ماذا يختبر:** الحكم على كود لم يكتبه الإنسان، وعلى اختبارات قد تختبر الخطأ نفسه.

إجابة المبتدئ

أقرأ الكود وأشغل الاختبارات، فإذا نجحت أراجع التنسيق والأسماء.

إجابة الخبير

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

أسئلة المتابعة
- الأداة أضافت مكتبة باسم قريب من مكتبة مشهورة، ما الخطر؟
- كيف تعرف أن الاختبار يكشف الخطأ فعلاً؟ (Mutation Testing)

**علامات تحذير:** يقبل الكود لأن الاختبارات خضراء.

**مقالات تشرح الموضوع:** [اختبارات يكتبها الذكاء الاصطناعي: متى تثق بها؟](https://arabroot.io/articles/%D8%A7%D8%AA%D9%85%D8%AA%D8%A9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D8%A8%D8%A7%D9%84%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D9%84%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A/)، و[OWASP ASVS ومراجعة الكود الأمنية لكل Pull Request](https://arabroot.io/articles/%D9%85%D8%AA%D8%B7%D9%84%D8%A8%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D9%85%D8%A7%D9%86-%D9%88%D9%85%D8%B1%D8%A7%D8%AC%D8%B9%D8%A9-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D9%85%D8%B9-owasp-asvs/)، و[أمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### 47\. متى تعرف أن أداة الذكاء الاصطناعي تخطئ بثقة؟

خبير **ماذا يختبر:** الخبرة التي تكشف الإجابة المقنعة الخاطئة.

إجابة المبتدئ

عندما لا يعمل الكود أو تظهر أخطاء عند التشغيل.

إجابة الخبير

الخطأ الخطير هو الذي يعمل: دالة API غير موجودة في الإصدار الذي نستخدمه، أو خيار إعداد قديم، أو حل يعمل لمستخدم واحد ويفشل مع التزامن، أو كود أمني يبدو صحيحاً (تشفير بوضع ECB، أو مقارنة توكن بـ `==`). وما يكشفه هو أن أعرف الأساس بنفسي، وأن أتحقق من الادعاءات في الوثائق الرسمية للإصدار المستخدم، وأن أشغل الكود تحت الظروف الصعبة وليس المثال السعيد. وأنتبه أكثر عندما يكون الموضوع خارج خبرتي، لأن هناك بالضبط لا أستطيع أن أرى الخطأ.

أسئلة المتابعة
- اذكر مرة اقترحت فيها الأداة شيئاً خاطئاً وكيف اكتشفته.
- كيف تقلل هذا في الفريق كله وليس عندك وحدك؟

**علامات تحذير:** لا يتذكر أي مرة أخطأت فيها الأداة.

**مقالات تشرح الموضوع:** [لماذا تتمرن دون ذكاء اصطناعي؟ تمارين Kata لك ولفريقك](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D8%AD%D8%A7%D9%81%D8%B8-%D8%B9%D9%84%D9%89-%D9%85%D9%87%D8%A7%D8%B1%D8%AA%D9%83-%D8%AF%D9%88%D9%86-%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A/)، و[ما هي جودة الكود Code Quality وكيف تقيسها؟](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%AC%D9%88%D8%AF%D8%A9-%D8%A7%D9%84%D9%83%D9%88%D8%AF-code-quality-%D9%88%D9%83%D9%8A%D9%81-%D8%AA%D9%82%D9%8A%D8%B3%D9%87%D8%A7/).

## تمارين عملية للمقابلة

الأسئلة وحدها تقيس ما يعرفه المرشح، أما التمرين العملي فيقيس ما يستطيع أن يفعله أمام كود حقيقي ومخرج حقيقي، وفيما يلي ثلاثة تمارين شغلناها فعلاً على PostgreSQL 18.6 داخل Docker وPython 3.13 مع SQLAlchemy 2.0.54 وpsycopg 3.3.6، والمخرجات المنشورة هي المخرجات الحقيقية. أعط المرشح الكود والمخرج الأول، واطلب منه أن يشرح ما يراه ثم يصلحه، والإجابة في الزر أسفل كل تمرين. ولتشغيلها بنفسك:

```bash
docker network create lab-iv-net
docker run -d --name lab-iv-pg --network lab-iv-net -e POSTGRES_USER=lab -e POSTGRES_PASSWORD=lab -e POSTGRES_DB=lab postgres:18.6
docker run -d --name lab-iv-py --network lab-iv-net -v "$PWD:/w" -w /w python:3.13.16-slim sleep infinity
docker exec lab-iv-py pip install "sqlalchemy==2.0.*" "psycopg[binary]==3.*"
docker exec lab-iv-py python n1.py
```

### التمرين الأول: صفحة آخر الطلبات

صفحة تعرض آخر 200 طلب مع اسم العميل، والكود يعمل والاختبارات خضراء، ولكن الصفحة بطيئة في الإنتاج. والكود التالي يكتب نفس الصفحة بثلاث طرق ويعد الاستعلامات التي ترسل فعلاً إلى قاعدة البيانات:

```python
# n1.py: the "latest orders" page, written three ways
import time
from sqlalchemy import create_engine, event, ForeignKey, String, select, insert
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session, selectinload, joinedload

class Base(DeclarativeBase):
    pass

class Customer(Base):
    __tablename__ = "customers"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(80))

class Order(Base):
    __tablename__ = "orders"
    id: Mapped[int] = mapped_column(primary_key=True)
    customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
    total: Mapped[int]
    customer: Mapped[Customer] = relationship()   # default loading: lazy="select"

engine = create_engine("postgresql+psycopg://lab:lab@lab-iv-pg/lab")
queries = 0

@event.listens_for(engine, "before_cursor_execute")
def count(conn, cursor, statement, params, context, executemany):
    global queries
    queries += 1

Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
with Session(engine) as s:
    s.execute(insert(Customer), [{"id": i, "name": f"customer-{i}"} for i in range(1, 501)])
    s.execute(insert(Order), [{"customer_id": (i % 500) + 1, "total": i} for i in range(1, 20001)])
    s.commit()

def latest_orders(options=()):
    global queries
    queries = 0
    t = time.perf_counter()
    with Session(engine) as s:
        orders = s.scalars(select(Order).options(*options).order_by(Order.id.desc()).limit(200)).unique().all()
        rows = [(o.id, o.customer.name, o.total) for o in orders]   # the template reads o.customer.name
    return len(rows), queries, (time.perf_counter() - t) * 1000

for label, opts in [("lazy (default)", ()),
                    ("selectinload", (selectinload(Order.customer),)),
                    ("joinedload", (joinedload(Order.customer),))]:
    latest_orders(opts)                      # warm up the connection pool
    n, q, ms = latest_orders(opts)
    print(f"{label:15} rows={n}  queries={q:3}  time={ms:6.1f} ms")
```

والمخرج سوف يكون كما يلي:

```text
lazy (default)  rows=200  queries=201  time= 209.6 ms
selectinload    rows=200  queries=  2  time=  47.2 ms
joinedload      rows=200  queries=  1  time=   7.4 ms
```

اسأل المرشح: من أين جاءت 201 استعلام، والسطر الذي يرسلها لا يحتوي أي استعلام؟ ولماذا الفرق في الوقت هنا صغير نسبياً وفي الإنتاج أكبر بكثير؟ ومتى تختار `selectinload` رغم أنه أبطأ هنا من `joinedload`؟ والصورة التالية تبين ما يحدث في الحالتين:

![على اليمين التطبيق يرسل استعلاماً واحداً للطلبات ثم سهماً لكل طلب يجلب عميله، فيصبح المجموع 201 استعلاماً إلى قاعدة البيانات، وعلى اليسار نفس الصفحة باستعلام واحد بـ JOIN أو باستعلامين مع IN، وتحت كل جانب العدد والوقت المقاس](https://arabroot.io/content/images/2026/10/interview-backend-01-n-plus-one.webp)

نفس الصفحة: 201 استعلاماً إلى قاعدة البيانات أو استعلام واحد

الإجابة 

العلاقة `customer` معرفة بالتحميل الكسول Lazy Loading الافتراضي في SQLAlchemy كما توضح [وثائق تحميل العلاقات](https://docs.sqlalchemy.org/en/20/orm/queryguide/relationships.html?ref=arabroot.io)، فالاستعلام الأول يجلب 200 طلب فقط، ثم السطر `o.customer.name` داخل الحلقة يرسل استعلاماً لكل طلب لم يحمل عميله بعد، ولأن كل طلب في البيانات لعميل مختلف فقد أصبحت 200 استعلام إضافية (ولو تكرر العميل لاستخدم ال ORM النسخة المحملة في ال Identity Map ولم يرسل استعلاماً).

والفرق في الوقت هنا 200 ملي ثانية تقريباً لأن قاعدة البيانات على نفس الجهاز، وزمن الذهاب والعودة Round Trip أقل من ملي ثانية، أما في الإنتاج حيث قاعدة البيانات على سيرفر آخر أو في منطقة سحابية أخرى فكل ذهاب وعودة يأخذ من 1 إلى 5 ملي ثانية، فتصبح 201 استعلاماً نصف ثانية إلى ثانية كاملة قبل أي عمل حقيقي، وهذا ما يجعل المشكلة غير مرئية على جهاز المطور.

والإصلاح سطر واحد في الاستعلام: `joinedload` يرسل استعلاماً واحداً بـ LEFT OUTER JOIN، و`selectinload` يرسل استعلامين (الطلبات، ثم العملاء بـ `WHERE id IN (...)`). والأول أسرع هنا لأن العلاقة من طرف «واحد» (لكل طلب عميل واحد)، أما مع علاقة «كثير» (عميل له 500 طلب) فإن JOIN يكرر بيانات الأب في كل صف ويضخم النتيجة، وهنا يكون `selectinload` أفضل. والمرشح الخبير يقترح أيضاً حماية دائمة: اختبار يعد الاستعلامات لكل صفحة ويفشل إذا تجاوزت حداً، أو تفعيل `lazy="raise"` على العلاقات حتى يفشل أي تحميل كسول غير مقصود بخطأ واضح.

### التمرين الثاني: عشر تذاكر وخمسون مشترياً

فعالية فيها عشر تذاكر فقط، وخمسون مستخدماً يضغطون «اشتر» في نفس اللحظة. الدالة الأولى هي ما كتبه المطور، والثانية والثالثة بديلان، و`time.sleep(0.02)` تمثل استدعاء بوابة الدفع بين القراءة والكتابة:

```python
# race.py: 50 buyers, 10 tickets, three versions of "buy"
import time
from concurrent.futures import ThreadPoolExecutor
import psycopg

DSN = "postgresql://lab:lab@lab-iv-pg/lab"

def reset():
    with psycopg.connect(DSN, autocommit=True) as c:
        c.execute("DROP TABLE IF EXISTS sales; DROP TABLE IF EXISTS events")
        c.execute("CREATE TABLE events (id int PRIMARY KEY, stock int NOT NULL)")
        c.execute("CREATE TABLE sales (id serial PRIMARY KEY, event_id int, buyer int)")
        c.execute("INSERT INTO events VALUES (1, 10)")

def buy_read_then_write(buyer):          # version 1: what the junior wrote
    with psycopg.connect(DSN) as c:
        stock = c.execute("SELECT stock FROM events WHERE id = 1").fetchone()[0]
        if stock <= 0:
            return False
        time.sleep(0.02)                     # the payment call happens here
        c.execute("UPDATE events SET stock = %s WHERE id = 1", (stock - 1,))
        c.execute("INSERT INTO sales (event_id, buyer) VALUES (1, %s)", (buyer,))
        return True

def buy_for_update(buyer):               # version 2: lock the row first
    with psycopg.connect(DSN) as c:
        stock = c.execute("SELECT stock FROM events WHERE id = 1 FOR UPDATE").fetchone()[0]
        if stock <= 0:
            return False
        time.sleep(0.02)
        c.execute("UPDATE events SET stock = stock - 1 WHERE id = 1")
        c.execute("INSERT INTO sales (event_id, buyer) VALUES (1, %s)", (buyer,))
        return True

def buy_conditional(buyer):              # version 3: one atomic statement decides
    with psycopg.connect(DSN) as c:
        row = c.execute("UPDATE events SET stock = stock - 1 WHERE id = 1 AND stock > 0 RETURNING stock").fetchone()
        if row is None:
            return False
        c.execute("INSERT INTO sales (event_id, buyer) VALUES (1, %s)", (buyer,))
        return True

for fn in (buy_read_then_write, buy_for_update, buy_conditional):
    reset()
    t = time.perf_counter()
    with ThreadPoolExecutor(max_workers=50) as pool:
        ok = sum(pool.map(fn, range(50)))
    ms = (time.perf_counter() - t) * 1000
    with psycopg.connect(DSN) as c:
        stock = c.execute("SELECT stock FROM events WHERE id = 1").fetchone()[0]
        sold = c.execute("SELECT count(*) FROM sales").fetchone()[0]
    print(f"{fn.__name__:20} accepted={ok:2}  sales rows={sold:2}  stock left={stock:2}  time={ms:5.0f} ms")
```

والمخرج سوف يكون كما يلي:

```text
buy_read_then_write  accepted=50  sales rows=50  stock left= 8  time=  467 ms
buy_for_update       accepted=10  sales rows=10  stock left= 0  time=  748 ms
buy_conditional      accepted=10  sales rows=10  stock left= 0  time=  703 ms
```

اسأل المرشح: لماذا قبلت الدالة الأولى خمسين عملية بيع والمخزون لم ينزل إلا تذكرتين؟ وما الفرق بين الحلين الآخرين؟ وأيهما تختار ولماذا؟

الإجابة 

في الدالة الأولى قرأ كل طلب قيمة المخزون ثم حسب القيمة الجديدة في Python ثم كتبها، فكل الطلبات التي قرأت `10` قبل أن يكتب أي منها كتبت `9`، وهذا هو التحديث المفقود Lost Update، والمعاملة الضمنية التي يفتحها psycopg بمستوى Read Committed لا تمنعه، كما تشرح [وثائق مستويات العزل في PostgreSQL](https://www.postgresql.org/docs/current/transaction-iso.html?ref=arabroot.io) لأن كل جملة فيها صحيحة وحدها. ولاحظ أن العدد المتبقي (8 في هذا التشغيل) يتغير من تشغيل لآخر، وهذا بالضبط ما يجعل الخطأ صعب الاكتشاف في الاختبارات.

والحل الثاني `SELECT ... FOR UPDATE` يقفل الصف، فينتظر كل طلب حتى ينتهي الذي قبله، والنتيجة صحيحة (10 مبيعات)، ولكن القفل يمتد فوق استدعاء الدفع، فإذا تأخرت البوابة ثانية فكل المشترين ينتظرون في طابور على صف واحد. والحل الثالث يجعل القرار والكتابة جملة واحدة ذرية: `UPDATE ... WHERE stock > 0 RETURNING stock`، فقاعدة البيانات نفسها تضمن أن الشرط يعاد تقييمه على آخر نسخة من الصف، وإذا لم يرجع صف فالتذاكر نفدت. وهو الأبسط، ولكنه يحجز التذكرة قبل الدفع، لذلك يحتاج في نظام حقيقي حالة «محجوزة» بمدة انتهاء وإرجاعاً للمخزون إذا فشل الدفع.

والوقت في المخرج لا يقارن بين الحلول بدقة، لأن أغلبه فتح خمسين اتصالاً جديداً بقاعدة البيانات في نفس اللحظة، وهذه ملاحظة يلتقطها المرشح الخبير وحده: في الإنتاج نستخدم مجمع اتصالات Connection Pool.

### التمرين الثالث: الصفحة رقم 45001

جدول فيه مليون منشور، والتطبيق يعرض 20 منشوراً في الصفحة بالترتيب الأحدث أولاً، وعليه فهرس على أعمدة الترتيب. والملف التالي يبني الجدول ثم يطلب نفس الصفحة بطريقتين:

```sql
-- page.sql: one million events, page 45,001 of a feed of 20 per page
DROP TABLE IF EXISTS feed;
CREATE TABLE feed (id bigint PRIMARY KEY, created_at timestamptz NOT NULL, body text);
INSERT INTO feed SELECT g, timestamptz '2026-01-01' + g * interval '1 second', 'post ' || g
FROM generate_series(1, 1000000) g;
CREATE INDEX feed_created_id ON feed (created_at DESC, id DESC);
ANALYZE feed;

\echo '--- OFFSET'
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY ON)
SELECT id, created_at FROM feed ORDER BY created_at DESC, id DESC OFFSET 900000 LIMIT 20;
```

```sql
\echo '--- KEYSET'
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY ON)
SELECT id, created_at FROM feed
WHERE (created_at, id) < (timestamptz '2026-01-02 03:46:41+00', 100001)
ORDER BY created_at DESC, id DESC LIMIT 20;
```

والمخرج سوف يكون كما يلي (بعد حذف رأس الجدول وأسطر ال Planning Buffers):

```text
--- OFFSET
 Limit (actual rows=20.00 loops=1)
   Buffers: shared hit=6618 read=3451
   ->  Index Only Scan using feed_created_id on feed (actual rows=900020.00 loops=1)
         Heap Fetches: 900020
         Index Searches: 1
         Buffers: shared hit=6618 read=3451
 Planning Time: 0.332 ms
 Execution Time: 697.913 ms
```

```text
--- KEYSET
 Limit (actual rows=20.00 loops=1)
   Buffers: shared hit=3 read=1
   ->  Index Only Scan using feed_created_id on feed (actual rows=20.00 loops=1)
         Index Cond: (ROW(created_at, id) < ROW('2026-01-02 03:46:41+00'::timestamp with time zone, 100001))
         Heap Fetches: 20
         Index Searches: 1
         Buffers: shared hit=3 read=1
 Planning Time: 0.179 ms
 Execution Time: 0.122 ms
```

اسأل المرشح: الاستعلامان يستخدمان نفس الفهرس، فلماذا الأول أبطأ بخمسة آلاف مرة؟ ومن أين جاءت القيمتان في شرط الاستعلام الثاني؟ وما الذي يخسره المستخدم مع الطريقة الثانية؟

الإجابة 

انظر إلى `actual rows=900020` في الاستعلام الأول: قاعدة البيانات مشت على الفهرس من بدايته وقرأت 900020 صفاً ثم رمت أول 900000 منها، فالفهرس يعطيها الترتيب ولا يعطيها طريقة للقفز إلى الصف رقم 900000، و[وثائق PostgreSQL](https://www.postgresql.org/docs/current/queries-limit.html?ref=arabroot.io) تقول ذلك صراحة: الصفوف التي يتخطاها OFFSET يجب أن تحسب داخل السيرفر. أما الاستعلام الثاني فيبدأ من مكان محدد في الشجرة بشرط `Index Cond` ويقرأ 20 صفاً فقط، لذلك يبقى وقته ثابتاً تقريباً مهما كان عمق الصفحة.

والقيمتان هما ترتيب آخر صف في الصفحة السابقة (المنشور 100001)، والتطبيق يرسلهما مع الصفحة في Cursor بدلاً من رقم الصفحة. ووجود `id` مع `created_at` ضروري لأن منشورين قد يكون لهما نفس الوقت، فبدونه قد يتكرر صف أو يختفي على حدود الصفحات. ولاحظ أن مقارنة الصفوف `(created_at, id) < (...)` تستخدم الفهرس فقط لأن الفهرس على نفس الأعمدة وبنفس اتجاه الترتيب.

وما يخسره المستخدم هو القفز إلى صفحة برقمها، وهذا مقبول في التمرير اللانهائي وفي ال API التي تقرأها برامج. ولاحظ أيضاً `Heap Fetches: 900020`: الجدول جديد ولم يمر عليه VACUUM، فخريطة الرؤية Visibility Map فارغة واضطرت قاعدة البيانات لزيارة الجدول نفسه رغم أنه Index Only Scan، والمرشح الخبير يلتقط هذه التفصيلة.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- اختر ثمانية أسئلة تقريباً لكل جلسة من مجالات مختلفة، واترك أغلب الوقت لأسئلة المتابعة.
- الفرق بين المبتدئ والخبير في Backend يظهر عند التزامن والأعطال والموازنة، وليس في التعريفات.
- التمرين العملي يكشف ما لا تكشفه الأسئلة: 201 استعلاماً خلف سطر واحد، وخمسون عملية بيع لعشر تذاكر، وصفحة أبطأ بخمسة آلاف مرة بنفس الفهرس.
- كل سؤال هنا مرتبط بمقال يشرح الموضوع، فاستخدم الروابط لتجهز نفسك، أو أرسلها لمرشح تريد أن تساعده على التعلم.

وبقية مقالات السلسلة:

[![](https://arabroot.io/content/images/2026/10/interview-guide-cover-cover.webp)دليلكيف تدير مقابلة تقنية تكشف مستوى المرشح؟كيف تبني مقابلة تقنية منظمة تفرق بين المبتدئ والخبير بأسئلة المتابعة والتقييم بالدرجات بدلاً من الانطباع.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AF%D9%8A%D8%B1-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D8%AA%D9%82%D9%86%D9%8A%D8%A9-%D8%AA%D9%83%D8%B4%D9%81-%D9%85%D8%B3%D8%AA%D9%88%D9%89-%D8%A7%D9%84%D9%85%D8%B1%D8%B4%D8%AD/)[![](https://arabroot.io/content/images/2026/10/interview-frontend-cover-cover.webp)دليلأسئلة مقابلة مطور Frontend من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور Frontend، لكل سؤال إجابة المبتدئ وإجابة الخبير وأسئلة متابعة، مع تمارين عملية شغلناها فعلاً.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A6%D9%84%D8%A9-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%85%D8%B7%D9%88%D8%B1-frontend-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AE%D8%A8%D9%8A%D8%B1/)[![](https://arabroot.io/content/images/2026/10/interview-mobile-cover-cover.webp)دليلأسئلة مقابلة مطور تطبيقات الجوال من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور تطبيقات الجوال على Android وiOS، لكل سؤال إجابة المبتدئ وإجابة الخبير، مع تمارين عملية شغلناها.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A6%D9%84%D8%A9-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%85%D8%B7%D9%88%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AE%D8%A8%D9%8A%D8%B1/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتشغيل التمارين الثلاثة على PostgreSQL 18.6 وPython 3.13.16 مع SQLAlchemy 2.0.54 وpsycopg 3.3.6 داخل Docker على Windows 11.