Skip to main content
فيما يلي بدائل لنمذجة JSON في ClickHouse. وقد وُثِّقت هذه البدائل من باب الاكتمال، وكانت مناسبة قبل تطوير نوع JSON، لذا لا يُنصح بها عمومًا، كما أنها لا تناسب معظم حالات الاستخدام.
اتّبع نهجًا على مستوى الكائنيمكن تطبيق تقنيات مختلفة على كائنات مختلفة ضمن المخطط نفسه. على سبيل المثال، قد يكون من الأنسب التعامل مع بعض الكائنات باستخدام النوع String ومع أخرى باستخدام النوع Map. لاحظ أنه بمجرد استخدام النوع String، لا تعود هناك حاجة إلى اتخاذ قرارات إضافية بشأن المخطط. وفي المقابل، يمكن تضمين كائنات فرعية داخل مفتاح Map، بما في ذلك قيمة String تمثل JSON، كما نوضح أدناه:

استخدام النوع String

إذا كانت الكائنات شديدة الديناميكية، من دون بنية متوقعة وتحتوي على كائنات متداخلة عشوائية، فينبغي استخدام النوع String. ويمكن استخراج القيم في وقت الاستعلام باستخدام دوال JSON كما نوضح أدناه. غالبًا ما لا يكون التعامل مع البيانات باستخدام النهج المنظَّم الموضَّح أعلاه عمليًا للمستخدمين الذين يتعاملون مع JSON ديناميكي، سواء كان عرضة للتغيير أو كانت بنيته غير مفهومة جيدًا. وللحصول على أقصى قدر من المرونة، يمكنك ببساطة تخزين JSON على هيئة String قبل استخدام الدوال لاستخراج الحقول حسب الحاجة. ويمثل هذا الطرف المقابل تمامًا للتعامل مع JSON ككائن منظَّم. لكن هذه المرونة لها تكلفة وتنطوي على عيوب كبيرة، أبرزها زيادة تعقيد صياغة الاستعلام وتراجع الأداء. كما أشرنا سابقًا، بالنسبة إلى كائن الشخص الأصلي، لا يمكننا ضمان بنية العمود tags. نُدرج الصف الأصلي (بما في ذلك company.labels، الذي نتجاهله في الوقت الحالي)، مع تعريف العمود Tags على أنه String:
يمكننا اختيار العمود tags وملاحظة أن JSON أُدرِج كسلسلة نصية:
يمكن استخدام دوال JSONExtract لاستخراج القيم من هذا الـ JSON. انظر إلى المثال البسيط أدناه:
لاحظ كيف تتطلب الدوال كلاً من الإشارة إلى العمود tags من النوع String، ومسارًا داخل JSON لاستخراجه. أما المسارات المتداخلة فتتطلب تداخل الدوال، مثلًا JSONExtractUInt(JSONExtractString(tags, 'car'), 'year')، التي تستخرج العمود tags.car.year. ويمكن تبسيط استخراج المسارات المتداخلة باستخدام الدالتين JSON_QUERY وJSON_VALUE. تأمل الحالة القصوى في مجموعة البيانات arxiv، حيث نعدّ المحتوى بالكامل من النوع String.
لإدراج البيانات في هذا المخطّط، نحتاج إلى استخدام تنسيق JSONAsString:
لنفترض أننا نريد إحصاء عدد الأوراق المنشورة حسب السنة. قارن الاستعلام التالي الذي يستخدم سلسلة نصية فقط مع النسخة المهيكلة من المخطط:
لاحظ هنا استخدام تعبير XPath لتصفية JSON حسب method، أي JSON_VALUE(body, '$.versions[0].created'). تكون دوال String أبطأ بكثير (> 10x) من تحويلات النوع الصريحة مع الفهارس. وتتطلب الاستعلامات أعلاه دائمًا فحصًا كاملًا للجدول ومعالجة كل صف. ومع أن هذه الاستعلامات ستظل سريعة على مجموعة بيانات صغيرة كهذه، فإن الأداء سيتدهور مع مجموعات البيانات الأكبر. تأتي مرونة هذا النهج بتكلفة واضحة على صعيدي الأداء والصياغة، لذا ينبغي استخدامه فقط مع الكائنات عالية الديناميكية في المخطط.

دوال JSON البسيطة

تستخدم الأمثلة أعلاه عائلة الدوال JSON*. وتعتمد هذه الدوال على محلّل JSON كامل مبني على simdjson، ويتسم بالدقة في التحليل ويميّز بين الحقل نفسه إذا كان متداخلًا على مستويات مختلفة. كما تستطيع هذه الدوال التعامل مع JSON الصحيح نحويًا لكنه غير منسّق جيدًا، مثل وجود مسافتين بين المفاتيح. تتوفر أيضًا مجموعة دوال أسرع وأكثر صرامة. وتوفّر دوال simpleJSON* هذه أداءً قد يكون أفضل، ويرجع ذلك أساسًا إلى أنها تفترض بشكل صارم بنية JSON وتنسيقه. وتحديدًا:
  • يجب أن تكون أسماء الحقول ثوابت
  • ترميز متّسق لأسماء الحقول، مثل simpleJSONHas('{"abc":"def"}', 'abc') = 1، لكن visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0
  • يجب أن تكون أسماء الحقول فريدة على امتداد جميع البنى المتداخلة. ولا يجري التمييز بين مستويات التداخل، إذ تتم المطابقة بشكل غير انتقائي. وفي حال وجود عدة حقول متطابقة، يُستخدم أول ظهور.
  • لا يُسمح بأحرف خاصة خارج القيم النصية. ويشمل ذلك المسافات. والمثال التالي غير صالح ولن يُحلَّل.
أما المثال التالي فسيُحلَّل على نحو صحيح:
يستخدم الاستعلام أعلاه الدالة simpleJSONExtractString لاستخراج المفتاح created، مستفيدًا من كوننا نحتاج إلى القيمة الأولى فقط لتاريخ النشر. في هذه الحالة، تُعدّ قيود دوال simpleJSON* مقبولة نظرًا لما توفره من تحسن في الأداء.

استخدام نوع Map

إذا كان الكائن يُستخدم لتخزين مفاتيح عشوائية، ويكون معظمها من نوع واحد، ففكّر في استخدام النوع Map. ومن الناحية المثالية، ينبغي ألا يتجاوز عدد المفاتيح الفريدة بضع مئات. ويمكن أيضًا استخدام النوع Map للكائنات التي تحتوي على كائنات فرعية، بشرط أن تكون أنواع هذه الكائنات متجانسة. وبوجه عام، نوصي باستخدام النوع Map للتسميات والوسوم، مثل تسميات كبسولات Kubernetes في بيانات السجل. على الرغم من أن Map يوفّر طريقة بسيطة لتمثيل البُنى المتداخلة، فإن له بعض القيود الملحوظة:
  • يجب أن تكون جميع الحقول من النوع نفسه.
  • يتطلب الوصول إلى الأعمدة الفرعية صياغة خاصة بـ map لأن الحقول لا تكون موجودة كأعمدة. فالكائن بأكمله هو عمود.
  • يؤدي الوصول إلى عمود فرعي إلى تحميل قيمة Map بالكامل، أي جميع العناصر النظيرة وقيمها المقابلة. وبالنسبة إلى الخرائط الأكبر حجمًا، قد يترتب على ذلك تأثير كبير في الأداء.
مفاتيح Stringعند نمذجة الكائنات على أنها Map، يُستخدم مفتاح من النوع String لتخزين اسم مفتاح JSON. لذلك ستكون الخريطة دائمًا Map(String, T)، حيث يعتمد T على البيانات.

القيم البدائية

أبسط استخدام لـ Map يكون عندما يحتوي الكائن على قيم من النوع البدائي نفسه. وفي معظم الحالات، يتضمن ذلك استخدام النوع String للقيمة T. تأمل مثال JSON السابق الخاص بالشخص، حيث تبيّن أن الكائن company.labels ديناميكي. والمهم هنا أننا لا نتوقع إضافة سوى أزواج مفتاح-قيمة من النوع String إلى هذا الكائن. لذا يمكننا تعريفه على أنه Map(String, String):
يمكننا إدراج كائن JSON الأصلي بالكامل:
يتطلّب الاستعلام عن هذه الحقول داخل كائن الطلب استخدام صيغة map، على سبيل المثال:
تتوفر مجموعة كاملة من دوال Map للاستعلام عن هذا النوع، وهي موضحة هنا. إذا لم تكن بياناتك من نوع متسق، فتوجد دوال لإجراء تحويل النوع اللازم.

قيم الكائنات

يمكن أيضًا النظر في النوع Map للكائنات التي تتضمن كائنات فرعية، شريطة أن تكون أنواع هذه الكائنات متسقة. لنفترض أن المفتاح tags في الكائن persons يتطلب بنية متسقة، بحيث يحتوي الكائن الفرعي لكل tag على العمودين name وtime. وقد يبدو مثال مبسّط على مستند JSON من هذا النوع كما يلي:
يمكن نمذجة ذلك باستخدام Map(String, Tuple(name String, time DateTime)) كما هو موضح أدناه:
يكون استخدام قيم maps في هذه الحالة نادرًا في العادة، وهذا يشير إلى أنه ينبغي إعادة نمذجة البيانات بحيث لا تتضمن أسماء المفاتيح الديناميكية كائنات فرعية. على سبيل المثال، يمكن إعادة نمذجة ما سبق على النحو التالي، بما يتيح استخدام Array(Tuple(key String, name String, time DateTime)).

استخدام النوع Nested

يمكن استخدام النوع Nested لنمذجة الكائنات الثابتة التي نادرًا ما تتغير، ما يوفّر بديلًا لـ Tuple وArray(Tuple). نوصي عمومًا بتجنّب استخدام هذا النوع مع JSON لأن سلوكه يكون مربكًا في كثير من الأحيان. وتتمثل الفائدة الأساسية لـ Nested في إمكانية استخدام الأعمدة الفرعية في مفاتيح الترتيب. فيما يلي مثال على استخدام النوع Nested لنمذجة كائن ثابت. لننظر إلى إدخال سجل بسيط التالي بصيغة JSON:
يمكننا تعريف الحقل request على أنه Nested. وكما هو الحال مع Tuple، يجب تحديد الأعمدة الفرعية.

flatten_nested

يتحكّم الإعداد flatten_nested في سلوك النوع Nested.

flatten_nested=1

القيمة 1 (الافتراضية) لا تدعم مستوىً عشوائيًا من التداخل. ومع هذه القيمة، من الأسهل اعتبار بنية البيانات المتداخلة مجموعةً من أعمدة Array المتعددة ذات الطول نفسه. وتكون الحقول method وpath وversion فعليًا أعمدة منفصلة من النوع Array(Type)، مع قيد أساسي واحد: يجب أن يكون طول الحقول method وpath وversion متماثلًا. ويوضح SHOW CREATE TABLE ذلك:
فيما يلي، نُدرِج بيانات في هذا الجدول:
هناك بعض النقاط المهمة التي ينبغي الانتباه إليها هنا:
  • نحتاج إلى استخدام الإعداد input_format_import_nested_json لإدراج بيانات JSON كبنية متداخلة. وبدون ذلك، سنضطر إلى تسطيح JSON، أي:
  • يجب تمرير الحقول المتداخلة method وpath وversion على شكل مصفوفات JSON، أي:
يمكن الاستعلام عن الأعمدة باستخدام ترميز النقطة:
لاحظ أن استخدام Array في الأعمدة الفرعية يعني أنه يمكن الاستفادة من كامل إمكانات دوال المصفوفات، بما في ذلك بند ARRAY JOIN — وهو مفيد إذا كانت أعمدتك تحتوي على قيم متعددة.

flatten_nested=0

يتيح هذا أي مستوى من التداخل، ويعني أن الأعمدة المتداخلة تبقى كمصفوفة واحدة من Tuple — أي إنها تصبح فعليًا مماثلة لـ Array(Tuple). يمثل هذا الأسلوب المفضل، وغالبًا ما يكون أيضًا أبسط طريقة، لاستخدام JSON مع Nested. وكما نوضح أدناه، لا يتطلب ذلك سوى أن تكون جميع الكائنات ضمن قائمة. فيما يلي، نعيد إنشاء جدولنا ونعيد إدراج صف:
فيما يلي بعض النقاط المهمة التي ينبغي الانتباه إليها هنا:
  • لا يلزم input_format_import_nested_json لعملية الإدراج.
  • يظل النوع Nested محفوظًا في SHOW CREATE TABLE. وفعليًا، تكون البنية الداخلية لهذا العمود هي Array(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String))))
  • ونتيجةً لذلك، يجب إدراج request على هيئة مصفوفة، أي:
يمكن أيضًا الاستعلام عن الأعمدة باستخدام ترميز النقطة:

مثال

يتوفر مثال أكبر للبيانات أعلاه ضمن bucket عامة على S3 على: s3://datasets-documentation/http/.
نظرًا إلى القيود وصيغة الإدخال الخاصة بـ JSON، نُدرِج مجموعة البيانات النموذجية هذه باستخدام الاستعلام التالي. هنا، نضبط flatten_nested=0. تُدرِج التعليمة التالية 10 ملايين صف، لذا قد يستغرق تنفيذها بضع دقائق. استخدم LIMIT إذا لزم الأمر:
يتطلّب الاستعلام عن هذه البيانات الوصول إلى حقول الطلب باعتبارها مصفوفات. فيما يلي نلخّص الأخطاء وطرق HTTP ضمن فترة زمنية ثابتة.

استخدام المصفوفات الثنائية

توفر المصفوفات الثنائية توازناً بين مرونة تمثيل JSON على هيئة Strings وأداء نهج أكثر تنظيماً. ويظل المخطط مرناً، إذ يمكن نظرياً إضافة أي حقول جديدة إلى الجذر. لكن هذا يتطلب صياغة query أكثر تعقيداً بدرجة كبيرة، كما أنه غير متوافق مع البُنى المتداخلة. على سبيل المثال، تأمل الجدول التالي:
لإدراج البيانات في هذا الجدول، نحتاج إلى هيكلة JSON في صورة قائمة من المفاتيح والقيم. يوضّح الاستعلام التالي كيفية استخدام JSONExtractKeysAndValues لتحقيق ذلك:
لاحظ كيف يظل عمود الطلب بنيةً متداخلةً ممثَّلةً كسلسلة نصية. يمكننا إدراج أي مفاتيح جديدة في المستوى الجذري. ويمكن أيضًا أن تكون هناك اختلافات كيفما كانت في JSON نفسه. للإدراج في جدولنا المحلي، نفّذ ما يلي:
يتطلب الاستعلام عن هذا التركيب استخدام الدالة indexOf لتحديد فهرس المفتاح المطلوب (على أن يكون متوافقًا مع ترتيب القيم). ويمكن استخدام ذلك للوصول إلى عمود المصفوفة values، أي values[indexOf(keys, 'status')]. وما زلنا بحاجة إلى طريقة لتحليل JSON لعمود request — وفي هذه الحالة، simpleJSONExtractString.
آخر تعديل في ٣ يوليو ٢٠٢٦