JSONB: أفضل طريقة لتدمير قاعدة بياناتك
JSONB قوي ومفيد، ومن السهل جدًا إساءة استخدامه عندما تترك blob يصبح مخططك الفعلي.
أضافت PostgreSQL JSONB لتتيح لك تخزين بيانات شبه منظمة دون تحديد مخططات صارمة مسبقًا. كانت الفكرة سليمة: أحيانًا لا تعرف حقًا كيف ستبدو البيانات، أو تتغير بشكل متكرر جدًا بحيث لا تجعل الأعمدة التقليدية منطقية.
هذا مهم لأن JSONB ليس خطأ. في العديد من الأنظمة، هو أنقى تمثيل لمساحة المشكلة. إذا كنت تخزن حمولات webhook من طرف ثالث، أو نصوص أحداث ذات إصدارات، أو أعلام ميزات، أو كائنات تكوين LLM حيث كل مزود ونموذج يعرض مجموعة خيارات مختلفة قليلاً ومتغيرة باستمرار، فإن إجبار كل شيء على أعمدة من الدرجة الأولى قد يكون أكثر إحراجًا من كونه مفيدًا.
المشكلة هي أن JSONB هو أيضًا أسهل طريقة لتأجيل قرارات المخطط دون الاعتراف بأنك تؤجلها. في مكان ما بين النية والتنفيذ، أصبح المكافئ في قواعد البيانات لعبارة “سأنظف غرفتي لاحقًا”. ذلك الحل المؤقت الذي لجأت إليه قبل ستة أشهر؟ لا يزال موجودًا، والآن يعتمد عليه الإنتاج.
أستمر في رؤية نفس النمط. يضيف فريق عمود JSONB لأنهم غير متأكدين من المتطلبات. يعدون أنفسهم بأنهم سيطبعونه بمجرد أن تستقر الأمور. بعد ثلاث سنوات، يحتوي ذلك العمود على أربعين نسخة مختلفة مما كان من المفترض أن يكون ملف تعريف مستخدم، يتم الاستعلام عنه بواسطة خمسة عشر خدمة، كل منها يضع افتراضات مختلفة حول ما بداخله.
الدين الفني ليس JSONB نفسه. إنه الفجوة بين ما أخبرت نفسك أنك تبنيه وما بنيته بالفعل: نظام مخطط عند القراءة غير موثق.
ما يحدث عادةً
أنت تضيف ميزة ولست متأكدًا مما إذا كان المستخدمون بحاجة إلى twitter_handle أو bluesky_handle أو شيء آخر تمامًا. بدلاً من التفكير في المخطط، تفعل هذا:
CREATE TABLE users ( id SERIAL PRIMARY KEY, profile JSONB);يعمل. تشحن الميزة، تنتقل إلى التالية، ثم التالية. ينمو عمود JSONB بهدوء في الخلفية.
هذا هو مفترق الطرق. إذا بقي profile كتلة غير شفافة يتم جلبها بواسطة user.id، فأنت على الأرجح بخير. إذا بدأ يصبح المكان الأساسي الذي تعيش فيه بيانات الأعمال، فإن المقايضات تتغير بسرعة.
يسأل المنتج: “كم عدد المستخدمين في نيويورك؟”
تكتب:
SELECT count(*) FROM users WHERE profile->>'location' = 'New York';يقوم Postgres بمسح الجدول بالكامل. كل صف على حدة.
لذا تضيف فهرس GIN. قد يكون ذلك مقبولًا في بعض الأحيان. لكنك الآن تدفع ثمن تعقيد وتخزين حقيقيين لأن حقلًا يتصرف كبيانات علائقية من الدرجة الأولى لم يصبح عمودًا من الدرجة الأولى.
السنة الأولى: انحراف المخطط
لديك ثلاثة إصدارات من البيانات في نفس العمود.
- الصف 1:
{"city": "NYC"} - الصف 1000:
{"location": "NYC"} - الصف 5000:
{"address": {"city": "New York"}}
يبدو كود تطبيقك الآن هكذا:
const city = user.location || user.city || user.address?.city || "Unknown";لم تقم بإزالة المخطط. لقد نقلت فقط التحقق من الصحة وفحوصات الاتساق من قاعدة البيانات إلى كود تطبيق متناثر.
متى تستخدم JSONB فعليًا
لدى JSONB حالات استخدام صالحة. في كثير من الأحيان يكون مقبولًا تمامًا، وأحيانًا يكون الخيار الأفضل المتاح.
الفرق الحاسم ليس “المنظم جيد، JSON سيء.” بل هو أقرب إلى هذا:
- هل يتم جلب البيانات في الغالب ككل بواسطة مفتاح أساسي ثابت؟
- هل تختلف المفاتيح بشكل جوهري عبر المزوّدين، أو الإصدارات، أو المستأجرين، أو الزمن؟
- هل تستعلم عن عدد قليل من الحقول المعروفة، أم تبتكر استعلامات مسار جديدة كل سباق؟
- هل يمتلك التطبيق التحكم في الإصدار والتحقق بشكل مقصود، أم أنه مجرد ارتجال؟
حالات استخدام مشروعة لـ JSONB
-
حمولات Webhook: تستقبل بيانات من Stripe أو Slack أو GitHub. ليس لديك أي سيطرة على المخطط. قد لا تستعلم عنها أبدًا. تحتاج فقط إلى تخزينها لأغراض التصحيح أو إعادة التشغيل. مثالي لـ JSONB.
-
السجلات وتدفقات الأحداث: سجلات التطبيق، مسارات التدقيق، سياقات الأخطاء. هذه عمليات كتابة كثيفة، نادرًا ما يتم الاستعلام عنها بحقول محددة، وغالبًا ما تُحلّل بكميات كبيرة أو تُصدّر إلى منصات تحليل. JSONB مناسب هنا.
-
تفضيلات المستخدم والإعدادات: كائنات إعدادات تحتوي على أكثر من 100 علامة منطقية، معظمها خاطئ، وتجلب دائمًا الكتلة بأكملها بواسطة معرف المستخدم. لا تقوم بتشغيل
WHERE preferences->>'theme' = 'dark'. JSONB يعمل. -
تكوين مزوّد/نموذج LLM: هذا أحد أوضح الأمثلة الحديثة. OpenAI وAnthropic وGemini والنماذج المحلية مفتوحة الوزن والبوابات الخاصة بالمزوّدين جميعها تعرض معاملات متداخلة ولكن مختلفة. حتى داخل مزوّد واحد، تتطور قدرات النموذج وأسماء الخيارات. غالبًا ما تكون كتلة تكوين JSONB أكثر صدقًا من التظاهر بأن
temperatureوtop_pوreasoning_effortوjson_schemaوtool_choiceوعشرون مفتاحًا آخر يجب أن تكون جميعها أعمدة عالمية. JSONB غالبًا ما يكون التجريد الصحيح هنا. -
تخزين استجابات API مؤقتًا: تخزين استجابات API كاملة. قاعدة البيانات هي مجرد Redis أسرع. تجلب بواسطة مفتاح التخزين المؤقت، ولا تستعلم أبدًا عن الخصائص المتداخلة. JSONB مناسب.
-
تسجيل الأحداث (Event Sourcing): تخزين حمولات أحداث غير قابلة للتغيير. استعلاماتك دائمًا هي “أعطني كل الأحداث للمجمّع X” مرتبة حسب الوقت. لا تقوم أبدًا بتشغيل جمل
WHEREعلى خصائص الحدث. JSONB يناسب. -
أسطح التوسعة (Extensibility Surfaces): التكاملات، إعدادات الإضافات، تجاوزات كل مستأجر، بيانات السوق، قدرات المزود، أو حقول “إضافات” حيث تتوقع صراحةً أن يختلف الشكل حسب النوع الفرعي. يمكن أن يكون JSONB العقد الصحيح، وليس حلاً وسطاً.
قاعدة عامة: إذا كان التطبيق يجلب المستند بواسطة مفتاح معروف ويفهم كيفية التحقق من صحته/إصداره، يمكن أن يكون JSONB ممتازاً. إذا استمرت الشركة في طرح أسئلة علائقية حول المفاتيح المتداخلة، فإن تلك الحقول تحاول أن تصبح أعمدة.
أفضل نمط غالباً ما يكون هجيناً
العديد من الأنظمة الناضجة تصل إلى هذا:
CREATE TABLE llm_requests ( id UUID PRIMARY KEY, provider TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), config JSONB NOT NULL);هذا عادةً أفضل من أي من الطرفين.
providerوmodelوstatusوcreated_atهي أعمدة من الدرجة الأولى لأنك ستصفي، وتضم، وتجمع، وتفهرس عليها.configيبقى JSONB لأن سطح الخيارات الدقيق خاص بالنموذج، خاص بالمزود، ومن المرجح أن يتطور.
هذا ليس “فشلاً في التطبيع.” هذا هو رسم الخط في المكان الصحيح.
على نطاق واسع: إصدار الكائنات > التطبيع
هنا يصبح الأمر مثيراً للاهتمام. على نطاق واسع بما يكفي، الحل “الصحيح” ليس التطبيع—بل إصدار الكائنات.
إذا كان لديك مليارات الصفوف وتطور متكرر للمخطط، يصبح ترحيل الأعمدة مكلفاً. شركات مثل Stripe و GitHub و Netflix لا تطبع كل شيء. بدلاً من ذلك:
CREATE TABLE entities ( id UUID PRIMARY KEY, version INT NOT NULL, data JSONB NOT NULL);تطبيقك يعرف كيف يقرأ version: 1 و version: 2 و version: 3. لا حاجة لترحيلات قاعدة البيانات للحقول الجديدة. الكود يتولى التوافق مع الإصدارات السابقة.
هذا قرار معماري، ليس كسلاً. إنه يقايض تعقيد قاعدة البيانات بتعقيد التطبيق. أحياناً تكون هذه المقايضة صحيحة تماماً، خاصةً عندما تكون الوثيقة ذات إصدارات طبيعية ويكون التطبيق هو المفسّر المعياري.
نمط الفشل ليس “استخدام JSONB.” نمط الفشل هو استخدام JSONB دون إصدارات، أو تحقق، أو قواعد ترقية، أو حدود واضحة بين بيانات الوثيقة والبيانات العلائقية.
الأسئلة التي تهم حقاً
قبل إضافة عمود JSONB، اسأل:
- هل سنستعلم عن الحقول المتداخلة في
WHEREأوJOINأوGROUP BYأوORDER BYبانتظام؟ - هل نتحكم في هذا المخطط، أم أنه مُعرّف خارجياً ومتقلب؟
- هل الشكل غير متجانس عمداً عبر السجلات؟
- هل لدينا تحقق وإصدارات على مستوى التطبيق؟
- أي الحقول من المرجح أن تصبح أبعاداً تشغيلية لاحقاً؟
إذا كانت الإجابة على #1 هي “نعم، باستمرار”، فهذه إشارة قوية لاستخدام الأعمدة.
إذا كانت الإجابات على #2 و #3 هي “نعم”، فمن المحتمل أن JSONB يقوم بعمل حقيقي لك.
الخروج من الفخ
إذا كنت بالفعل في هذه الحفرة، توقف عن الحفر.
- التدقيق: شغّل
jsonb_object_keysوافحص الانحراف الفعلي في الشكل، وليس الشكل الذي تفترض وجوده. - الترقية: حدد الحقول التي تقوم بالتصفية أو الربط أو الفرز أو إعداد التقارير بناءً عليها بشكل متكرر. اجعلها أعمدة حقيقية.
- التحقق: أضف تحققًا على مستوى التطبيق أو قاعدة البيانات لأي شيء يبقى في JSONB.
- الإصدار: إذا كان الكائن الثنائي الكبير (blob) يمثل بيانات نطاق حقيقية، فقم بإصداره بشكل صريح.
- التقليم: أزل المفاتيح المكررة من الكائن الثنائي الكبير بمجرد إنشاء الأعمدة المرفوعة.
لا تخبر نفسك أنه يجب تطبيع كل كائن ثنائي كبير. ولا تخبر نفسك أيضًا أن كائنًا ثنائيًا كبيرًا يحمل دلالات أعمال دائمة هو “مؤقت”.
JSONB رائع عندما تكون الوثيقة ذات شكل وثائقي حقيقي. إنه خطير عندما يكون مخططًا علائقيًا يرتدي شاربًا مزيفًا.
الموارد
- توثيق PostgreSQL JSONB
- استراتيجيات فهرسة JSONB
- متى تستخدم JSONB مقابل الأعمدة العلائقية
- أفضل ممارسات تصميم مخطط PostgreSQL