اتّبع نهجًا على مستوى الكائنيمكن تطبيق تقنيات مختلفة على كائنات مختلفة ضمن المخطط نفسه. على سبيل المثال، قد يكون من الأنسب التعامل مع بعض الكائنات باستخدام النوع
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:
JSON_VALUE(body, '$.versions[0].created').
تكون دوال String أبطأ بكثير (> 10x) من تحويلات النوع الصريحة مع الفهارس. وتتطلب الاستعلامات أعلاه دائمًا فحصًا كاملًا للجدول ومعالجة كل صف. ومع أن هذه الاستعلامات ستظل سريعة على مجموعة بيانات صغيرة كهذه، فإن الأداء سيتدهور مع مجموعات البيانات الأكبر.
تأتي مرونة هذا النهج بتكلفة واضحة على صعيدي الأداء والصياغة، لذا ينبغي استخدامه فقط مع الكائنات عالية الديناميكية في المخطط.
دوال 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):
map، على سبيل المثال:
Map للاستعلام عن هذا النوع، وهي موضحة هنا. إذا لم تكن بياناتك من نوع متسق، فتوجد دوال لإجراء تحويل النوع اللازم.
قيم الكائنات
يمكن أيضًا النظر في النوعMap للكائنات التي تتضمن كائنات فرعية، شريطة أن تكون أنواع هذه الكائنات متسقة.
لنفترض أن المفتاح tags في الكائن persons يتطلب بنية متسقة، بحيث يحتوي الكائن الفرعي لكل tag على العمودين name وtime. وقد يبدو مثال مبسّط على مستند JSON من هذا النوع كما يلي:
Map(String, Tuple(name String, time DateTime)) كما هو موضح أدناه:
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/.
flatten_nested=0.
تُدرِج التعليمة التالية 10 ملايين صف، لذا قد يستغرق تنفيذها بضع دقائق. استخدم LIMIT إذا لزم الأمر:
استخدام المصفوفات الثنائية
توفر المصفوفات الثنائية توازناً بين مرونة تمثيل JSON على هيئة Strings وأداء نهج أكثر تنظيماً. ويظل المخطط مرناً، إذ يمكن نظرياً إضافة أي حقول جديدة إلى الجذر. لكن هذا يتطلب صياغة query أكثر تعقيداً بدرجة كبيرة، كما أنه غير متوافق مع البُنى المتداخلة. على سبيل المثال، تأمل الجدول التالي:JSONExtractKeysAndValues لتحقيق ذلك:
indexOf لتحديد فهرس المفتاح المطلوب (على أن يكون متوافقًا مع ترتيب القيم). ويمكن استخدام ذلك للوصول إلى عمود المصفوفة values، أي values[indexOf(keys, 'status')]. وما زلنا بحاجة إلى طريقة لتحليل JSON لعمود request — وفي هذه الحالة، simpleJSONExtractString.