الدروس
متوسط ما بعد الديمو · ٢ من ٥

ما بعد الديمو — قاعدة البيانات والأداء في تطبيقات الفايب كودنق

قاعدة البيانات التي تعمل اليوم قد تنهار غدًا — من تصميم الجداول إلى الفهارس والمعاملات والتزامن والتخزين المؤقت.

د
Devsamhan
٢٠ يوليو ٢٠٢٦ ٦٥ دقيقة قراءة ٤٨ نقطة تحقق ما بعد الديمو — Devsamhan
ما بعد الديمو — قاعدة البيانات والأداء في تطبيقات الفايب كودنق

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

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

وبذلك أنهينا المحطة الأولى من رحلتنا من النموذج التجريبي إلى المنتج الحقيقي.

📚
خمسة أجزاء — دليل كامل لما بعد الديمو
  1. الأمن السيبراني وحماية التطبيق — انتهينا منه في الجزء الأول.
  2. قواعد البيانات والأداء — الجزء الحالي: التصميم، الفهارس، N+1، المعاملات، التزامن، الاتصالات، والتخزين المؤقت.
  3. الاعتمادية والاستعداد لبيئة الإنتاج.
  4. كتابة كود نظيف وقابل للصيانة بدل كود السباغيتي.
  5. اختبار ما بناه الذكاء الاصطناعي وإثبات جودته بالأدلة.

في هذا الجزء سننتقل من سؤال:

هل يستطيع المستخدم اختراق الميزة أو تجاوز صلاحياتها؟

إلى سؤال آخر لا يقل أهمية:

هل تستطيع قاعدة البيانات تنفيذ هذه الميزة بصورة صحيحة وسريعة عندما يزداد عدد المستخدمين والطلبات؟

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

نجاح الديمو لا يعني أن تصميم البيانات صحيح، مثلما أن ظهور الصفحة لا يعني أن التطبيق آمن.

١. قاعدة البيانات ليست مجرد مكان لحفظ البيانات

عند بدء مشروع باستخدام الفايب كودنق، غالبًا يكون التركيز على الواجهة:

ثم يأتي دور قاعدة البيانات في صورة طلب بسيط:

أنشئ الجداول المطلوبة واربطها بالتطبيق.

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

لكن قاعدة البيانات ليست صندوقًا نلقي فيه البيانات ثم نستعيدها عند الحاجة. إنها تمثل الحقيقة الأساسية للنظام:

قاعدة البيانات المصممة جيدًا لا تحفظ البيانات فقط، بل تساعد في رفض الحالات غير الصحيحة.

٢. من قاعدة بيانات تحفظ البيانات إلى قاعدة بيانات تحمي معناها

لنفترض أن لديك جدولًا للاشتراكات:

CREATE TABLE subscriptions (
  id uuid PRIMARY KEY,
  user_id uuid,
  plan_name text,
  status text,
  amount numeric,
  started_at timestamp,
  ended_at timestamp
);

الجدول يعمل، ويمكنك إدخال الاشتراكات وقراءتها. لكن هناك أسئلة لم تُحسم:

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

لذلك يمكن لقاعدة البيانات نفسها أن تفرض جزءًا من القواعد:

CREATE TABLE subscriptions (
  id uuid PRIMARY KEY,
  user_id uuid NOT NULL REFERENCES users(id),
  plan_code text NOT NULL,
  status text NOT NULL
    CHECK (status IN ('active', 'cancelled', 'expired')),
  amount_in_halalas bigint NOT NULL
    CHECK (amount_in_halalas >= 0),
  started_at timestamptz NOT NULL,
  ended_at timestamptz,
  CHECK (ended_at IS NULL OR ended_at >= started_at)
);

الفرق بين التصميمين أن الأول يقول:

سأحفظ أي قيمة تصل إليّ.

أما الثاني فيقول:

سأحفظ القيم التي تتوافق مع معنى النظام فقط.
📌
نقطة مهمة لمطور الفايب كودنق

لا تطلب من الذكاء الاصطناعي إنشاء الجداول قبل أن تشرح له قواعد البيانات التي يجب ألا يسمح بانتهاكها.

٣. ابدأ بقواعد العمل، لا بأسماء الجداول

قبل إنشاء أي جدول، اسأل:

  1. ما الكيانات الأساسية في المنتج؟
  2. ما العلاقة بينها؟
  3. من يملك كل سجل؟
  4. ما الحالات التي يمر بها السجل؟
  5. ما القيم الإلزامية؟
  6. ما الأشياء التي يجب أن تكون فريدة؟
  7. ما البيانات التي لا يجوز تعديلها بعد إنشائها؟
  8. ماذا يحدث عند الحذف؟
  9. ما العمليات التي يجب أن تنفذ معًا؟
  10. ما الاستعلامات التي ستُستخدم كثيرًا؟

إذا كان لديك تطبيق للطلبات، فقد تكون الكيانات:

لكن لا تجعل الذكاء الاصطناعي يضع كل ذلك في جدول واحد:

orders
- user_name
- user_email
- product_1
- product_2
- product_3
- address
- payment_status
- delivery_status

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

٤. لا تحوّل كل شيء إلى JSON لمجرد أنه أسهل

تدعم كثير من قواعد البيانات تخزين JSON، وهو مفيد في حالات كثيرة، لكنه قد يتحول إلى مهرب من تصميم قاعدة البيانات.

قد يقترح المساعد:

CREATE TABLE requests (
  id uuid PRIMARY KEY,
  data jsonb NOT NULL
);

ثم يضع داخل data:

{
  "userId": "123",
  "status": "approved",
  "amount": 500,
  "department": "finance",
  "createdAt": "..."
}

هذا سريع في النموذج الأولي، لكنه قد يصعّب:

JSON ليس خطأ. قد يكون مناسبًا للبيانات المرنة، أو الإعدادات، أو Snapshot تاريخي، أو بيانات مزود خارجي لا تتحكم في بنيتها. لكن الحقول الأساسية التي تعتمد عليها الصلاحيات، والبحث، والحسابات، والعلاقات، وحالات العمل، ينبغي غالبًا أن تكون أعمدة واضحة ذات أنواع وقيود واضحة.

💡
قاعدة عملية

إذا كنت ستبحث في القيمة، أو تربطها بجدول آخر، أو تفرض عليها صلاحيات، أو تستخدمها في قاعدة عمل، فاسأل أولًا: هل ينبغي أن تكون عمودًا حقيقيًا بدل أن تختبئ داخل JSON؟

٥. لا تستخدم النصوص لكل شيء

من السهل أن ينشئ الذكاء الاصطناعي معظم الحقول كنوع text:

amount: "500"
isActive: "true"
createdAt: "2026-07-20"
status: "anything"

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

مثلًا، بدل تخزين:

10.50 ريال

يمكن تخزين:

1050 هللة

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

٦. لا تجعل الواجهة مسؤولة عن الحقيقة

في الجزء الأول تحدثنا عن نقل قواعد العمل الحساسة إلى الخادم، لكن الأمر يمتد إلى قاعدة البيانات.

لنفترض أن الواجهة تحسب إجمالي الطلب:

const total = items.reduce(
  (sum, item) => sum + item.price * item.quantity,
  0
);

ثم ترسل:

{
  "items": ["product-1", "product-2"],
  "total": 50
}

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

الواجهة تستطيع عرض إجمالي تقديري سريع، لكنها ليست الجهة التي تحدد الحقيقة المالية.

٧. لا تُرجع كل الأعمدة في كل استعلام

من أكثر الأوامر شيوعًا:

SELECT * FROM users;

أو في ORM:

const users = await db.user.findMany();

قد يعمل ذلك، لكنه يعيد حقولًا لا تحتاج إليها الشاشة. لنفترض أن صفحة اختيار المستلم تحتاج:

لكن الاستعلام يعيد أيضًا:

هذه مشكلة أمنية وأدائية في الوقت نفسه. استخدم قائمة حقول صريحة:

const users = await db.user.findMany({
  select: {
    id: true,
    displayName: true,
    avatarUrl: true
  }
});

تشجع وثائق Prisma الرسمية على استخدام select وinclude بصورة مقصودة لإعادة الحقول والعلاقات المطلوبة فقط، بما يجعل النتيجة أصغر وأكثر وضوحًا.

العقد الخفيف — Lightweight Payload

كلما زادت البيانات في الاستجابة:

لا تجعل API مرآة كاملة لصف قاعدة البيانات. أعد النتيجة التي يحتاج إليها الاستخدام الحالي، وليس كل ما تعرفه قاعدة البيانات.

٨. مشكلة N+1: الشاشة التي تنفذ مئات الاستعلامات

تعد مشكلة N+1 Query من أشهر مشكلات الأداء في التطبيقات التي تستخدم ORMs.

تبدأ باستعلام واحد لجلب قائمة:

const orders = await getOrders();

ثم داخل حلقة تجلب بيانات المستخدم لكل طلب:

for (const order of orders) {
  order.customer = await getUser(order.userId);
}

إذا كان لديك 100 طلب، فقد تنفذ:

أي 101 استعلام لعرض صفحة واحدة.

تعرّف وثائق Prisma مشكلة N+1 بأنها تنفيذ استعلام واحد لجلب القائمة، ثم استعلام إضافي لكل عنصر، مما يسبب جولات غير ضرورية إلى قاعدة البيانات.

كيف تعالجها؟

بحسب الأداة والتصميم، يمكن استخدام:

مثلًا:

const orders = await db.order.findMany({
  include: {
    customer: {
      select: {
        id: true,
        displayName: true
      }
    }
  }
});

لكن انتبه: علاج N+1 لا يعني تحميل كل العلاقات دائمًا. قد تتحول من مئة استعلام صغير إلى استعلام ضخم يعيد آلاف الصفوف والحقول.

المطلوب ليس:

استخدم Eager Loading في كل مكان.

بل:

افهم عدد الاستعلامات، وحجم النتائج، وما تحتاج إليه الشاشة فعلًا.

٩. كيف تعرف أن لديك N+1؟

لا تعتمد فقط على قراءة الكود. فعّل تسجيل الاستعلامات في بيئة التطوير أو الاختبار، ثم افتح الشاشة.

إذا رأيت الاستعلام نفسه يتكرر عشرات المرات مع اختلاف المعرّف فقط، فهذه إشارة قوية. مثلًا:

SELECT * FROM users WHERE id = '1';
SELECT * FROM users WHERE id = '2';
SELECT * FROM users WHERE id = '3';
SELECT * FROM users WHERE id = '4';

يمكن لأدوات ORM تسجيل استعلامات SQL المنفذة، وتعرض وثائق Prisma الرسمية خيار تسجيل الاستعلامات ضمن إعدادات العميل.

اطلب من المساعد البرمجي ألا يكتفي بالنظر إلى الكود، بل أن يشغل الميزة ويعرض:

١٠. الفهارس: أسرع طريق إلى الصفوف المطلوبة

إذا كان لديك جدول يحتوي مليون معاملة، وتبحث دائمًا بهذه الصورة:

SELECT *
FROM transactions
WHERE user_id = $1
ORDER BY occurred_at DESC;

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

CREATE INDEX idx_transactions_user_date
ON transactions (user_id, occurred_at DESC);

توضح وثائق PostgreSQL أن الفهارس تساعد الخادم على العثور على صفوف محددة واسترجاعها أسرع مما لو بحث دون فهرس، لكنها تضيف كذلك تكلفة على عمليات الكتابة والتخزين، ولذلك يجب استخدامها بعناية.

لماذا لا نضيف فهرسًا إلى كل عمود؟

لأن كل فهرس يحتاج إلى:

قد تسرّع الفهارس القراءة، لكنها تبطئ الكتابة بدرجات مختلفة. لهذا لا تقل للمساعد:

أضف فهارس لجميع الأعمدة.

بل قل:

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

١١. ترتيب أعمدة الفهرس مهم

هذان الفهرسان ليسا متطابقين:

(user_id, occurred_at)
(occurred_at, user_id)

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

لا تختَر ترتيب الأعمدة بناءً على شكل الجدول، بل على كيفية الاستعلام عن البيانات.

١٢. لا تخمّن أداء الاستعلام: استخدم EXPLAIN

قد ينظر الذكاء الاصطناعي إلى استعلام ويقول: "هذا الاستعلام محسّن." لكن الأداء لا يُحكم عليه من الشكل وحده. يعتمد على:

يوفر PostgreSQL أمر EXPLAIN لرؤية الخطة التي اختارها مخطط الاستعلام، بينما ينفذ EXPLAIN ANALYZE الاستعلام ويعرض معلومات فعلية عن التنفيذ. وتشير الوثائق الرسمية إلى أن اختيار خطة مناسبة حاسم للأداء.

مثال:

EXPLAIN ANALYZE
SELECT id, amount_in_halalas, occurred_at
FROM transactions
WHERE user_id = '...'
ORDER BY occurred_at DESC
LIMIT 20;
📌
تنبيه

EXPLAIN ANALYZE ينفذ الاستعلام فعليًا. إذا استخدمته مع DELETE أو UPDATE أو INSERT، فقد يعدّل البيانات. استخدمه على بيئة آمنة أو داخل معاملة يمكن التراجع عنها، وافهم الأمر قبل تشغيله على الإنتاج.

١٣. لا تبدأ التحسين قبل القياس

من السهل أن يقترح المساعد: Redis، Sharding، Read Replicas، Materialized Views، Microservices.

لكن قد تكون المشكلة الحقيقية:

ابدأ بالقياس:

يمكن لإضافة pg_stat_statements في PostgreSQL تتبع إحصاءات التخطيط والتنفيذ للاستعلامات، بما يساعد على معرفة الاستعلامات المتكررة أو المكلفة بدل الاعتماد على التخمين.

١٤. Pagination: لا تحمل كل البيانات دفعة واحدة

قد يبدأ التطبيق بهذا الاستعلام:

const notifications = await getAllNotifications();

يعمل عندما يكون لدى المستخدم 20 إشعارًا. لكن ماذا عندما يصبح لديه 20 ألف إشعار؟

يجب ألا تحمل القائمة كاملة ثم تعرض أول عشرين عنصرًا. استخدم Pagination على مستوى قاعدة البيانات.

Offset Pagination

مثل:

SELECT *
FROM notifications
ORDER BY created_at DESC
LIMIT 20 OFFSET 100;

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

Cursor أو Keyset Pagination

مثل:

SELECT *
FROM notifications
WHERE created_at < $cursor_date
ORDER BY created_at DESC
LIMIT 20;

قد تكون أكثر ملاءمة للقوائم الكبيرة والمتغيرة، بشرط وجود ترتيب ثابت وفهرس مناسب.

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

١٥. المعاملات: إما أن تنجح العملية كاملة أو تفشل كاملة

تخيّل عملية تحويل 100 ريال من حساب A إلى حساب B:

  1. خصم 100 ريال من A.
  2. إضافة 100 ريال إلى B.
  3. تسجيل حركة التحويل.

ماذا لو نجحت الخطوة الأولى، ثم فشلت الثانية؟ يخسر الحساب A المال، ولا يحصل عليه B.

يجب أن تنفذ الخطوات كوحدة واحدة داخل Transaction:

BEGIN;

UPDATE accounts
SET balance_in_halalas = balance_in_halalas - 10000
WHERE id = 'A';

UPDATE accounts
SET balance_in_halalas = balance_in_halalas + 10000
WHERE id = 'B';

INSERT INTO transfers (
  from_account_id,
  to_account_id,
  amount_in_halalas
)
VALUES ('A', 'B', 10000);

COMMIT;

إذا فشلت خطوة:

ROLLBACK;

ترتبط المعاملات بخصائص ACID: الذرية، والاتساق، والعزل، والاستمرارية، وهي خصائص تهدف إلى الحفاظ على صحة العمليات حتى مع التزامن والأخطاء.

لكن مجرد كتابة BEGIN وCOMMIT لا يجعل العملية صحيحة تلقائيًا. يجب كذلك التحقق من الرصيد والتزامن والصلاحيات وطريقة القفل.

١٦. شرط الرصيد وحده قد لا يكون كافيًا

في الجزء الأول استخدمنا مثال السحب المالي:

if (amount > balance) {
  reject();
}

هذا يمنع سحب مبلغ أكبر من الرصيد في طلب واحد. لكن تخيل وصول طلبين في اللحظة نفسها:

يقرأ الطلب الأول الرصيد: 600. ويقرأ الطلب الثاني الرصيد: 600. كلاهما يرى أن السحب مسموح. ثم ينفذ كلاهما العملية.

هذه Race Condition: تعتمد النتيجة على توقيت عمليات متزامنة تتنافس على البيانات نفسها.

تغطي وثائق PostgreSQL التحكم في التزامن عندما تحاول جلستان أو أكثر الوصول إلى البيانات نفسها، وتهدف آليات العزل والقفل إلى السماح بالوصول المتزامن مع الحفاظ على سلامة البيانات.

حل ذري محتمل

يمكن تنفيذ التحقق والتعديل في أمر واحد:

UPDATE accounts
SET balance_in_halalas = balance_in_halalas - $amount
WHERE id = $account_id
  AND balance_in_halalas >= $amount
RETURNING balance_in_halalas;

إذا لم يرجع الاستعلام صفًا، فالرصيد غير كافٍ أو الحساب غير موجود ضمن النطاق المسموح.

بهذا لا تفصل بين: 1) قراءة الرصيد، 2) اتخاذ القرار، 3) تعديل الرصيد.

لكن الحل النهائي يعتمد على قواعد النظام، ومستوى العزل، والعمليات المرتبطة، وقد تحتاج إلى SELECT ... FOR UPDATE أو قيد آخر أو معاملة أشمل.

١٧. مستويات العزل ليست أسماء نظرية فقط

توجد مستويات مختلفة لعزل المعاملات، مثل: Read Committed، Repeatable Read، Serializable.

كل مستوى يوازن بين التزامن والصرامة. لا يعني اختيار أعلى مستوى دائمًا أن التطبيق أصبح أفضل؛ فقد يزيد احتمال إعادة المحاولة أو يقلل التوازي في بعض السيناريوهات.

المهم أن تحدد:

توضح وثائق PostgreSQL مستويات العزل وسلوكها، وتشير إلى أن تطبيقات المستويات الأعلى قد تحتاج إلى التعامل مع Serialization Failures وإعادة تنفيذ المعاملة.

لا تطلب من الذكاء الاصطناعي:

استخدم أفضل Isolation Level.

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

١٨. Deadlock: عندما تنتظر كل عملية الأخرى

تخيل معاملتين:

المعاملة الأولى:

  1. تقفل الحساب A.
  2. تحاول قفل الحساب B.

المعاملة الثانية:

  1. تقفل الحساب B.
  2. تحاول قفل الحساب A.

كل معاملة تنتظر الأخرى. هذا يسمى Deadlock.

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

كيف نقلل Deadlocks؟

يوفر PostgreSQL إحصائية لعدد حالات Deadlock المكتشفة ضمن نظام إحصاءات قاعدة البيانات.

١٩. Idempotency: لا تنفذ العملية مرتين

تخيل أن المستخدم ضغط زر الدفع. وصل الطلب إلى الخادم، ونُفذت العملية، لكن الاتصال انقطع قبل وصول الاستجابة.

التطبيق لا يعرف هل نجحت العملية، فيعيد الطلب. إذا لم يكن النظام مصممًا لتحمل التكرار، فقد ينفذ الدفع مرتين.

Idempotency تعني أن تكرار الطلب نفسه لا يؤدي إلى تكرار أثره بعد التنفيذ الأول.

يمكن للعميل إرسال مفتاح فريد للعملية:

Idempotency-Key: 3ea4f...

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

توضح وثائق Stripe الرسمية أن Idempotency Keys تسمح بإعادة محاولة الطلبات بأمان دون تنفيذ العملية نفسها مرتين، خصوصًا عندما ينقطع الاتصال ولا يعرف العميل نتيجة المحاولة الأولى.

أين تحتاج Idempotency؟

خصوصًا في:

المفتاح وحده لا يكفي

يجب تحديد:

٢٠. اتصالات قاعدة البيانات مورد محدود

من السهل أن يتعامل التطبيق مع اتصال قاعدة البيانات كأنه مورد غير محدود:

const connection = openDatabaseConnection();

لكل طلب، أو لكل دالة، أو لكل عملية Serverless. لكن قاعدة البيانات لديها حد للاتصالات المتزامنة، وكل اتصال يستهلك موارد.

توضح وثائق PostgreSQL أن max_connections يحدد العدد الأقصى للاتصالات المتزامنة، وأن القيمة الافتراضية تكون عادةً 100، لكنها قد تختلف بحسب إعدادات النظام والمنصة.

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

إذا فتحت كل نسخة من التطبيق أو كل Serverless Function اتصالات كثيرة، قد تصل إلى الحد وتبدأ الأخطاء:

too many connections

٢١. Connection Pooling: مشاركة الاتصالات بدل فتح اتصال لكل طلب

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

PgBouncer مثلًا هو Connection Pooler خفيف لـPostgreSQL، ويمكن للتطبيق الاتصال به كما لو كان خادم PostgreSQL. يوفر PgBouncer أوضاعًا مختلفة، منها:

لكن أوضاع Pooling ليست قابلة للتبديل بلا دراسة؛ فقد تعتمد بعض المكتبات أو الميزات على حالة الجلسة أو Prepared Statements أو خصائص معينة.

لذلك لا تقل للمساعد:

فعّل Connection Pooling.

بل اطلب منه أن يتحقق من:

٢٢. لماذا تظهر المشكلة مع Serverless؟

قد تتوسع المنصات Serverless بإنشاء نسخ كثيرة من الوظيفة عند زيادة الطلبات. إذا أنشأت كل نسخة مجموعة اتصالات كبيرة، فقد يصبح لديك:

عدد النسخ × حجم المجموعة

من الاتصالات المحتملة. مثلًا:

بينما قاعدة البيانات قد تسمح بعدد أقل بكثير. الحل قد يشمل:

رفع max_connections ليس الحل الأول دائمًا؛ فقد يزيد استهلاك الموارد دون معالجة سبب فتح الاتصالات بهذا الشكل.

٢٣. التخزين المؤقت: لا تجعل قاعدة البيانات تعيد العمل نفسه

إذا كانت لديك نتيجة مكلفة لا تتغير كثيرًا، فقد لا يكون من المنطقي حسابها عند كل طلب. يمكن استخدام Caching لتخزين النتيجة مؤقتًا وإعادة استخدامها. لكن التخزين المؤقت ليس طبقة واحدة.

١. Browser Cache

يخزن المتصفح ملفات مثل: CSS، JavaScript، الصور، الخطوط، وبعض استجابات HTTP.

يتحكم Cache-Control في سلوك التخزين داخل المتصفح والـShared Caches مثل Proxies وCDNs.

إذا كانت أسماء ملفات البناء تحتوي على Hash يتغير عند تغير المحتوى، يمكن تخزين الملفات الثابتة مدة طويلة:

Cache-Control: public, max-age=31536000, immutable

أما HTML الذي يشير إلى هذه الملفات فقد يحتاج سياسة مختلفة حتى يحصل المستخدم على الإصدار الجديد.

٢. CDN Cache

يوزع المحتوى على نقاط قريبة من المستخدمين، ويقلل وصول الطلبات إلى الخادم الأصلي. يناسب غالبًا:

لكن تخزين استجابة شخصية في Shared Cache بطريقة خاطئة قد يؤدي إلى ظهور بيانات مستخدم لمستخدم آخر. تميز وثائق MDN بين Private Cache المرتبط بعميل محدد وShared Cache، وتحذر من تسرب المحتوى الشخصي إذا خُزن في Cache مشترك.

٣. Application Cache

يخزن التطبيق نتائج مثل:

٤. Database Cache

تدير قاعدة البيانات نفسها ذاكرة وPages وخططًا وإحصاءات، ولا ينبغي الخلط بينها وبين Redis أو Cache التطبيق.

٢٤. Cache-Aside: ابحث في الكاش أولًا

في نمط Cache-Aside:

  1. يبحث التطبيق في Cache.
  2. إذا وجد النتيجة، يعيدها.
  3. إذا لم يجدها، يقرأها من قاعدة البيانات.
  4. يخزنها مؤقتًا.
  5. يعيدها للمستخدم.

تصف Redis هذا النمط بأنه شائع للأنظمة كثيفة القراءة، حيث يدير التطبيق الوصول إلى Cache وقاعدة البيانات.

مثال مبسط:

async function getProduct(id: string) {
  const key = `product:${id}`;

  const cached = await cache.get(key);

  if (cached) {
    return JSON.parse(cached);
  }

  const product = await database.getProduct(id);

  await cache.set(key, JSON.stringify(product), {
    ttlSeconds: 300
  });

  return product;
}

لكن المثال يخفي أصعب سؤال:

ماذا يحدث عندما يتغير المنتج في قاعدة البيانات؟

٢٥. Cache Invalidation: كيف تمنع عرض بيانات قديمة؟

لنفترض أن سعر المنتج تغير من 100 إلى 80 ريالًا، لكن Cache ما زال يحتوي على السعر القديم. قد يستمر المستخدمون في رؤية 100 حتى تنتهي مدة التخزين.

هناك استراتيجيات مختلفة:

توضح Redis أن العملاء الذين يتلقون رسائل Invalidation يجب أن يزيلوا المفاتيح المقابلة حتى لا يستمروا في تقديم بيانات قديمة.

لا توجد سياسة واحدة مناسبة لكل البيانات. عرض صورة قديمة لعدة ثوانٍ ليس مثل عرض:

كلما كانت البيانات أكثر حساسية، قلت قدرتك على قبول Stale Data.

٢٦. Cache Stampede: عندما ينتهي المفتاح في اللحظة الخطأ

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

بدل أن يحمي Cache النظام، تصبح لحظة انتهاء المفتاح سببًا في إغراق قاعدة البيانات. هذه هي Cache Stampede.

من الحلول:

تشرح MDN مفهوم Request Collapse، كما تدعم بعض شبكات CDN إعادة التحقق في الخلفية مع تقديم محتوى قديم مؤقتًا عبر stale-while-revalidate.

٢٧. Multi-Layer Cache Coherence: عندما تختلف الطبقات

قد يحتوي النظام على: Cache داخل المتصفح، Cache في CDN، Cache داخل التطبيق، Redis، ذاكرة محلية في كل خادم، وبيانات قاعدة البيانات الأصلية.

تم تحديث البيانات في قاعدة البيانات وRedis، لكن أحد خوادم التطبيق لا يزال يحمل نسخة قديمة في ذاكرته. أو تم تحديث API، لكن CDN ما زال يقدم استجابة سابقة.

هذه مشكلة التناسق بين طبقات التخزين المؤقت.

عند إضافة طبقة Cache جديدة، يجب أن تكتب بوضوح:

٢٨. لا تخزّن كل شيء مؤقتًا

بعض المطورين يرون أن Redis حل سحري للأداء، فيطلبون تخزين كل استجابة. لكن Cache يضيف:

استخدمه عندما تكون لديك قراءة متكررة أو عملية مكلفة تثبت القياسات حاجتك إلى تخزين نتيجتها. لا تضف Cache لإخفاء استعلام سيئ قبل إصلاح الاستعلام نفسه.

٢٩. هل يجب تخزين نتائج الذكاء الاصطناعي؟

قد تكون استدعاءات النماذج مكلفة وبطيئة، ولذلك قد يفيد تخزين بعض النتائج. مثلًا:

لكن لا تستخدم مفتاحًا عامًا لمحتوى شخصي. يجب أن يحتوي المفتاح، بحسب الحالة، على:

وإذا تغير Prompt أو النموذج أو قواعد المشروع، فقد تصبح النتيجة المخزنة غير صالحة حتى لو لم يتغير نص المستخدم. ولا ينبغي تخزين بيانات حساسة لدى طرف أو طبقة لا تتوافق مع سياسة الخصوصية.

٣٠. الضغط وحجم JSON

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

لكن لا تجعل "تصغير Payload" سببًا لاستخدام أسماء غامضة مثل:

{
  "u": "123",
  "n": "Abdullah",
  "s": 1
}

ما لم تكن هناك حاجة خاصة جدًا؛ فوضوح عقود API مهم للصيانة.

٣١. الصور بالحجم الكامل مشكلة بيانات وأداء

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

الأفضل إنشاء نسخ مثل: الصورة الأصلية، Preview، وThumbnail. ثم استخدام الحجم المناسب في كل موضع. هذا يقلل:

لا تجعل قاعدة البيانات تحمل الملف نفسه إذا كانت بنية المشروع تعتمد على Object Storage؛ خزن عادةً بيانات الملف ومساره وصلاحياته، واترك المحتوى لخدمة التخزين المناسبة.

٣٢. Optimistic Rendering: السرعة المتصورة ليست حقيقة قاعدة البيانات

في Optimistic UI يحدث تحديث الواجهة قبل وصول تأكيد الخادم. مثلًا، يضغط المستخدم إعجابًا، فيتغير الزر فورًا ثم يرسل الطلب في الخلفية.

هذا يجعل التطبيق يبدو أسرع، لكنه يحتاج إلى:

يمكن أن يكون Optimistic UI مناسبًا لتفاعل قابل للتراجع، لكنه لا يعني أن قاعدة البيانات نجحت.

لا تعرض للمستخدم:

تم الدفع بنجاح

قبل أن يؤكد الخادم النتيجة المعتمدة.

٣٣. Partitioning ليس Sharding

يخلط البعض بين المصطلحين.

Table Partitioning

تقسيم جدول كبير إلى أجزاء داخل نظام قاعدة البيانات وفق مفتاح، مثل: الشهر، السنة، Tenant، نطاق رقمي، أو Hash.

يدعم PostgreSQL Declarative Partitioning، حيث يكون الجدول المقسّم واجهة منطقية وتُخزن الصفوف فعليًا داخل Partitions وفق مفتاح التقسيم.

قد يفيد في:

لكنه يضيف تعقيدًا إلى: الفهارس، القيود، عمليات الهجرة، الصيانة، واختيار مفتاح التقسيم.

Database Sharding

تقسيم البيانات بين خوادم أو قواعد متعددة. مثلًا: العملاء 1–1000 في قاعدة، والعملاء 1001–2000 في قاعدة أخرى. أو التقسيم حسب المنطقة أو Tenant.

يضيف Sharding مشكلات كبيرة:

لذلك لا ينبغي أن يكون أول حل عند بطء استعلام. إرشادات PostgreSQL تفرق بين Partitioning داخل الجداول وتقسيم البيانات بين خوادم في حلول Data Partitioning موزعة.

٣٤. لا تبدأ بـSharding

قبل التفكير في Sharding، افحص:

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

٣٥. Read Replicas ليست مصدرًا فوريًا دائمًا

عندما تزداد القراءات، قد تستخدم نسخة للقراءة. لكن النسخة قد تتأخر عن الخادم الأساسي.

قد يحدث السيناريو التالي:

  1. يحدّث المستخدم اسمه.
  2. يقرأ التطبيق الملف من Replica.
  3. تظهر القيمة القديمة لثوانٍ.

يسمى ذلك أحيانًا Replication Lag. لذلك تحتاج إلى تحديد العمليات التي تتطلب Read-After-Write Consistency، وقد تقرأها من الخادم الأساسي مؤقتًا.

لا ترسل كل القراءات تلقائيًا إلى Replica دون فهم أثر التأخير.

٣٦. Vector Database ليست بديلًا لقاعدة البيانات العلائقية

مع انتشار تطبيقات الذكاء الاصطناعي، قد يقترح المساعد إضافة Vector Database منذ البداية.

تستخدم قواعد المتجهات لتخزين واسترجاع Embeddings بناءً على التشابه، وهو مفيد في حالات مثل: البحث الدلالي، RAG، العثور على نصوص متشابهة، توصية المحتوى، ومقارنة الصور أو المستندات عند توفر تمثيل مناسب.

لكنها ليست بديلًا طبيعيًا عن الجداول والعلاقات والمعاملات. لا تستخدم Vector Database لتحديد:

قد يحتوي النظام على PostgreSQL للبيانات التشغيلية وVector Index للبحث الدلالي، لكن لكل منهما وظيفة مختلفة.

٣٧. Row-Level Security لا تعالج التصميم السيئ

ناقشنا RLS في الجزء الأول بوصفها طبقة لحماية الصفوف. في PostgreSQL، يمكن إنشاء سياسات تحدد الصفوف التي يسمح للدور بقراءتها أو تعديلها، وتطبق السياسات على مستوى كل جدول.

لكن RLS لا تصلح:

الأمن والتصميم والأداء طبقات متكاملة، وليست ميزة واحدة تحل كل المشكلات. كما يجب اختبار السياسات بأدوار المستخدمين الفعلية؛ لأن بعض الأدوار الإدارية أو مالك الجدول أو الأدوار التي تملك BYPASSRLS قد تتصرف بطريقة مختلفة.

٣٨. علامات قاعدة البيانات الهشة في مشروع Vibe Coding

راجع مشروعك إذا وجدت:

٣٩. كيف توجه الذكاء الاصطناعي لتصميم قاعدة البيانات؟

لا تقل فقط:

صمّم قاعدة بيانات احترافية وقابلة للتوسع.

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

Act as a senior database architect.

Before creating or modifying the schema, inspect the product requirements and identify:

1. Core entities and their ownership.
2. Relationships and cardinality.
3. Required and optional fields.
4. Business invariants that the database should enforce.
5. Unique constraints and foreign keys.
6. Deletion and retention behavior.
7. Tenant and user isolation boundaries.
8. High-frequency read and write paths.
9. Operations that require transactions or concurrency control.
10. Expected data volume and growth assumptions.

Do not create tables yet.

First produce a proposed schema with:

- tables and columns,
- precise data types,
- primary and foreign keys,
- nullability,
- checks and unique constraints,
- indexes tied to actual query patterns,
- transaction boundaries,
- and remaining assumptions requiring product decisions.

Avoid using JSON for core relational fields.
Avoid premature partitioning, sharding, caching, or replicas.
Explain the concrete problem solved by every advanced mechanism.

٤٠. أمر لمراجعة الاستعلامات والأداء

Review this feature for database and API performance.

Do not optimize by intuition alone.

Run the feature and collect evidence for:

- total SQL query count,
- duplicate and N+1 queries,
- slowest queries,
- rows scanned versus rows returned,
- response payload size,
- missing pagination,
- fields and relations loaded but not used,
- index usage,
- lock waits,
- transaction duration,
- database connection count,
- and cache hit or miss behavior.

Use EXPLAIN or EXPLAIN ANALYZE safely where appropriate.
Do not run destructive statements against production data.

For every confirmed issue, report:

1. Exact file and query.
2. Current measured behavior.
3. Root cause.
4. Smallest safe fix.
5. Evidence after the fix.
6. Any cost added to writes, memory, or complexity.

Do not introduce Redis, sharding, replicas, or new infrastructure
unless measurements show a concrete need.

٤١. أمر لمراجعة التزامن والعمليات المالية

Review this workflow for concurrency and data-integrity failures.

Assume multiple requests may arrive at the same time.

Identify:

- read-then-write race conditions,
- missing transactions,
- double submission risks,
- missing idempotency,
- inconsistent lock ordering,
- deadlock risks,
- stale reads,
- duplicate webhook processing,
- and partial failure between database and external API operations.

For each workflow, define:

1. The invariant that must always remain true.
2. The transaction boundary.
3. The required lock or atomic statement.
4. The idempotency strategy.
5. The retry behavior.
6. The failure and rollback behavior.
7. A concurrent integration test that proves the fix.

Do not claim the workflow is safe based only on sequential unit tests.

٤٢. قائمة اختبار لقواعد البيانات والأداء

قبل اعتبار الميزة جاهزة، اطلب الإجابة عن هذه الأسئلة بالأدلة:

٤٨ نقطة تحقق لقواعد البيانات والأداء

تصميم البيانات

  1. هل لكل جدول مسؤولية واضحة؟
  2. هل العلاقات ممثلة بـForeign Keys؟
  3. هل القيم الإلزامية محددة بـNOT NULL؟
  4. هل القيم الفريدة محمية بـUnique Constraint؟
  5. هل حالات العمل مقيدة بقيم مسموحة؟
  6. هل المبالغ والتواريخ والأنواع ممثلة بطريقة صحيحة؟
  7. هل توجد قواعد لا تعتمد على الواجهة وحدها؟
  8. هل حذف السجلات له سلوك واضح؟
  9. هل بيانات المستخدمين وTenants معزولة؟

الاستعلامات

  1. كم استعلامًا تنفذ الشاشة؟
  2. هل توجد مشكلة N+1؟
  3. هل تعاد حقول أو علاقات غير مستخدمة؟
  4. هل جميع القوائم الكبيرة تستخدم Pagination؟
  5. هل البحث والفلترة يحدثان في قاعدة البيانات؟
  6. هل الاستعلامات المهمة لها فهارس مناسبة؟
  7. هل تم فحص خطة التنفيذ؟
  8. هل اختُبرت الميزة بحجم بيانات واقعي؟

التزامن

  1. ماذا يحدث عند وصول طلبين في الوقت نفسه؟
  2. هل العملية الحساسة ذرية؟
  3. هل يمكن أن يصبح الرصيد أو المخزون غير صحيح؟
  4. هل توجد احتمالات Deadlock؟
  5. هل ترتيب الأقفال ثابت؟
  6. هل توجد Idempotency للطلبات القابلة للتكرار؟
  7. هل يمكن معالجة Webhook مرتين؟
  8. هل إعادة المحاولة آمنة؟

الاتصالات

  1. كم اتصالًا تفتح كل نسخة من التطبيق؟
  2. هل يوجد Connection Pool؟
  3. هل حجم Pool متوافق مع عدد النسخ وحد قاعدة البيانات؟
  4. هل تغلق الاتصالات بصورة صحيحة؟
  5. هل توجد معاملات تبقى Idle؟
  6. ماذا يحدث عند امتلاء الاتصالات؟
  7. هل Serverless Scaling قد يضاعف عددها؟

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

  1. ما البيانات التي يجري تخزينها؟
  2. ما Source of Truth؟
  3. ما مدة TTL؟
  4. كيف يحدث Invalidation؟
  5. هل المفاتيح تفصل المستخدمين وTenants؟
  6. هل توجد بيانات شخصية في Shared Cache؟
  7. ماذا يحدث إذا تعطل Cache؟
  8. كيف تمنع Cache Stampede؟
  9. ما أقصى مدة مسموحة لعرض بيانات قديمة؟

API والحمولة

  1. هل تعيد الاستجابة الحقول المطلوبة فقط؟
  2. ما حجم Payload؟
  3. هل JSON مضغوط عند الحاجة؟
  4. هل الصور تقدم بالحجم المناسب؟
  5. هل العلاقات المتداخلة محدودة؟
  6. هل الواجهة تحمل بيانات لن تعرضها؟
  7. هل API Contract مستقر وواضح؟

٤٣. لا تقبل عبارة: «تم تحسين قاعدة البيانات»

مثلما قلنا في الجزء الأول: لا تقبل عبارة «تم تأمين الميزة» دون اختبار، لا تقبل أيضًا:

تم تحسين الاستعلامات.

اطلب أرقامًا. تقرير ضعيف:

The database queries were optimized and indexes were added.

تقرير أقوى:

Before:
- 101 SQL queries when loading 100 orders.
- 2.4 MB API response.
- P95 response time: 1.8 seconds.

After:
- 2 SQL queries.
- 186 KB API response.
- P95 response time: 240 milliseconds.

Validation:
- 12 integration tests passed.
- EXPLAIN ANALYZE confirmed use of idx_orders_tenant_created_at.
- No change in returned business data.

لا تكفي الألوان الخضراء ولا الكلمات الواثقة. اطلب:

٤٤. الخلاصة: المشكلة لا تبدأ عندما تكبر قاعدة البيانات

يقال أحيانًا: "سنهتم بالأداء عندما يصبح لدينا مستخدمون كثيرون." وهذا صحيح جزئيًا؛ فلا ينبغي بناء بنية موزعة معقدة قبل الحاجة.

لكن هناك فرقًا بين تأجيل التحسين المبكر، وتأجيل التصميم الصحيح.

لا تحتاج في أول يوم إلى: Sharding، عشر قواعد بيانات، Redis Cluster، Read Replicas في عدة مناطق، Event Sourcing، أو بنية موزعة ضخمة.

لكنك تحتاج منذ البداية إلى:

الذكاء الاصطناعي قد يبني لك جدولًا خلال دقائق. وقد يكتب ORM Models وMigrations وRepositories واستعلامات وربطًا كاملًا بالواجهة.

لكن مسؤوليته الافتراضية غالبًا هي:

اجعل البيانات تظهر.

أما مسؤوليتك بصفتك مطورًا فهي أن تسأل:

هل تمثل هذه الجداول معنى المنتج؟
هل تستطيع قاعدة البيانات رفض الحالة الخاطئة؟
كم استعلامًا تنفذ هذه الشاشة؟
ماذا يحدث عند وصول طلبين معًا؟
هل يمكن تنفيذ العملية مرتين؟
هل ستنفد الاتصالات؟
هل Cache يعرض بيانات قديمة؟
وهل أملك قياسات تثبت أن الحل أصبح أفضل؟

فالتطبيق الذي يعمل مع خمسة مستخدمين قد لا يعمل مع خمسة آلاف. والاستعلام الذي يستغرق عشرين ملي ثانية على مئة سجل قد يصبح كارثة على ملايين السجلات. والعملية المالية التي تنجح في الاختبار اليدوي قد تفشل عند التزامن. والـCache الذي يسرّع الصفحة قد يعرض رصيدًا أو صلاحية قديمة.

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

٤٥. في الجزء الثالث

بعد أن انتهينا في الجزء الأول من الأمن السيبراني، وناقشنا في هذا الجزء قواعد البيانات والأداء والتزامن والاتصالات والتخزين المؤقت، سننتقل في الجزء الثالث إلى مستوى أكبر:

ماذا يحدث عندما تتعطل خدمة خارجية، أو يمتلئ Queue، أو يسقط خادم، أو يفشل النشر، أو تحتاج إلى التراجع عن إصدار؟

سنتناول:

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