يا جماعة الخير، السلام عليكم ورحمة الله. معكم أخوكم أبو عمر.
قبل كم سنة، كنا شغالين على نظام إدارة محتوى (CMS) لعميل عنده متجر إلكتروني. المشروع كان ماشي زي الحلاوة، لحد ما وصلنا لمرحلة “خصائص المنتجات”. هون بلشت القصة تتعقد. العميل كان يبيع كل إشي ممكن تتخيلوه: ملابس، إلكترونيات، أدوات مطبخ… الله وكيلكم، كأنه سوق الجمعة بس على الإنترنت.
المشكلة وين؟ القميص إله خاصية “مقاس” و”لون”. المايكروويف إله “قوة بالواط” و”سعة باللتر”. الخلاط إله “عدد شفرات” و”مادة الإبريق”. كيف بدنا نخزن كل هالتنوع في قاعدة بيانات PostgreSQL علائقية ومحترمة؟
قعدنا في اجتماع، والشباب بلشوا يقترحوا. واحد قلك: “بنعمل جدول لكل نوع منتج”. قلتله: “يا زلمة، العميل كل يوم بضيف فئة جديدة، بدك نضل نعدّل في قاعدة البيانات لآخر العمر؟”. واحد ثاني، الله يستر عليه، قلك: “بنعمل جدول خصائص عملاق فيه 50 عامود (property1, property2, … property50) وبنعبّي اللي بنحتاجه”. بصراحة، وقتها حسيت إنه لازم أرمي حالي من الشباك. تخيل كمية الـ NULLs اللي راح تكون في الجدول هاد! شغلانة بتجلط وعالفاضي.
بالآخر، رسينا على الحل “الكلاسيكي” المُرّ: نموذج الـ EAV (Entity-Attribute-Value). عملنا جدول اسمه product_attributes فيه product_id و attribute_name و attribute_value. نظرياً، الحل سحري ومرن. عملياً؟ كان كابوس. الاستعلام عن منتج واحد بخصائصه كان يحتاج JOINs معقدة، والـ Performance كان في الحضيض. كنا حرفياً نمزق كائن الـ JSON اللي جاي من الـ Front-end إرباً، ونوزعه على أسطر متعددة في جدول، بس عشان نرجع نجمعه بطلوع الروح. حسينا حالنا بنخربش وبنعيد تجميع بازل مع كل طلب HTTP. الوضع كان لا يُطاق.
لماذا تفشل الطرق التقليدية مع البيانات شبه المهيكلة؟
قصتنا هاي بتسلط الضوء على مشكلة جوهرية. قواعد البيانات العلائقية، مثل PostgreSQL، مصممة للتأكد من تكامل البيانات واتساقها عبر بنية (Schema) صارمة ومحددة مسبقاً. هذا إشي ممتاز للبيانات المهيكلة (Structured Data) زي معلومات المستخدمين (اسم، إيميل، باسورد). لكن لما نتعامل مع بيانات شبه مهيكلة (Semi-structured Data) مثل خصائص المنتجات، إعدادات المستخدم، أو سجلات الأحداث (logs)، هاي الصرامة بتصير عائق.
الطريقة الأولى: حقل نصي (TEXT)
أبسط حل (وأسوأهم) هو تخزين الـ JSON كنص عادي في عامود من نوع TEXT أو VARCHAR. ليش هاد الحل سيء؟
- لا يوجد تحقق (Validation): قاعدة البيانات ما بتعرف إذا كان النص اللي خزنته هو JSON صالح ولا مجرد “خربشة”.
- استعلام كارثي: بدك تبحث عن كل المنتجات اللي لونها “أحمر”؟ بدك تستخدم
LIKE '%"color":"red"%'. هاد بطيء جداً، غير دقيق، وما بستفيد من أي فهرسة (Indexing). - التعديل شبه مستحيل: لتغيير قيمة واحدة داخل الـ JSON، لازم تقرأ النص كله، تعدله في كود التطبيق، وترجع تكتبه كله من جديد.
الطريقة الثانية: جحيم الـ EAV (Entity-Attribute-Value)
هذا النموذج اللي استخدمناه في قصتنا. الفكرة هي تفكيك الكائن إلى أجزاء صغيرة.
-- هيك كان شكل الجدول التعيس
CREATE TABLE product_attributes (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id),
attribute_name VARCHAR(255),
attribute_value VARCHAR(255)
);
المشاكل اللي واجهناها مع هاد النموذج:
- استعلامات معقدة: عشان تجيب منتج واحد مع كل خصائصه، بدك تعمل
JOINلكل خاصية، أو تستخدمGROUP_CONCATمع تعقيدات إضافية. - أداء سيء: مع نمو البيانات، الـ JOINs بتصير مكلفة جداً.
- مشاكل أنواع البيانات: كل القيم تُخزن كنصوص (VARCHAR). لو بدك تبحث عن سعر بين قيمتين، لازم تحول النص لأرقام (casting) في كل مرة، وهذا بطيء جداً.
هون إحنا كنا بنطبع إشي مش المفروض إنه يتطبّع. كنا بنفرض هيكلية صارمة على بيانات طبيعتها مرنة ومتغيرة. كنا بنسبح ضد التيار.
المنقذ: أنواع بيانات JSON في PostgreSQL
في خضم معاناتنا، ومع صدور نسخ أحدث من PostgreSQL (تحديداً من 9.2 وطالع)، ظهر الضوء في آخر النفق. قدمت PostgreSQL دعماً أصيلاً لبيانات JSON. وهون لازم نميز بين نوعين مهمين جداً.
الفرق بين JSON و JSONB: مش مجرد حرف!
كتير ناس بخلطوا بينهم، بس الفرق جوهري ومهم جداً تفهمه يا صاحبي.
JSON: هذا النوع بخزن النص اللي بتبعته زي ما هو بالزبط، بكل مسافاته البيضاء وترتيب مفاتيحه. لما تعمل INSERT، هو بس بتأكد إنه JSON صالح. سريع في الإدخال، لكنه أبطأ في الاستعلام لأنه بحتاج يحلل (parse) النص في كل مرة بدك تبحث جواته.JSONB(Binary JSON): هاد هو البطل الحقيقي. لما تعمل INSERT، PostgreSQL بتاخد الـ JSON تبعك، بتحلله، بتشيل المسافات الزائدة، بتعيد ترتيب المفاتيح، وبتخزنه بصيغة ثنائية (Binary) محسّنة جداً للاستعلام. الإدخال أبطأ بشعرة (لأنه في معالجة)، لكن الاستعلام والبحث أسرع بأضعاف مضاعفة.
نصيحة من أبو عمر: القاعدة بسيطة. هل تحتاج تحافظ على شكل الـ JSON الأصلي بكل تفاصيله (ترتيب المفاتيح، المسافات)؟ 99% من الحالات جوابك هو “لأ”. إذن، دائماً استخدم
JSONBإلا إذا عندك سبب قوي جداً جداً لغير ذلك.
كيف نستخدم JSONB عملياً؟
خلونا نرجع لمثالنا تبع خصائص المنتجات، ونبنيه بالطريقة الصح.
1. تصميم الجدول
بدل كل التعقيدات اللي فاتت، الآن جدولنا بصير بسيط وأنيق:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price NUMERIC(10, 2) NOT NULL,
attributes JSONB -- كل السحر هنا
);
2. إضافة البيانات
عملية الإضافة صارت منطقية جداً. بنمرر كائن JSON كامل زي ما هو.
INSERT INTO products (name, price, attributes) VALUES
('قميص قطن أزرق', 19.99, '{"color": "blue", "size": "L", "material": "cotton"}'),
('مايكروويف 1100 واط', 89.50, '{"brand": "SuperHeat", "power_watt": 1100, "features": ["defrost", "grill"] }'),
('لابتوب للمطورين', 1200.00, '{"cpu": "Core i7", "ram_gb": 16, "storage": {"type": "SSD", "size_gb": 512}}');
لاحظوا جمال الموضوع. كل منتج عنده خصائصه المختلفة، وكلها في نفس العامود بدون أي مشاكل.
3. الاستعلام عن البيانات (هنا يبدأ السحر)
PostgreSQL بتوفر مجموعة من المشغلات (Operators) الرهيبة للتعامل مع JSONB.
الوصول إلى الحقول (Operators: -> و ->>)
->: يرجع قيمة الحقل كـ JSON.->>: يرجع قيمة الحقل كنص (TEXT). هذا هو اللي بنستخدمه غالباً في جملة WHERE.
-- جيبلي اسم المنتج اللي لونه أزرق
SELECT name FROM products WHERE attributes->>'color' = 'blue';
-- Result: 'قميص قطن أزرق'
-- جيبلي قوة المايكروويف بالواط
SELECT attributes->'power_watt' FROM products WHERE name LIKE '%مايكروويف%';
-- Result: 1100 (لاحظ إنه رجع كـ JSON/number وليس كنص)
الوصول إلى الحقول المتداخلة (Operators: #> و #>>)
ماذا عن البيانات المتداخلة (nested) مثل مواصفات التخزين في اللابتوب؟
-- جيبلي نوع التخزين للابتوب
SELECT attributes#>>'{storage,type}' FROM products WHERE name LIKE '%لابتوب%';
-- Result: 'SSD'
البحث والتحقق من الاحتواء (Operator: @>)
هذا المشغل هو قوتي الخارقة المفضلة. هو بفحص إذا كان الـ JSON في العامود يحتوي على الـ JSON اللي بتمرره في الاستعلام. إشي فخم!
-- جيبلي كل المنتجات اللي حجمها "L"
SELECT name FROM products WHERE attributes @> '{"size": "L"}';
-- Result: 'قميص قطن أزرق'
-- جيبلي كل المنتجات اللي قوتها 1100 واط
SELECT name FROM products WHERE attributes @> '{"power_watt": 1100}';
-- Result: 'مايكروويف 1100 واط'
-- جيبلي كل المنتجات اللي فيها ميزة الشوي "grill" (داخل مصفوفة)
SELECT name FROM products WHERE attributes @> '{"features": ["grill"]}';
-- Result: 'مايكروويف 1100 واط'
4. الفهرسة (Indexing) لسرعة البرق ⚡
كل اللي فات عظيم، لكن بدون فهرسة، البحث راح يصير بطيء مع ملايين السجلات. لحسن الحظ، PostgreSQL تسمح لنا بفهرسة أعمدة الـ JSONB باستخدام فهرس GIN (Generalized Inverted Index).
الفهرس العادي (B-Tree) ما بيعرف يتعامل مع بنية الـ JSONB المعقدة. فهرس GIN مصمم خصيصاً لفهرسة أنواع البيانات المركبة اللي بتحتوي على عناصر متعددة (مثل المصفوفات والـ JSONB).
-- إنشاء فهرس GIN على كامل عامود الـ attributes
CREATE INDEX idx_products_attributes_gin ON products USING GIN (attributes);
بمجرد إنشاء هذا الفهرس، كل استعلامات الاحتواء (@>) والوجود (?, ?|, ?&) راح تصير سريعة جداً جداً. هذا هو ما يفصل بين الحل الهاوي والحل الاحترافي الجاهز للإنتاج.
الخلاصة: متى نستخدم JSONB؟
بعد كل هالكلام، هل لازم نرمي كل مبادئ قواعد البيانات العلائقية ونستخدم JSONB لكل إشي؟ طبعاً لأ. الحكمة هي “استخدم الأداة الصح للشغلانة الصح”.
✅ استخدم JSONB في هذه الحالات:
- بيانات شبه مهيكلة لا تتبع نمطاً ثابتاً (مثل خصائص المنتجات، إعدادات، بيانات من APIs خارجية).
- عندما تحتاج مرونة في تطوير الـ Schema بدون الحاجة لعمل
ALTER TABLEكل يوم. - لتخزين سجلات (logs) أو أحداث (events) لها بنية متغيرة.
❌ تجنب استخدام JSONB في هذه الحالات:
- لبيانات جوهرية وعلائقية (مثل IDs، المفاتيح الأجنبية، العلاقات بين الجداول الرئيسية).
- لبيانات تحتاج إلى قيود صارمة (Constraints) على مستوى قاعدة البيانات.
- عندما تكون كل سجلاتك تتبع نفس البنية 100%، هنا الأعمدة التقليدية أفضل وأكثر كفاءة.
في النهاية، JSONB في PostgreSQL أعطانا أفضل ما في العالمين: مرونة قواعد بيانات NoSQL مع قوة وموثوقية وتكامل قواعد البيانات العلائقية. تعلمنا الدرس بالطريقة الصعبة، بس الحمد لله، خرجنا منه بخبرة ثمينة. ما عدنا نمزق الـ JSON، صرنا نحتضنه ونستفيد من قوته كما هو. 😉
فيا صديقي المبرمج، المرة الجاي اللي بتقابلك فيها بيانات “مش راكبة صح” في قالب علائقي، تذكر قصة أبو عمر، وتذكر إن الحل ممكن يكون أبسط وأجمل بكثير مما تتخيل. الله يوفقكم جميعاً.