في الجزء الأول من هذه السلسلة تحدثنا عن الأمن السيبراني، وعن الخطأ الذي قد لا يكون في سطر كتبه الذكاء الاصطناعي، بل في شرط حماية لم يكتبه أصلًا.
رأينا أن الزر المعطل في الواجهة ليس نظام صلاحيات، وأن نجاح تسجيل الدخول لا يعني أن المستخدم يحق له الوصول إلى كل شيء، وأن الخادم يجب أن يعيد التحقق من المدخلات وقواعد العمل، وألا يثق في الواجهة أو المستخدم أو حتى مخرجات نموذج الذكاء الاصطناعي.
وبذلك أنهينا المحطة الأولى من رحلتنا من النموذج التجريبي إلى المنتج الحقيقي.
- الأمن السيبراني وحماية التطبيق — انتهينا منه في الجزء الأول.
- قواعد البيانات والأداء — الجزء الحالي: التصميم، الفهارس، N+1، المعاملات، التزامن، الاتصالات، والتخزين المؤقت.
- الاعتمادية والاستعداد لبيئة الإنتاج.
- كتابة كود نظيف وقابل للصيانة بدل كود السباغيتي.
- اختبار ما بناه الذكاء الاصطناعي وإثبات جودته بالأدلة.
في هذا الجزء سننتقل من سؤال:
هل يستطيع المستخدم اختراق الميزة أو تجاوز صلاحياتها؟
إلى سؤال آخر لا يقل أهمية:
هل تستطيع قاعدة البيانات تنفيذ هذه الميزة بصورة صحيحة وسريعة عندما يزداد عدد المستخدمين والطلبات؟
فالتطبيق قد يكون آمنًا، لكنه بطيء. وقد يحمي بيانات كل مستخدم، لكنه ينفذ مئات الاستعلامات لعرض شاشة واحدة. وقد يعمل جيدًا مع عشرة مستخدمين، ثم تتوقف قاعدة البيانات عندما يصل العدد إلى ألف. وقد ينفذ عملية مالية بصورة صحيحة عند التجربة اليدوية، لكنه يكررها مرتين إذا انقطع الاتصال وأعاد العميل إرسال الطلب.
نجاح الديمو لا يعني أن تصميم البيانات صحيح، مثلما أن ظهور الصفحة لا يعني أن التطبيق آمن.
١. قاعدة البيانات ليست مجرد مكان لحفظ البيانات
عند بدء مشروع باستخدام الفايب كودنق، غالبًا يكون التركيز على الواجهة:
- أنشئ صفحة تسجيل.
- أضف نموذجًا.
- اربط زر الحفظ.
- اعرض البيانات في قائمة.
- أضف البحث والتصفية.
- أنشئ لوحة تحكم.
ثم يأتي دور قاعدة البيانات في صورة طلب بسيط:
أنشئ الجداول المطلوبة واربطها بالتطبيق.
فيبني المساعد البرمجي بضعة جداول، ويضيف أعمدة، ويربط الواجهة بالخادم، وتبدأ البيانات في الظهور. من الخارج يبدو كل شيء ناجحًا.
لكن قاعدة البيانات ليست صندوقًا نلقي فيه البيانات ثم نستعيدها عند الحاجة. إنها تمثل الحقيقة الأساسية للنظام:
- من يملك هذه البيانات؟
- ما العلاقة بين الجداول؟
- ما القيم التي يجوز تخزينها؟
- ما الذي يجب أن يكون فريدًا؟
- ما الذي لا يجوز حذفه؟
- ما الذي يحدث إذا وصل طلبان في اللحظة نفسها؟
- كيف نضمن تنفيذ عدة تغييرات كوحدة واحدة؟
- كيف نستعيد البيانات بسرعة عندما يصل عدد السجلات إلى الملايين؟
- كيف نمنع مستخدمًا من الوصول إلى صفوف مستخدم آخر؟
- وكيف نمنع التطبيق نفسه من إدخال بيانات متناقضة؟
قاعدة البيانات المصممة جيدًا لا تحفظ البيانات فقط، بل تساعد في رفض الحالات غير الصحيحة.
٢. من قاعدة بيانات تحفظ البيانات إلى قاعدة بيانات تحمي معناها
لنفترض أن لديك جدولًا للاشتراكات:
CREATE TABLE subscriptions (
id uuid PRIMARY KEY,
user_id uuid,
plan_name text,
status text,
amount numeric,
started_at timestamp,
ended_at timestamp
);
الجدول يعمل، ويمكنك إدخال الاشتراكات وقراءتها. لكن هناك أسئلة لم تُحسم:
- هل
user_idإلزامي؟ - هل يمكن أن يشير إلى مستخدم غير موجود؟
- ما حالات
statusالمسموحة؟ - هل يمكن أن يكون المبلغ سالبًا؟
- هل يمكن أن يكون تاريخ النهاية أقدم من تاريخ البداية؟
- هل تسمح بوجود اشتراكين نشطين للخطة نفسها؟
- ماذا يحدث عند حذف المستخدم؟
- هل المبلغ يمثل الريال أم الهللة؟
- هل الوقت محفوظ بمنطقة زمنية واضحة؟
- هل الاشتراك يخص مستخدمًا فقط أم مؤسسة أو Tenant أيضًا؟
قد تكون الواجهة تمنع بعض هذه الأخطاء، لكننا تعلمنا في الجزء الأول أن الواجهة ليست مصدرًا موثوقًا للحماية.
لذلك يمكن لقاعدة البيانات نفسها أن تفرض جزءًا من القواعد:
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)
);
الفرق بين التصميمين أن الأول يقول:
سأحفظ أي قيمة تصل إليّ.
أما الثاني فيقول:
سأحفظ القيم التي تتوافق مع معنى النظام فقط.
لا تطلب من الذكاء الاصطناعي إنشاء الجداول قبل أن تشرح له قواعد البيانات التي يجب ألا يسمح بانتهاكها.
٣. ابدأ بقواعد العمل، لا بأسماء الجداول
قبل إنشاء أي جدول، اسأل:
- ما الكيانات الأساسية في المنتج؟
- ما العلاقة بينها؟
- من يملك كل سجل؟
- ما الحالات التي يمر بها السجل؟
- ما القيم الإلزامية؟
- ما الأشياء التي يجب أن تكون فريدة؟
- ما البيانات التي لا يجوز تعديلها بعد إنشائها؟
- ماذا يحدث عند الحذف؟
- ما العمليات التي يجب أن تنفذ معًا؟
- ما الاستعلامات التي ستُستخدم كثيرًا؟
إذا كان لديك تطبيق للطلبات، فقد تكون الكيانات:
- المستخدم.
- الطلب.
- عناصر الطلب.
- المنتج.
- الدفع.
- عنوان التوصيل.
- سجل تغير حالة الطلب.
لكن لا تجعل الذكاء الاصطناعي يضع كل ذلك في جدول واحد:
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": "..."
}
هذا سريع في النموذج الأولي، لكنه قد يصعّب:
- فرض العلاقات.
- إضافة Foreign Keys.
- منع الحالات غير المسموحة.
- إنشاء فهارس مناسبة.
- كتابة الاستعلامات.
- تنفيذ التقارير.
- تغيير بنية البيانات لاحقًا.
- اكتشاف القيم المفقودة أو غير الصحيحة.
JSON ليس خطأ. قد يكون مناسبًا للبيانات المرنة، أو الإعدادات، أو Snapshot تاريخي، أو بيانات مزود خارجي لا تتحكم في بنيتها. لكن الحقول الأساسية التي تعتمد عليها الصلاحيات، والبحث، والحسابات، والعلاقات، وحالات العمل، ينبغي غالبًا أن تكون أعمدة واضحة ذات أنواع وقيود واضحة.
إذا كنت ستبحث في القيمة، أو تربطها بجدول آخر، أو تفرض عليها صلاحيات، أو تستخدمها في قاعدة عمل، فاسأل أولًا: هل ينبغي أن تكون عمودًا حقيقيًا بدل أن تختبئ داخل JSON؟
٥. لا تستخدم النصوص لكل شيء
من السهل أن ينشئ الذكاء الاصطناعي معظم الحقول كنوع text:
amount: "500"
isActive: "true"
createdAt: "2026-07-20"
status: "anything"
لكن الأنواع ليست مجرد تفاصيل تقنية. النوع الصحيح يمنع حالات كاملة من الأخطاء. استخدم:
- الأنواع الرقمية للأرقام.
- Boolean للقيم المنطقية.
- Date أو Timestamp للتواريخ.
- UUID أو نوع المعرّف المناسب للمعرفات.
- Enum أو Check Constraint للحالات المحددة.
- أصغر وحدة للعملة عند الحاجة لتجنب أخطاء الكسور.
مثلًا، بدل تخزين:
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
كلما زادت البيانات في الاستجابة:
- زاد وقت نقلها.
- زاد استهلاك الشبكة.
- زاد وقت تحليل JSON.
- زاد استهلاك الذاكرة.
- زادت احتمالات كشف معلومات غير لازمة.
- أصبحت عقود API أصعب في التغيير.
لا تجعل API مرآة كاملة لصف قاعدة البيانات. أعد النتيجة التي يحتاج إليها الاستخدام الحالي، وليس كل ما تعرفه قاعدة البيانات.
٨. مشكلة N+1: الشاشة التي تنفذ مئات الاستعلامات
تعد مشكلة N+1 Query من أشهر مشكلات الأداء في التطبيقات التي تستخدم ORMs.
تبدأ باستعلام واحد لجلب قائمة:
const orders = await getOrders();
ثم داخل حلقة تجلب بيانات المستخدم لكل طلب:
for (const order of orders) {
order.customer = await getUser(order.userId);
}
إذا كان لديك 100 طلب، فقد تنفذ:
- استعلامًا واحدًا لجلب الطلبات.
- 100 استعلام إضافي لجلب المستخدمين.
أي 101 استعلام لعرض صفحة واحدة.
تعرّف وثائق Prisma مشكلة N+1 بأنها تنفيذ استعلام واحد لجلب القائمة، ثم استعلام إضافي لكل عنصر، مما يسبب جولات غير ضرورية إلى قاعدة البيانات.
كيف تعالجها؟
بحسب الأداة والتصميم، يمكن استخدام:
- Join.
- Eager Loading.
- Nested Read.
- Batch Query.
- DataLoader.
- جلب المعرفات أولًا ثم تنفيذ استعلام واحد باستخدام
IN.
مثلًا:
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.
لكن قد تكون المشكلة الحقيقية:
- استعلام داخل حلقة.
- غياب فهرس واحد.
- تحميل جميع الصفوف.
- إرجاع أعمدة لا تحتاج إليها.
- عدم وجود Pagination.
- استعلام تقرير يعمل مع كل فتح للصفحة.
- فلترة البيانات في العميل بدل قاعدة البيانات.
ابدأ بالقياس:
- ما أبطأ الاستعلامات؟
- ما أكثر الاستعلامات تكرارًا؟
- كم صفًا تقرأ وكم صفًا تعيد؟
- هل الفهرس مستخدم؟
- ما زمن الاستجابة في النسبة 95 أو 99؟
- كم اتصالًا مفتوحًا؟
- هل تنتظر الطلبات Locks؟
- هل توجد ملفات مؤقتة كبيرة أو عمليات فرز مكلفة؟
يمكن لإضافة 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:
- خصم 100 ريال من A.
- إضافة 100 ريال إلى B.
- تسجيل حركة التحويل.
ماذا لو نجحت الخطوة الأولى، ثم فشلت الثانية؟ يخسر الحساب 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 ريال.
- الطلب الأول يريد سحب 500.
- الطلب الثاني يريد سحب 500.
يقرأ الطلب الأول الرصيد: 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: عندما تنتظر كل عملية الأخرى
تخيل معاملتين:
المعاملة الأولى:
- تقفل الحساب A.
- تحاول قفل الحساب B.
المعاملة الثانية:
- تقفل الحساب B.
- تحاول قفل الحساب A.
كل معاملة تنتظر الأخرى. هذا يسمى Deadlock.
يوضح PostgreSQL أن Deadlock يحدث عندما تحتفظ معاملات بأقفال تحتاج إليها معاملات أخرى، ولا يستطيع أي منها الاستمرار. وتوصي وثائقه بتوحيد ترتيب الحصول على الأقفال لتقليل هذه المشكلة.
كيف نقلل Deadlocks؟
- اقفل الموارد دائمًا بترتيب ثابت.
- اجعل المعاملات قصيرة.
- لا تنفذ استدعاء API خارجيًا وأنت تحتفظ بقفل قاعدة بيانات.
- لا تنتظر إدخال المستخدم داخل Transaction.
- حدّد الاستعلامات التي تحتاج إلى القفل فقط.
- تعامل مع Deadlock بوصفه خطأً قابلًا لإعادة المحاولة في السيناريوهات المناسبة.
- راقب عدد Deadlocks الفعلي بدل افتراض عدم وجودها.
يوفر PostgreSQL إحصائية لعدد حالات Deadlock المكتشفة ضمن نظام إحصاءات قاعدة البيانات.
١٩. Idempotency: لا تنفذ العملية مرتين
تخيل أن المستخدم ضغط زر الدفع. وصل الطلب إلى الخادم، ونُفذت العملية، لكن الاتصال انقطع قبل وصول الاستجابة.
التطبيق لا يعرف هل نجحت العملية، فيعيد الطلب. إذا لم يكن النظام مصممًا لتحمل التكرار، فقد ينفذ الدفع مرتين.
Idempotency تعني أن تكرار الطلب نفسه لا يؤدي إلى تكرار أثره بعد التنفيذ الأول.
يمكن للعميل إرسال مفتاح فريد للعملية:
Idempotency-Key: 3ea4f...
ويحفظ الخادم النتيجة المرتبطة بالمفتاح. إذا وصل الطلب نفسه مجددًا، يعيد الخادم النتيجة السابقة بدل تنفيذ العملية مرة أخرى.
توضح وثائق Stripe الرسمية أن Idempotency Keys تسمح بإعادة محاولة الطلبات بأمان دون تنفيذ العملية نفسها مرتين، خصوصًا عندما ينقطع الاتصال ولا يعرف العميل نتيجة المحاولة الأولى.
أين تحتاج Idempotency؟
خصوصًا في:
- المدفوعات.
- إنشاء الطلبات.
- تحويل الأموال.
- حجز المخزون.
- إرسال عمليات إلى خدمة خارجية.
- معالجة Webhooks.
- إنشاء الموارد المكلفة.
- الطلبات التي يعيد العميل إرسالها تلقائيًا.
المفتاح وحده لا يكفي
يجب تحديد:
- من أنشأ المفتاح؟
- ما مدة الاحتفاظ به؟
- هل هو فريد لكل مستخدم أو Tenant؟
- ماذا يحدث إذا استخدم المفتاح نفسه مع Payload مختلف؟
- هل حُفظت النتيجة قبل إعادة الرد؟
- هل العملية نفسها محمية من التزامن؟
٢٠. اتصالات قاعدة البيانات مورد محدود
من السهل أن يتعامل التطبيق مع اتصال قاعدة البيانات كأنه مورد غير محدود:
const connection = openDatabaseConnection();
لكل طلب، أو لكل دالة، أو لكل عملية Serverless. لكن قاعدة البيانات لديها حد للاتصالات المتزامنة، وكل اتصال يستهلك موارد.
توضح وثائق PostgreSQL أن max_connections يحدد العدد الأقصى للاتصالات المتزامنة، وأن القيمة الافتراضية تكون عادةً 100، لكنها قد تختلف بحسب إعدادات النظام والمنصة.
وهذا لا يعني أن كل مشروع يستطيع استخدام مئة اتصال لتطبيقه. قد تحتاج قاعدة البيانات إلى اتصالات من أجل:
- لوحة الإدارة.
- أدوات الصيانة.
- النسخ الاحتياطي.
- المهام المجدولة.
- خدمات أخرى.
- مراقبة النظام.
إذا فتحت كل نسخة من التطبيق أو كل Serverless Function اتصالات كثيرة، قد تصل إلى الحد وتبدأ الأخطاء:
too many connections
٢١. Connection Pooling: مشاركة الاتصالات بدل فتح اتصال لكل طلب
يقف Connection Pool بين التطبيق وقاعدة البيانات. بدل أن ينشئ كل طلب اتصالًا جديدًا كاملًا، يستعير التطبيق اتصالًا متاحًا من المجموعة، ثم يعيده بعد انتهاء العمل.
PgBouncer مثلًا هو Connection Pooler خفيف لـPostgreSQL، ويمكن للتطبيق الاتصال به كما لو كان خادم PostgreSQL. يوفر PgBouncer أوضاعًا مختلفة، منها:
- Session Pooling: يحتفظ العميل باتصال الخادم طوال مدة جلسته.
- Transaction Pooling: يُخصص اتصال الخادم للعميل أثناء المعاملة فقط، ثم يعاد إلى المجموعة عند انتهائها.
لكن أوضاع Pooling ليست قابلة للتبديل بلا دراسة؛ فقد تعتمد بعض المكتبات أو الميزات على حالة الجلسة أو Prepared Statements أو خصائص معينة.
لذلك لا تقل للمساعد:
فعّل Connection Pooling.
بل اطلب منه أن يتحقق من:
- نوع بيئة التشغيل.
- عدد نسخ التطبيق.
- حجم المجموعة في كل نسخة.
- حد اتصالات قاعدة البيانات.
- توافق ORM مع وضع Pooling.
- استخدام Prepared Statements.
- المهلات.
- الاتصالات الخاملة.
- آلية الإغلاق الصحيح.
- مراقبة عدد الاتصالات الفعلية.
٢٢. لماذا تظهر المشكلة مع Serverless؟
قد تتوسع المنصات Serverless بإنشاء نسخ كثيرة من الوظيفة عند زيادة الطلبات. إذا أنشأت كل نسخة مجموعة اتصالات كبيرة، فقد يصبح لديك:
عدد النسخ × حجم المجموعة
من الاتصالات المحتملة. مثلًا:
- 50 نسخة.
- كل نسخة تفتح 10 اتصالات.
- النتيجة المحتملة: 500 اتصال.
بينما قاعدة البيانات قد تسمح بعدد أقل بكثير. الحل قد يشمل:
- Pooler خارجيًا.
- حجم Pool صغيرًا ومدروسًا.
- إعادة استخدام الاتصال ضمن عمر النسخة.
- حدودًا للتزامن.
- وضع Transaction Pooling عند توافقه.
- نقل العمليات المكلفة إلى Queue.
- استخدام مزود يدعم بنية 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
يوزع المحتوى على نقاط قريبة من المستخدمين، ويقلل وصول الطلبات إلى الخادم الأصلي. يناسب غالبًا:
- الصور العامة.
- CSS وJavaScript.
- الخطوط.
- الملفات القابلة للتنزيل.
- الصفحات العامة المناسبة للتخزين.
- بعض استجابات API العامة المدروسة.
لكن تخزين استجابة شخصية في Shared Cache بطريقة خاطئة قد يؤدي إلى ظهور بيانات مستخدم لمستخدم آخر. تميز وثائق MDN بين Private Cache المرتبط بعميل محدد وShared Cache، وتحذر من تسرب المحتوى الشخصي إذا خُزن في Cache مشترك.
٣. Application Cache
يخزن التطبيق نتائج مثل:
- بيانات الإعدادات العامة.
- قائمة الدول.
- صلاحيات محسوبة.
- تقارير مكلفة.
- نتائج استعلامات متكررة.
- بيانات مزود خارجي.
- نتائج AI متكررة عندما يكون استخدامها آمنًا.
٤. Database Cache
تدير قاعدة البيانات نفسها ذاكرة وPages وخططًا وإحصاءات، ولا ينبغي الخلط بينها وبين Redis أو Cache التطبيق.
٢٤. Cache-Aside: ابحث في الكاش أولًا
في نمط Cache-Aside:
- يبحث التطبيق في Cache.
- إذا وجد النتيجة، يعيدها.
- إذا لم يجدها، يقرأها من قاعدة البيانات.
- يخزنها مؤقتًا.
- يعيدها للمستخدم.
تصف 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 حتى تنتهي مدة التخزين.
هناك استراتيجيات مختلفة:
- حذف Cache عند تحديث البيانات.
- تحديث Cache مباشرة بعد الكتابة.
- استخدام TTL قصير.
- إرسال حدث Invalidation إلى بقية النسخ.
- استخدام Versioned Keys.
- استخدام Stale-While-Revalidate عند قبول بيانات قديمة لفترة قصيرة.
توضح Redis أن العملاء الذين يتلقون رسائل Invalidation يجب أن يزيلوا المفاتيح المقابلة حتى لا يستمروا في تقديم بيانات قديمة.
لا توجد سياسة واحدة مناسبة لكل البيانات. عرض صورة قديمة لعدة ثوانٍ ليس مثل عرض:
- رصيد مالي قديم.
- صلاحية ملغاة.
- حالة دفع.
- سعر نهائي عند إتمام الطلب.
- نتيجة تصويت.
كلما كانت البيانات أكثر حساسية، قلت قدرتك على قبول Stale Data.
٢٦. Cache Stampede: عندما ينتهي المفتاح في اللحظة الخطأ
تخيل أن نتيجة شائعة مخزنة لمدة خمس دقائق. عند انتهاء المدة، تصل ألف طلب في اللحظة نفسها. جميعها لا تجد القيمة في Cache. وجميعها ترسل الاستعلام المكلف إلى قاعدة البيانات.
بدل أن يحمي Cache النظام، تصبح لحظة انتهاء المفتاح سببًا في إغراق قاعدة البيانات. هذه هي Cache Stampede.
من الحلول:
- Request Collapsing أو Single Flight: طلب واحد يعيد بناء القيمة والبقية تنتظر.
- إضافة Jitter إلى مدة TTL حتى لا تنتهي آلاف المفاتيح معًا.
- Stale-While-Revalidate.
- تحديث القيمة قبل انتهائها.
- قفل قصير خاص بإعادة البناء.
- حماية المصدر بحدود التزامن.
تشرح MDN مفهوم Request Collapse، كما تدعم بعض شبكات CDN إعادة التحقق في الخلفية مع تقديم محتوى قديم مؤقتًا عبر stale-while-revalidate.
٢٧. Multi-Layer Cache Coherence: عندما تختلف الطبقات
قد يحتوي النظام على: Cache داخل المتصفح، Cache في CDN، Cache داخل التطبيق، Redis، ذاكرة محلية في كل خادم، وبيانات قاعدة البيانات الأصلية.
تم تحديث البيانات في قاعدة البيانات وRedis، لكن أحد خوادم التطبيق لا يزال يحمل نسخة قديمة في ذاكرته. أو تم تحديث API، لكن CDN ما زال يقدم استجابة سابقة.
هذه مشكلة التناسق بين طبقات التخزين المؤقت.
عند إضافة طبقة Cache جديدة، يجب أن تكتب بوضوح:
- ما Source of Truth؟
- ما المفتاح؟
- ما TTL؟
- متى يحدث Invalidation؟
- كيف تصل رسالة التحديث إلى جميع النسخ؟
- ما أقصى مدة مسموحة للبيانات القديمة؟
- ماذا يحدث إذا تعطل Cache؟
- هل يستطيع النظام الرجوع إلى قاعدة البيانات؟
- هل يجوز تخزين هذه البيانات في Shared Cache؟
- كيف نمنع اختلاط بيانات المستخدمين أو Tenants؟
٢٨. لا تخزّن كل شيء مؤقتًا
بعض المطورين يرون أن Redis حل سحري للأداء، فيطلبون تخزين كل استجابة. لكن Cache يضيف:
- مصدرًا جديدًا للأخطاء.
- بيانات قديمة.
- تكاليف إضافية.
- منطق Invalidation.
- تعقيدًا في التزامن.
- صعوبة أكبر في التصحيح.
- احتمال تسرب بيانات شخصية إذا كانت المفاتيح خاطئة.
استخدمه عندما تكون لديك قراءة متكررة أو عملية مكلفة تثبت القياسات حاجتك إلى تخزين نتيجتها. لا تضف Cache لإخفاء استعلام سيئ قبل إصلاح الاستعلام نفسه.
٢٩. هل يجب تخزين نتائج الذكاء الاصطناعي؟
قد تكون استدعاءات النماذج مكلفة وبطيئة، ولذلك قد يفيد تخزين بعض النتائج. مثلًا:
- تلخيص محتوى ثابت.
- Embedding لملف لم يتغير.
- تصنيف نص مطابق.
- تحليل تقرير ثابت.
لكن لا تستخدم مفتاحًا عامًا لمحتوى شخصي. يجب أن يحتوي المفتاح، بحسب الحالة، على:
- المستخدم أو Tenant.
- نسخة النموذج.
- نسخة الـPrompt.
- إعدادات التوليد.
- Hash المحتوى.
- اللغة.
- صلاحية الوصول.
- نسخة Schema المطلوبة.
وإذا تغير Prompt أو النموذج أو قواعد المشروع، فقد تصبح النتيجة المخزنة غير صالحة حتى لو لم يتغير نص المستخدم. ولا ينبغي تخزين بيانات حساسة لدى طرف أو طبقة لا تتوافق مع سياسة الخصوصية.
٣٠. الضغط وحجم JSON
حتى بعد اختيار الحقول المطلوبة، قد تكون الاستجابة كبيرة. يمكن تحسين ذلك عبر:
- ضغط HTTP.
- Pagination.
- عدم تكرار البيانات داخل كل عنصر.
- استخدام أسماء عقود واضحة دون تضخيم غير ضروري.
- عدم تضمين ملفات أو صور Base64 داخل JSON إلا عند حاجة حقيقية.
- إرسال الصور عبر تخزين ملفات أو CDN.
- استخدام أحجام صور مناسبة.
- عدم إعادة علاقات متداخلة بلا حدود.
لكن لا تجعل "تصغير 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 مشكلات كبيرة:
- اختيار الـShard.
- نقل البيانات بين Shards.
- الاستعلامات العابرة.
- المعاملات الموزعة.
- النسخ الاحتياطي.
- التوازن.
- تغيير مفتاح التقسيم.
- مراقبة عدة قواعد.
لذلك لا ينبغي أن يكون أول حل عند بطء استعلام. إرشادات PostgreSQL تفرق بين Partitioning داخل الجداول وتقسيم البيانات بين خوادم في حلول Data Partitioning موزعة.
٣٤. لا تبدأ بـSharding
قبل التفكير في Sharding، افحص:
- هل تصميم الجداول صحيح؟
- هل الاستعلامات مقيدة؟
- هل توجد فهارس مناسبة؟
- هل يوجد N+1؟
- هل Pagination مطبقة؟
- هل الاتصالات مضبوطة؟
- هل التقارير المكلفة منفصلة؟
- هل يمكن استخدام Partitioning؟
- هل القراءات هي المشكلة ويمكن استخدام Replica؟
- هل Cache مناسب؟
- هل المشكلة مثبتة بقياسات؟
قد تبني Sharding معقدًا لتطبيق لا يحتاج إليه، بينما كان فهرس مركب واحد كافيًا.
٣٥. Read Replicas ليست مصدرًا فوريًا دائمًا
عندما تزداد القراءات، قد تستخدم نسخة للقراءة. لكن النسخة قد تتأخر عن الخادم الأساسي.
قد يحدث السيناريو التالي:
- يحدّث المستخدم اسمه.
- يقرأ التطبيق الملف من Replica.
- تظهر القيمة القديمة لثوانٍ.
يسمى ذلك أحيانًا Replication Lag. لذلك تحتاج إلى تحديد العمليات التي تتطلب Read-After-Write Consistency، وقد تقرأها من الخادم الأساسي مؤقتًا.
لا ترسل كل القراءات تلقائيًا إلى Replica دون فهم أثر التأخير.
٣٦. Vector Database ليست بديلًا لقاعدة البيانات العلائقية
مع انتشار تطبيقات الذكاء الاصطناعي، قد يقترح المساعد إضافة Vector Database منذ البداية.
تستخدم قواعد المتجهات لتخزين واسترجاع Embeddings بناءً على التشابه، وهو مفيد في حالات مثل: البحث الدلالي، RAG، العثور على نصوص متشابهة، توصية المحتوى، ومقارنة الصور أو المستندات عند توفر تمثيل مناسب.
لكنها ليست بديلًا طبيعيًا عن الجداول والعلاقات والمعاملات. لا تستخدم Vector Database لتحديد:
- رصيد المستخدم.
- حالة الدفع.
- ملكية الطلب.
- صلاحيات الحساب.
- عدد الوحدات في المخزون.
- ما إذا كانت الفاتورة مدفوعة.
قد يحتوي النظام على PostgreSQL للبيانات التشغيلية وVector Index للبحث الدلالي، لكن لكل منهما وظيفة مختلفة.
٣٧. Row-Level Security لا تعالج التصميم السيئ
ناقشنا RLS في الجزء الأول بوصفها طبقة لحماية الصفوف. في PostgreSQL، يمكن إنشاء سياسات تحدد الصفوف التي يسمح للدور بقراءتها أو تعديلها، وتطبق السياسات على مستوى كل جدول.
لكن RLS لا تصلح:
- جدولًا بلا علاقات واضحة.
- حالات نصية غير منضبطة.
- فهارس مفقودة.
- استعلام N+1.
- عملية مالية غير ذرية.
- Cache يعرض بيانات قديمة.
- استجابة API ضخمة.
الأمن والتصميم والأداء طبقات متكاملة، وليست ميزة واحدة تحل كل المشكلات. كما يجب اختبار السياسات بأدوار المستخدمين الفعلية؛ لأن بعض الأدوار الإدارية أو مالك الجدول أو الأدوار التي تملك BYPASSRLS قد تتصرف بطريقة مختلفة.
٣٨. علامات قاعدة البيانات الهشة في مشروع Vibe Coding
راجع مشروعك إذا وجدت:
- كل الأعمدة من نوع Text.
- معظم البيانات داخل JSON واحد.
- غياب Foreign Keys.
- غياب Unique Constraints.
- حالات العمل Strings مفتوحة بلا قيود.
- مبالغ مالية مخزنة بطريقة غير واضحة.
- استخدام
SELECT *في كل مكان. - تحميل جميع الصفوف دون Pagination.
- استعلامات داخل Loops.
- فلترة عشرات الآلاف من الصفوف في الواجهة.
- حساب الأسعار في العميل.
- عمليات متعددة دون Transaction.
- قراءة ثم كتابة دون حماية من Race Conditions.
- عمليات حساسة بلا Idempotency.
- اتصال جديد لكل طلب.
- Pool أكبر من قدرة قاعدة البيانات.
- Cache بلا سياسة Invalidation.
- مفاتيح Cache لا تحتوي Tenant أو User.
- التفكير في Sharding قبل قياس الاستعلامات.
- عدم وجود تسجيل للاستعلامات البطيئة.
- عدم تشغيل
EXPLAIN. - عدم اختبار البيانات بأحجام واقعية.
٣٩. كيف توجه الذكاء الاصطناعي لتصميم قاعدة البيانات؟
لا تقل فقط:
صمّم قاعدة بيانات احترافية وقابلة للتوسع.
قد تحصل على عشرات الجداول والتقنيات دون فهم. استخدم أمرًا أكثر تحديدًا:
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.
٤٢. قائمة اختبار لقواعد البيانات والأداء
قبل اعتبار الميزة جاهزة، اطلب الإجابة عن هذه الأسئلة بالأدلة:
تصميم البيانات
- هل لكل جدول مسؤولية واضحة؟
- هل العلاقات ممثلة بـForeign Keys؟
- هل القيم الإلزامية محددة بـ
NOT NULL؟ - هل القيم الفريدة محمية بـUnique Constraint؟
- هل حالات العمل مقيدة بقيم مسموحة؟
- هل المبالغ والتواريخ والأنواع ممثلة بطريقة صحيحة؟
- هل توجد قواعد لا تعتمد على الواجهة وحدها؟
- هل حذف السجلات له سلوك واضح؟
- هل بيانات المستخدمين وTenants معزولة؟
الاستعلامات
- كم استعلامًا تنفذ الشاشة؟
- هل توجد مشكلة N+1؟
- هل تعاد حقول أو علاقات غير مستخدمة؟
- هل جميع القوائم الكبيرة تستخدم Pagination؟
- هل البحث والفلترة يحدثان في قاعدة البيانات؟
- هل الاستعلامات المهمة لها فهارس مناسبة؟
- هل تم فحص خطة التنفيذ؟
- هل اختُبرت الميزة بحجم بيانات واقعي؟
التزامن
- ماذا يحدث عند وصول طلبين في الوقت نفسه؟
- هل العملية الحساسة ذرية؟
- هل يمكن أن يصبح الرصيد أو المخزون غير صحيح؟
- هل توجد احتمالات Deadlock؟
- هل ترتيب الأقفال ثابت؟
- هل توجد Idempotency للطلبات القابلة للتكرار؟
- هل يمكن معالجة Webhook مرتين؟
- هل إعادة المحاولة آمنة؟
الاتصالات
- كم اتصالًا تفتح كل نسخة من التطبيق؟
- هل يوجد Connection Pool؟
- هل حجم Pool متوافق مع عدد النسخ وحد قاعدة البيانات؟
- هل تغلق الاتصالات بصورة صحيحة؟
- هل توجد معاملات تبقى Idle؟
- ماذا يحدث عند امتلاء الاتصالات؟
- هل Serverless Scaling قد يضاعف عددها؟
التخزين المؤقت
- ما البيانات التي يجري تخزينها؟
- ما Source of Truth؟
- ما مدة TTL؟
- كيف يحدث Invalidation؟
- هل المفاتيح تفصل المستخدمين وTenants؟
- هل توجد بيانات شخصية في Shared Cache؟
- ماذا يحدث إذا تعطل Cache؟
- كيف تمنع Cache Stampede؟
- ما أقصى مدة مسموحة لعرض بيانات قديمة؟
API والحمولة
- هل تعيد الاستجابة الحقول المطلوبة فقط؟
- ما حجم Payload؟
- هل JSON مضغوط عند الحاجة؟
- هل الصور تقدم بالحجم المناسب؟
- هل العلاقات المتداخلة محدودة؟
- هل الواجهة تحمل بيانات لن تعرضها؟
- هل 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، أو بنية موزعة ضخمة.
لكنك تحتاج منذ البداية إلى:
- أنواع بيانات صحيحة.
- علاقات واضحة.
- قيود تمنع البيانات غير الصحيحة.
- فصل المستخدمين وTenants.
- عمليات ذرية.
- عقود API محددة.
- Pagination للقوائم القابلة للنمو.
- معرفة الاستعلامات التي تنفذها الميزة.
- طريقة آمنة للتعامل مع الطلبات المتزامنة.
- تصميم لا يمنعك من الإصلاح لاحقًا.
الذكاء الاصطناعي قد يبني لك جدولًا خلال دقائق. وقد يكتب ORM Models وMigrations وRepositories واستعلامات وربطًا كاملًا بالواجهة.
لكن مسؤوليته الافتراضية غالبًا هي:
اجعل البيانات تظهر.
أما مسؤوليتك بصفتك مطورًا فهي أن تسأل:
هل تمثل هذه الجداول معنى المنتج؟
هل تستطيع قاعدة البيانات رفض الحالة الخاطئة؟
كم استعلامًا تنفذ هذه الشاشة؟
ماذا يحدث عند وصول طلبين معًا؟
هل يمكن تنفيذ العملية مرتين؟
هل ستنفد الاتصالات؟
هل Cache يعرض بيانات قديمة؟
وهل أملك قياسات تثبت أن الحل أصبح أفضل؟
فالتطبيق الذي يعمل مع خمسة مستخدمين قد لا يعمل مع خمسة آلاف. والاستعلام الذي يستغرق عشرين ملي ثانية على مئة سجل قد يصبح كارثة على ملايين السجلات. والعملية المالية التي تنجح في الاختبار اليدوي قد تفشل عند التزامن. والـCache الذي يسرّع الصفحة قد يعرض رصيدًا أو صلاحية قديمة.
لذلك لا تنظر إلى قاعدة البيانات بوصفها مخزنًا يقع خلف التطبيق. انظر إليها بوصفها قلب المنتج: تحفظ حقيقته، وتحمي اتساقه، وتحدد قدرته على الاستمرار.
٤٥. في الجزء الثالث
بعد أن انتهينا في الجزء الأول من الأمن السيبراني، وناقشنا في هذا الجزء قواعد البيانات والأداء والتزامن والاتصالات والتخزين المؤقت، سننتقل في الجزء الثالث إلى مستوى أكبر:
ماذا يحدث عندما تتعطل خدمة خارجية، أو يمتلئ Queue، أو يسقط خادم، أو يفشل النشر، أو تحتاج إلى التراجع عن إصدار؟
سنتناول:
- Load Balancing.
- Horizontal Scaling.
- Auto Scaling.
- المعالجة غير المتزامنة.
- Message Queues.
- Queue Overflow.
- Dead Letter Queues.
- Heartbeats.
- Graceful Fallback.
- Circuit Breakers.
- مراقبة Logs وMetrics وTraces.
- خطط Rollback.
- Site Reliability Engineering.
- وتجربة المستخدم عندما يفشل جزء من النظام.
فالمنتج الحقيقي ليس الذي لا يتعطل أبدًا. بل الذي يعرف كيف يكتشف التعطل، ويحد من أثره، ويتعافى منه دون أن يفقد بيانات المستخدمين أو يتركهم أمام شاشة لا تخبرهم بما يحدث.
- PostgreSQL — Indexes
- PostgreSQL — Using EXPLAIN
- PostgreSQL — Concurrency Control (MVCC)
- PostgreSQL — Connection Settings (max_connections)
- PostgreSQL — Table Partitioning
- PostgreSQL — Row Security Policies
- PgBouncer — Features
- Prisma — Relation Queries (N+1)
- MDN — HTTP Caching
- Stripe — Idempotent Requests