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

JSON الثابت مقابل الديناميكي

تتمثل المهمة الرئيسية عند تعريف مخطط لـ JSON في تحديد النوع المناسب لقيمة كل مفتاح. نوصي المستخدمين بتطبيق القواعد التالية بشكل متكرر على كل مفتاح ضمن التسلسل الهرمي لـ JSON لتحديد النوع المناسب لكل مفتاح.
  1. الأنواع البدائية - إذا كانت قيمة المفتاح من الأنواع البدائية، سواء كانت جزءًا من كائن فرعي أو موجودة في الجذر، فتأكد من اختيار نوعها وفقًا لـ أفضل ممارسات تصميم المخطط وقواعد تحسين الأنواع. ويمكن تمثيل مصفوفات الأنواع البدائية، مثل phone_numbers أدناه، على أنها Array(<type>)، مثل Array(String).
  2. الثابت مقابل الديناميكي - إذا كانت قيمة المفتاح كائنًا معقدًا، أي كائنًا أو مصفوفة من الكائنات، فحدّد ما إذا كانت قابلة للتغيّر. يمكن اعتبار الكائنات التي نادرًا ما تُضاف إليها مفاتيح جديدة، والتي يمكن توقّع إضافة مفتاح جديد إليها والتعامل معه عبر تعديل المخطط باستخدام ALTER TABLE ADD COLUMN، كائنات ثابتة. ويشمل ذلك الكائنات التي قد لا يظهر فيها سوى جزء من المفاتيح في بعض مستندات JSON. أما الكائنات التي تُضاف إليها مفاتيح جديدة بشكل متكرر و/أو يتعذر التنبؤ بها، فينبغي اعتبارها ديناميكية. والاستثناء هنا هو البُنى التي تحتوي على مئات أو آلاف المفاتيح الفرعية، إذ يمكن اعتبارها ديناميكية لأغراض التيسير.
ولتحديد ما إذا كانت القيمة ثابتة أم ديناميكية، راجع القسمين ذوي الصلة التعامل مع الكائنات الثابتة والتعامل مع الكائنات الديناميكية أدناه.

مهم: ينبغي تطبيق القواعد المذكورة أعلاه بشكل متكرر. إذا تبيّن أن قيمة المفتاح ديناميكية، فلا حاجة إلى مزيد من التقييم، ويمكن اتباع الإرشادات الواردة في التعامل مع الكائنات الديناميكية. أما إذا كان الكائن ثابتًا، فتابع تقييم المفاتيح الفرعية إلى أن تصبح قيم المفاتيح بدائية أو تُصادَف مفاتيح ديناميكية. ولتوضيح هذه القواعد، نستخدم مثال JSON التالي الذي يمثل شخصًا:
بتطبيق هذه القواعد:
  • يمكن تمثيل المفاتيح الجذرية name وusername وemail وwebsite بالنوع String. أما العمود phone_numbers فهو قيمة أولية من النوع Array(String)، بينما يكون نوعا dob وid هما Date وUInt32 على التوالي.
  • لن تُضاف مفاتيح جديدة إلى الكائن address (بل كائنات address جديدة فقط)، ولذلك يمكن اعتباره ثابتًا. وإذا تابعنا بشكل تكراري، فيمكن اعتبار جميع الأعمدة الفرعية قيمًا أولية (ومن النوع String) باستثناء geo. وهذا أيضًا هيكل ثابت يحتوي على عمودين من النوع Float32، هما lat وlon.
  • العمود tags ديناميكي. ونفترض أنه يمكن إضافة وسوم عشوائية جديدة إلى هذا الكائن بأي نوع أو بنية.
  • الكائن company ثابت وسيحتوي دائمًا على المفاتيح الثلاثة المحددة كحد أقصى. المفتاحان الفرعيان name وcatchPhrase من النوع String. أما المفتاح labels فهو ديناميكي. ونفترض أنه يمكن إضافة وسوم عشوائية جديدة إلى هذا الكائن. وستكون القيم دائمًا أزواج مفتاح-قيمة من النوع string.
يمكن اعتبار البنى التي تحتوي على مئات أو آلاف المفاتيح الثابتة ديناميكية، إذ نادرًا ما يكون من العملي تعريف الأعمدة الخاصة بها تعريفًا ثابتًا. ومع ذلك، حيثما أمكن، تخطَّ المسارات غير المطلوبة لتقليل كلٍّ من مساحة التخزين والعبء الإضافي للاستدلال.

التعامل مع البُنى الثابتة

نوصي بمعالجة البُنى الثابتة باستخدام Tuple مُسمّاة. ويمكن تمثيل مصفوفات الكائنات باستخدام مصفوفات من Tuple، أي Array(Tuple). وداخل قيم Tuple نفسها، ينبغي تعريف الأعمدة وأنواعها المقابلة وفق القواعد نفسها. وقد ينتج عن ذلك استخدام Tuple متداخلة لتمثيل الكائنات المتداخلة، كما هو موضح أدناه. ولتوضيح ذلك، نستخدم مثال شخص JSON السابق، مع حذف الكائنات الديناميكية:
فيما يلي مخطط هذا الجدول:
لاحظ كيف تم تعريف العمود company على أنه Tuple(catchPhrase String, name String). ويستخدم المفتاح address النوع ‎Array(Tuple)، مع Tuple متداخلة لتمثيل العمود geo. يمكن إدراج JSON في هذا الجدول ببنيته الحالية:
في مثالنا أعلاه، لدينا قدر ضئيل من البيانات، ولكن كما هو موضح أدناه، يمكننا الاستعلام عن أعمدة Tuple باستخدام أسمائها المفصولة بنقاط.
لاحظ كيف يُعاد العمود address.street على شكل Array. وللاستعلام عن كائن معيّن داخل مصفوفة حسب موضعه، يجب تحديد فهرس المصفوفة بعد اسم العمود. على سبيل المثال، للوصول إلى الشارع من العنوان الأول:
يمكن أيضًا استخدام الأعمدة الفرعية في مفاتيح الترتيب اعتبارًا من الإصدار 24.12:

التعامل مع القيم الافتراضية

حتى إذا كانت كائنات JSON منظَّمة، فإنها غالبًا ما تكون متناثرة ولا تتضمن سوى مجموعة فرعية من المفاتيح المعروفة. ولحسن الحظ، لا يتطلب النوع Tuple وجود جميع الأعمدة في حمولة JSON. وإذا لم تُوفَّر، فستُستخدَم القيم الافتراضية. لننظر إلى جدول people الذي استخدمناه سابقًا، وإلى JSON المتناثر التالي الذي يفتقد المفاتيح suite وgeo وphone_numbers وcatchPhrase.
كما نرى أدناه، يمكن إدراج هذا الصف بنجاح:
عند الاستعلام عن هذا الصف، نرى أن القيم الافتراضية تُستخدَم للأعمدة (بما في ذلك الكائنات الفرعية) التي لم تُدرَج:
التمييز بين القيمة الفارغة وNULLإذا كنت بحاجة إلى التمييز بين كون القيمة فارغة وبين عدم توفيرها، فيمكن استخدام النوع Nullable. ويُنصح بتجنّب ذلك ما لم تكن هناك حاجة ماسّة إليه، لأنه سيؤثر سلبًا في التخزين وأداء الاستعلامات لهذه الأعمدة.

التعامل مع الأعمدة الجديدة

في حين أن النهج المهيكل هو الأبسط عندما تكون مفاتيح JSON ثابتة، فلا يزال بالإمكان استخدام هذا النهج إذا أمكن التخطيط للتغييرات على المخطط، أي إذا كانت المفاتيح الجديدة معروفة مسبقًا، ويمكن تعديل المخطط تبعًا لذلك. لاحظ أن ClickHouse يتجاهل، افتراضيًا، مفاتيح JSON المُضمَّنة في الحمولة وغير الموجودة في المخطط. تأمّل حمولة JSON المعدّلة التالية بعد إضافة المفتاح nickname:
يمكن إدراج بيانات JSON هذه بنجاح مع تجاهل المفتاح nickname:
يمكن إضافة أعمدة إلى المخطط باستخدام الأمر ALTER TABLE ADD COLUMN. ويمكن تحديد قيمة افتراضية عبر عبارة DEFAULT، وتُستخدم هذه القيمة إذا لم تُحدَّد أثناء عمليات الإدراج اللاحقة. كما أن الصفوف التي لا تتضمن هذه القيمة (لأنها أُدرجت قبل إنشائها) ستُرجِع أيضًا هذه القيمة الافتراضية. وإذا لم يتم تحديد قيمة DEFAULT، فستُستخدم القيمة الافتراضية للنوع. على سبيل المثال:

التعامل مع البنى شبه المنظَّمة/الديناميكية

إذا كانت بيانات JSON شبه منظَّمة، بحيث يمكن إضافة المفاتيح إليها ديناميكيًا و/أو تكون لها أنواع متعددة، فإن النوع JSON هو الخيار الموصى به. وبشكل أكثر تحديدًا، استخدم النوع JSON عندما تكون بياناتك:
  • تحتوي على مفاتيح غير متوقعة يمكن أن تتغير بمرور الوقت.
  • تتضمن قيمًا ذات أنواع متفاوتة (على سبيل المثال، قد يحتوي مسار ما أحيانًا على سلسلة نصية، وأحيانًا أخرى على رقم).
  • تتطلب مرونة في المخطط حيث لا يكون الالتزام الصارم بالأنواع عمليًا.
  • لديك مئات أو حتى آلاف المسارات الثابتة، لكن التصريح بها صراحةً ليس عمليًا. ويكون هذا نادرًا في العادة.
انظر إلى مثال JSON السابق للشخص، حيث تبيَّن أن الكائن company.labels ديناميكي. لنفترض أن company.labels يحتوي على مفاتيح عشوائية. بالإضافة إلى ذلك، قد لا يكون نوع أي مفتاح في هذه البنية متسقًا بين الصفوف. على سبيل المثال:
نظرًا للطبيعة الديناميكية للعمود company.labels عبر الكائنات من حيث المفاتيح والأنواع، لدينا عدة خيارات لنمذجة هذه البيانات:
  • عمود JSON واحد - يمثّل المخطط بالكامل على هيئة عمود JSON واحد، مما يتيح أن تكون جميع البُنى تحته ديناميكية.
  • عمود JSON موجَّه - استخدام النوع JSON فقط للعمود company.labels، مع الإبقاء على المخطط المنظَّم المستخدم أعلاه لجميع الأعمدة الأخرى.
مع أن النهج الأول لا يتوافق مع المنهجية السابقة، فإن نهج العمود الواحد من نوع JSON مفيد في النماذج الأولية ومهام هندسة البيانات. بالنسبة إلى عمليات نشر ClickHouse في بيئات الإنتاج وعلى نطاق واسع، نوصي بتحديد البنية بدقة واستخدام النوع JSON للبُنى الفرعية الديناميكية الموجَّهة حيثما أمكن. للمخطط الصارم عدد من الفوائد:
  • التحقق من صحة البيانات – إن فرض مخطط صارم يجنّب خطر تضخّم الأعمدة خارج البُنى المحددة.
  • تجنّب خطر تضخّم الأعمدة - رغم أن نوع JSON يمكنه التوسّع ليشمل آلاف الأعمدة المحتملة، إذ تُخزَّن الأعمدة الفرعية كأعمدة مخصّصة، فقد يؤدي ذلك إلى تضخّم ملفات الأعمدة، أي إنشاء عدد مفرط من ملفات الأعمدة بما يؤثر في الأداء. وللحد من ذلك، يوفّر النوع Dynamic الأساسي الذي يستخدمه JSON المعلَمة max_dynamic_paths، التي تقيّد عدد المسارات الفريدة المخزّنة كملفات أعمدة منفصلة. وما إن يُبلَغ هذا الحد، تُخزَّن المسارات الإضافية في ملف عمود مشترك باستخدام تنسيق مرمّز مضغوط، بما يحافظ على الأداء وكفاءة التخزين مع دعم استيعاب البيانات بمرونة. إلا أن الوصول إلى ملف العمود المشترك هذا يكون أقل أداءً. ومع ذلك، تجدر الإشارة إلى أنه يمكن استخدام عمود JSON مع تلميحات الأنواع. وستوفّر الأعمدة “المزوّدة بتلميحات” الأداء نفسه الذي توفّره الأعمدة المخصّصة.
  • استبطان أبسط للمسارات والأنواع - رغم أن نوع JSON يدعم دوال الاستبطان لتحديد الأنواع والمسارات التي جرى استنتاجها، فإن البُنى الثابتة قد تكون أسهل في الاستكشاف، مثلًا باستخدام DESCRIBE.

عمود JSON واحد

يُعد هذا النهج مفيدًا لإنشاء النماذج الأولية ولمهام هندسة البيانات. أمّا في بيئات الإنتاج، فحاول استخدام JSON فقط للبُنى الفرعية الديناميكية عند الحاجة.
اعتبارات الأداءيمكن تحسين عمود JSON واحد من خلال تخطي مسارات JSON غير المطلوبة (أي عدم تخزينها)، ومن خلال استخدام تلميحات الأنواع. تتيح تلميحات الأنواع للمستخدم تحديد النوع صراحةً لعمود فرعي، وبذلك يُستغنى عن الاستدلال والمعالجة غير المباشرة وقت تنفيذ الاستعلام. ويمكن استخدام ذلك لتحقيق مستوى الأداء نفسه كما لو استُخدم مخطط صريح. راجع “استخدام تلميحات الأنواع وتخطي المسارات” لمزيد من التفاصيل.
المخطط لعمود JSON واحد هنا بسيط:
نقدّم تلميحًا للنوع للعمود username في تعريف JSON لأننا نستخدمه ضمن مفتاح الترتيب/المفتاح الأساسي. يساعد ذلك ClickHouse على معرفة أن هذا العمود لن تكون قيمته NULL، ويضمن أيضًا تحديد أي عمود فرعي لـ username ينبغي استخدامه (إذ قد يوجد أكثر من عمود فرعي واحد لكل نوع، وإلا فسيكون الأمر ملتبسًا).
يمكن إدراج الصفوف في الجدول أعلاه باستخدام تنسيق JSONAsObject:
يمكننا التعرّف على الأعمدة الفرعية المستنتجة وأنواعها باستخدام دوال الاستبطان. على سبيل المثال:
للاطّلاع على قائمة كاملة بدوال الاستبطان، راجع “دوال الاستبطان” يمكن الوصول إلى المسارات الفرعية باستخدام ترميز .، مثل:
لاحظ أن الأعمدة غير الموجودة في الصفوف تُعاد بقيمة NULL. بالإضافة إلى ذلك، يُنشأ عمود فرعي منفصل للمسارات التي لها النوع نفسه. على سبيل المثال، يوجد عمود فرعي للمسار company.labels.type بكلٍّ من النوعين String وArray(Nullable(String)). ومع أنه ستتم إعادة كليهما متى أمكن، يمكننا استهداف أعمدة فرعية محددة باستخدام الصياغة .::
لإرجاع الكائنات الفرعية المتداخلة، يلزم استخدام ^. وهذا خيار تصميمي لتجنّب قراءة عدد كبير من الأعمدة ما لم يُطلب ذلك صراحةً. وستُرجِع الكائنات التي يتم الوصول إليها من دون ^ القيمة NULL، كما هو موضح أدناه:

عمود JSON المستهدف

على الرغم من فائدته في مرحلة النماذج الأولية وسيناريوهات هندسة البيانات، نوصي باستخدام مخطط صريح في بيئة الإنتاج كلما أمكن ذلك. يمكن نمذجة المثال السابق باستخدام عمود JSON واحد لعمود company.labels.
يمكننا إدراج البيانات في هذا الجدول باستخدام تنسيق JSONEachRow:
يمكن استخدام دوال الاستبطان لتحديد المسارات والأنواع المستنتجة لعمود company.labels.

استخدام تلميحات الأنواع وتخطّي المسارات

تتيح لنا تلميحات الأنواع تحديد نوع مسارٍ ما وعموده الفرعي، مما يمنع استدلال النوع غير الضروري. انظر إلى المثال التالي، حيث نحدّد الأنواع لمفاتيح JSON ‏dissolved وemployees وfounded داخل عمود JSON ‏company.labels
لاحظ كيف أصبح لهذه الأعمدة الآن أنواع محددة صراحةً:
بالإضافة إلى ذلك، يمكننا تخطي المسارات داخل JSON التي لا نريد تخزينها باستخدام المعاملتين SKIP وSKIP REGEXP لتقليل مساحة التخزين وتجنب الاستدلال غير الضروري على المسارات غير المطلوبة. على سبيل المثال، لنفترض أننا نستخدم عمود JSON واحدًا للبيانات أعلاه. يمكننا تخطي المسارين address وcompany:
لاحظ كيف استُبعدت الأعمدة من بياناتنا:

تحسين الأداء باستخدام تلميحات الأنواع

توفّر تلميحات الأنواع أكثر من مجرد وسيلة لتجنّب استدلال النوع غير الضروري؛ فهي تُلغي بالكامل أي مستوى وسيط في التخزين والمعالجة، كما تتيح تحديد الأنواع البدائية المثلى. وتُخزَّن مسارات JSON التي تتضمن تلميحات للأنواع دائمًا بالطريقة نفسها التي تُخزَّن بها الأعمدة التقليدية، مما يتجاوز الحاجة إلى أعمدة التمييز أو التحليل الديناميكي وقت تنفيذ الاستعلام. وهذا يعني أنه عند استخدام تلميحات أنواع محددة جيدًا، تحقق مفاتيح JSON المتداخلة الأداء والكفاءة نفسيهما كما لو كانت مُصمَّمة منذ البداية كأعمدة من المستوى الأعلى. ونتيجةً لذلك، بالنسبة إلى مجموعات البيانات المتسقة في معظمها والتي لا تزال تستفيد من مرونة JSON، توفّر تلميحات الأنواع وسيلة عملية للحفاظ على الأداء من دون الحاجة إلى إعادة هيكلة المخطط أو مسار إدخال البيانات.

تهيئة المسارات الديناميكية

يخزّن ClickHouse كل مسار JSON كعمود فرعي ضمن تخطيط عمودي حقيقي، مما يتيح مزايا الأداء نفسها التي نراها مع الأعمدة التقليدية، مثل الضغط، والمعالجة المسرّعة باستخدام SIMD، وتقليل عمليات الإدخال/الإخراج على القرص إلى الحد الأدنى. ويمكن لكل تركيبة فريدة من المسار والنوع في بيانات JSON لديك أن تصبح ملف عمود مستقلاً على القرص. على سبيل المثال، عند إدراج مساري JSON بنوعين مختلفين، يخزّن ClickHouse قيم كل نوع فعلي في أعمدة فرعية منفصلة. ويمكن الوصول إلى هذه الأعمدة الفرعية بشكل مستقل، مما يقلّل من عمليات الإدخال/الإخراج غير الضرورية. لاحظ أنه عند الاستعلام عن عمود يضم عدة أنواع، تظل قيمه تُعاد في استجابة عمودية واحدة. إضافةً إلى ذلك، ومن خلال الاستفادة من الإزاحات، يضمن ClickHouse أن تظل هذه الأعمدة الفرعية كثيفة، من دون تخزين أي قيم افتراضية لمسارات JSON غير الموجودة. ويزيد هذا النهج من كفاءة الضغط ويقلل عمليات الإدخال/الإخراج بدرجة أكبر. ومع ذلك، في السيناريوهات التي تتضمن بُنى JSON عالية الكاردينالية أو شديدة التغيّر، مثل خطوط معالجة بيانات القياس عن بُعد، أو السجلات، أو مخازن السمات للتعلّم الآلي - قد يؤدي هذا السلوك إلى تضخم كبير في عدد ملفات الأعمدة. إذ ينتج عن كل مسار JSON فريد جديد ملف عمود جديد، وينتج عن كل تنويعة نوع ضمن ذلك المسار ملف عمود إضافي. وعلى الرغم من أن هذا مثالي لأداء القراءة، فإنه يفرض تحديات تشغيلية، منها استنفاد واصفات الملفات، وزيادة استخدام الذاكرة، وبطء عمليات الدمج بسبب العدد الكبير من الملفات الصغيرة. وللتخفيف من ذلك، يقدّم ClickHouse مفهوم العمود الفرعي الفائض: فبمجرد أن يتجاوز عدد مسارات JSON المميزة عتبةً معيّنة، تُخزَّن المسارات الإضافية في ملف مشترك واحد باستخدام تنسيق مرمّز مضغوط. ويظل هذا الملف قابلاً للاستعلام، لكنه لا يستفيد من خصائص الأداء نفسها التي توفرها الأعمدة الفرعية المخصّصة. ويُتحكَّم في هذه العتبة بواسطة المَعلمة max_dynamic_paths في تعريف نوع JSON.
تجنب ضبط هذه المعلَمة على قيمة مرتفعة جدًا - فالقيم الكبيرة تزيد من استهلاك الموارد وتقلل الكفاءة. وكقاعدة عامة، احرص على أن تبقى دون 10,000. بالنسبة إلى أعباء العمل ذات البُنى شديدة الديناميكية، استخدم تلميحات الأنواع ومعلَمات SKIP لتقييد ما يُخزَّن. للمستخدمين المهتمين بكيفية تنفيذ هذا النوع الجديد من الأعمدة، نوصي بقراءة تدوينتنا التفصيلية “نوع بيانات JSON جديد وقوي لـ ClickHouse”.
آخر تعديل في ٢٩ يونيو ٢٠٢٦