يقدّم ClickHouse الآن نوع عمود JSON أصليًا، وهو مصمَّم للتعامل مع البيانات شبه المنظَّمة والديناميكية. ومن المهم توضيح أن هذا نوع عمود وليس تنسيق بيانات—فيمكنك إدراج JSON في ClickHouse كسلسلة نصية أو عبر التنسيقات المدعومة مثل JSONEachRow، لكن هذا لا يعني أنك تستخدم نوع عمود JSON. ولا ينبغي استخدام نوع JSON إلا عندما يكون هيكل بياناتك ديناميكيًا، لا لمجرد أنك تخزّن بيانات بتنسيق JSON.
صُمِّم النوع JSON للاستعلام عن حقول محددة داخل كائنات JSON وتصفيتها وتجميعها عندما تكون بنيتها ديناميكية أو غير متوقعة. ويحقق ذلك عبر تقسيم كائنات JSON إلى أعمدة فرعية منفصلة، ما يقلل بدرجة كبيرة من حجم البيانات المقروءة ويُسرّع الاستعلامات على الحقول المحددة مقارنةً ببدائل مثل Map أو تحليل السلاسل النصية.
لكن هذا ينطوي على بعض المقايضات المهمة:
- بطء عمليات
INSERT - إن تقسيم JSON إلى أعمدة فرعية، وإجراء استنتاج النوع، وإدارة هياكل تخزين مرنة، يجعل عمليات الإدراج أبطأ مقارنةً بتخزين JSON في عمود String بسيط.
- بطء عند قراءة الكائنات كاملةً - إذا كنت بحاجة إلى استرجاع مستندات JSON كاملة (بدلاً من حقول محددة)، فإن النوع
JSON يكون أبطأ من القراءة من عمود String. فالعبء الإضافي الناتج عن إعادة تكوين الكائنات من الأعمدة الفرعية المنفصلة لا يحقق أي فائدة عندما لا تُجري استعلامات على مستوى الحقول.
- عبء تخزيني إضافي - إن الاحتفاظ بأعمدة فرعية منفصلة يضيف عبئًا بنيويًا مقارنةً بتخزين JSON كسلسلة نصية واحدة.
- تكون بياناتك ذات بنية ديناميكية أو غير متوقعة، مع مفاتيح تختلف من مستند إلى آخر
- تتغير أنواع الحقول أو البُنى بمرور الوقت أو تختلف بين السجلات
- تحتاج إلى الاستعلام أو التصفية أو التجميع على مسارات محددة داخل كائنات JSON التي لا يمكنك توقّع بنيتها مسبقًا
- تتضمن حالة الاستخدام لديك بيانات شبه مهيكلة مثل السجلات أو الأحداث أو المحتوى الذي ينشئه المستخدم، مع بُنى غير متسقة
استخدم عمود String (أو الأنواع المهيكلة) عندما:
- تكون بنية بياناتك معروفة ومتسقة — في هذه الحالة، استخدم الأعمدة العادية أو الأنواع
Tuple وArray وDynamic وVariant بدلًا من ذلك
- تُعامَل مستندات
JSON على أنها blobs معتمة لا تُخزَّن ولا تُسترجَع إلا كاملةً، من دون تحليل على مستوى الحقول
- لا تحتاج إلى إجراء استعلامات أو تصفية على حقول
JSON الفردية داخل قاعدة البيانات
- يكون
JSON مجرد تنسيق للنقل/التخزين، ولا يُحلَّل داخل ClickHouse
إذا كان JSON مستندًا معتمًا لا يُحلَّل داخل قاعدة البيانات، ويُخزَّن ويُسترجَع فقط، فينبغي تخزينه كحقل String. ولا تظهر مزايا النوع JSON إلا عندما تحتاج إلى الاستعلام أو التصفية أو التجميع بكفاءة على حقول محددة داخل بُنى JSON الديناميكية.يمكنك أيضًا المزج بين النهجين — استخدم الأعمدة القياسية للحقول العلوية المتوقعة، وعمود JSON للأجزاء الديناميكية من الحمولة.
اعتبارات ونصائح لاستخدام JSON
يتيح نوع JSON تخزينًا عموديًا فعّالًا عبر تسطيح المسارات إلى أعمدة فرعية. لكن المرونة تتطلب أيضًا قدرًا من الانضباط. ولاستخدامه بفعالية:
- حدِّد أنواع المسارات باستخدام تلميحات في تعريف العمود لتحديد أنواع الأعمدة الفرعية المعروفة، وتجنّب استدلال الأنواع غير الضروري.
- تخطَّ بعض المسارات إذا لم تكن بحاجة إلى قيمها، باستخدام SKIP و SKIP REGEXP لتقليل التخزين وتحسين الأداء.
- تجنّب ضبط
max_dynamic_paths على قيمة مرتفعة جدًا—إذ تؤدي القيم الكبيرة إلى زيادة استهلاك الموارد وتقليل الكفاءة. وكقاعدة عامة، اجعله أقل من 10,000.
تلميحات الأنواعتوفّر تلميحات الأنواع أكثر من مجرد وسيلة لتجنّب استدلال الأنواع غير الضروري، إذ إنها تلغي بالكامل طبقة عدم المباشرة في التخزين والمعالجة. فمسارات JSON التي تتضمن تلميحات أنواع تُخزَّن دائمًا بالطريقة نفسها التي تُخزَّن بها الأعمدة التقليدية، من دون الحاجة إلى أعمدة التمييز أو التحليل الديناميكي وقت الاستعلام. وهذا يعني أنه عند تحديد تلميحات الأنواع بشكل جيد، تحقق حقول JSON المتداخلة الأداء والكفاءة نفسيهما كما لو أنها صُممت منذ البداية كحقول من المستوى الأعلى. ونتيجةً لذلك، بالنسبة إلى مجموعات البيانات المتسقة في معظمها مع الاستفادة في الوقت نفسه من مرونة JSON، توفّر تلميحات الأنواع وسيلة عملية للحفاظ على الأداء من دون الحاجة إلى إعادة هيكلة المخطط أو مسار إدخال البيانات.
- يمكن استخدام أعمدة JSON في المفاتيح الأساسية مثل أي أعمدة أخرى. ولا يمكن تحديد خوارزميات الضغط لعمود فرعي.
- وهي تدعم الفحص الداخلي عبر دوال مثل
JSONAllPathsWithTypes() و JSONDynamicPaths().
- يمكنك قراءة الكائنات الفرعية المتداخلة باستخدام الصياغة
.^.
- قد تختلف بنية الاستعلام عن SQL القياسية، وقد تتطلب تحويل أنواع خاصًا أو عوامل خاصة للحقول المتداخلة.
للحصول على إرشادات إضافية، راجع توثيق JSON في ClickHouse أو اطّلع على منشور مدونتنا نوع بيانات JSON جديد وقوي في ClickHouse.
تأمل نموذج JSON التالي، الذي يمثّل صفًا من مجموعة بيانات Python PyPI:
{
"date": "2022-11-15",
"country_code": "ES",
"project": "clickhouse-connect",
"type": "bdist_wheel",
"installer": "pip",
"python_minor": "3.9",
"system": "Linux",
"version": "0.3.0"
}
لنفترض أن هذا المخطّط ثابت، وأن الأنواع فيه يمكن تحديدها بوضوح. وحتى إذا كانت البيانات بتنسيق NDJSON (صف JSON واحد في كل سطر)، فلا حاجة إلى استخدام نوع JSON لمثل هذا المخطّط. يكفي تعريف المخطّط باستخدام الأنواع التقليدية.
CREATE TABLE pypi (
`date` Date,
`country_code` String,
`project` String,
`type` String,
`installer` String,
`python_minor` String,
`system` String,
`version` String
)
ENGINE = MergeTree
ORDER BY (project, date)
وأدرِج صفوف JSON:
INSERT INTO pypi FORMAT JSONEachRow
{"date":"2022-11-15","country_code":"ES","project":"clickhouse-connect","type":"bdist_wheel","installer":"pip","python_minor":"3.9","system":"Linux","version":"0.3.0"}
لنأخذ مجموعة بيانات arXiv التي تضم 2.5 مليون ورقة علمية. يمثّل كل صف في مجموعة البيانات هذه، والموزعة بصيغة NDJSON، ورقةً أكاديمية منشورة. ويظهر أدناه صفٌّ نموذجي:
{
"id": "2101.11408",
"submitter": "Daniel Lemire",
"authors": "Daniel Lemire",
"title": "Number Parsing at a Gigabyte per Second",
"comments": "Software at https://github.com/fastfloat/fast_float and\n https://github.com/lemire/simple_fastfloat_benchmark/",
"journal-ref": "Software: Practice and Experience 51 (8), 2021",
"doi": "10.1002/spe.2984",
"report-no": null,
"categories": "cs.DS cs.MS",
"license": "http://creativecommons.org/licenses/by/4.0/",
"abstract": "With disks and networks providing gigabytes per second ....\n",
"versions": [
{
"created": "Mon, 11 Jan 2021 20:31:27 GMT",
"version": "v1"
},
{
"created": "Sat, 30 Jan 2021 23:57:29 GMT",
"version": "v2"
}
],
"update_date": "2022-11-07",
"authors_parsed": [
[
"Lemire",
"Daniel",
""
]
]
}
رغم أن JSON هنا معقّد ويحتوي على بُنى متداخلة، فإنه يمكن التنبؤ به. لن يتغير عدد الحقول ولا أنواعها. ومع أنه يمكننا استخدام النوع JSON في هذا المثال، يمكننا أيضًا ببساطة تعريف البنية بشكل صريح باستخدام النوعين Tuples وNested:
CREATE TABLE arxiv
(
`id` String,
`submitter` String,
`authors` String,
`title` String,
`comments` String,
`journal-ref` String,
`doi` String,
`report-no` String,
`categories` String,
`license` String,
`abstract` String,
`versions` Array(Tuple(created String, version String)),
`update_date` Date,
`authors_parsed` Array(Array(String))
)
ENGINE = MergeTree
ORDER BY update_date
مرة أخرى، يمكننا إدراج البيانات بتنسيق JSON:
INSERT INTO arxiv FORMAT JSONEachRow
{"id":"2101.11408","submitter":"Daniel Lemire","authors":"Daniel Lemire","title":"Number Parsing at a Gigabyte per Second","comments":"Software at https://github.com/fastfloat/fast_float and\n https://github.com/lemire/simple_fastfloat_benchmark/","journal-ref":"Software: Practice and Experience 51 (8), 2021","doi":"10.1002/spe.2984","report-no":null,"categories":"cs.DS cs.MS","license":"http://creativecommons.org/licenses/by/4.0/","abstract":"With disks and networks providing gigabytes per second ....\n","versions":[{"created":"Mon, 11 Jan 2021 20:31:27 GMT","version":"v1"},{"created":"Sat, 30 Jan 2021 23:57:29 GMT","version":"v2"}],"update_date":"2022-11-07","authors_parsed":[["Lemire","Daniel",""]]}
لنفترض أنه تمت إضافة عمود آخر باسم tags. إذا كانت هذه مجرد قائمة من السلاسل النصية، فيمكننا تمثيلها على أنها Array(String)، لكن لنفترض أنه يمكنك إضافة هياكل وسوم عشوائية بأنواع مختلطة (لاحظ أن score قد يكون سلسلة نصية أو عددًا صحيحًا). مستند JSON المعدَّل لدينا:
{
"id": "2101.11408",
"submitter": "Daniel Lemire",
"authors": "Daniel Lemire",
"title": "Number Parsing at a Gigabyte per Second",
"comments": "Software at https://github.com/fastfloat/fast_float and\n https://github.com/lemire/simple_fastfloat_benchmark/",
"journal-ref": "Software: Practice and Experience 51 (8), 2021",
"doi": "10.1002/spe.2984",
"report-no": null,
"categories": "cs.DS cs.MS",
"license": "http://creativecommons.org/licenses/by/4.0/",
"abstract": "With disks and networks providing gigabytes per second ....\n",
"versions": [
{
"created": "Mon, 11 Jan 2021 20:31:27 GMT",
"version": "v1"
},
{
"created": "Sat, 30 Jan 2021 23:57:29 GMT",
"version": "v2"
}
],
"update_date": "2022-11-07",
"authors_parsed": [
[
"Lemire",
"Daniel",
""
]
],
"tags": {
"tag_1": {
"name": "ClickHouse user",
"score": "A+",
"comment": "A good read, applicable to ClickHouse"
},
"28_03_2025": {
"name": "professor X",
"score": 10,
"comment": "Didn't learn much",
"updates": [
{
"name": "professor X",
"comment": "Wolverine found more interesting"
}
]
}
}
}
في هذه الحالة، يمكننا نمذجة مستندات arXiv إما بالكامل بصيغة JSON أو ببساطة بإضافة عمود tags من نوع JSON. نوفّر كلا المثالين أدناه:
CREATE TABLE arxiv
(
`doc` JSON(update_date Date)
)
ENGINE = MergeTree
ORDER BY doc.update_date
نقدّم تلميحًا للنوع للعمود update_date في تعريف JSON، لأننا نستخدمه في الترتيب/المفتاح الأساسي. وهذا يساعد ClickHouse على معرفة أن هذا العمود لن تكون قيمته NULL، ويضمن أيضًا معرفة أي عمود فرعي update_date يجب استخدامه (إذ قد يوجد أكثر من عمود فرعي واحد لكل نوع، ولولا ذلك لكان الأمر ملتبسًا).
يمكننا إدراج في هذا الجدول وعرض المخطط المستنتج لاحقًا باستخدام الدالة JSONAllPathsWithTypes وoutput format PrettyJSONEachRow:
INSERT INTO arxiv FORMAT JSONAsObject
{"id":"2101.11408","submitter":"Daniel Lemire","authors":"Daniel Lemire","title":"Number Parsing at a Gigabyte per Second","comments":"Software at https://github.com/fastfloat/fast_float and\n https://github.com/lemire/simple_fastfloat_benchmark/","journal-ref":"Software: Practice and Experience 51 (8), 2021","doi":"10.1002/spe.2984","report-no":null,"categories":"cs.DS cs.MS","license":"http://creativecommons.org/licenses/by/4.0/","abstract":"With disks and networks providing gigabytes per second ....\n","versions":[{"created":"Mon, 11 Jan 2021 20:31:27 GMT","version":"v1"},{"created":"Sat, 30 Jan 2021 23:57:29 GMT","version":"v2"}],"update_date":"2022-11-07","authors_parsed":[["Lemire","Daniel",""]],"tags":{"tag_1":{"name":"ClickHouse user","score":"A+","comment":"A good read, applicable to ClickHouse"},"28_03_2025":{"name":"professor X","score":10,"comment":"Didn't learn much","updates":[{"name":"professor X","comment":"Wolverine found more interesting"}]}}}
SELECT JSONAllPathsWithTypes(doc)
FROM arxiv
FORMAT PrettyJSONEachRow
{
"JSONAllPathsWithTypes(doc)": {
"abstract": "String",
"authors": "String",
"authors_parsed": "Array(Array(Nullable(String)))",
"categories": "String",
"comments": "String",
"doi": "String",
"id": "String",
"journal-ref": "String",
"license": "String",
"submitter": "String",
"tags.28_03_2025.comment": "String",
"tags.28_03_2025.name": "String",
"tags.28_03_2025.score": "Int64",
"tags.28_03_2025.updates": "Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))",
"tags.tag_1.comment": "String",
"tags.tag_1.name": "String",
"tags.tag_1.score": "String",
"title": "String",
"update_date": "Date",
"versions": "Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))"
}
}
1 row in set. Elapsed: 0.003 sec.
بدلًا من ذلك، يمكننا تمثيل ذلك باستخدام المخطط السابق وعمود JSON باسم tags. ويُفضَّل هذا النهج عمومًا لأنه يقلّل من مقدار الاستدلال الذي يتطلبه ClickHouse:
CREATE TABLE arxiv
(
`id` String,
`submitter` String,
`authors` String,
`title` String,
`comments` String,
`journal-ref` String,
`doi` String,
`report-no` String,
`categories` String,
`license` String,
`abstract` String,
`versions` Array(Tuple(created String, version String)),
`update_date` Date,
`authors_parsed` Array(Array(String)),
`tags` JSON()
)
ENGINE = MergeTree
ORDER BY update_date
INSERT INTO arxiv FORMAT JSONEachRow
{"id":"2101.11408","submitter":"Daniel Lemire","authors":"Daniel Lemire","title":"Number Parsing at a Gigabyte per Second","comments":"Software at https://github.com/fastfloat/fast_float and\n https://github.com/lemire/simple_fastfloat_benchmark/","journal-ref":"Software: Practice and Experience 51 (8), 2021","doi":"10.1002/spe.2984","report-no":null,"categories":"cs.DS cs.MS","license":"http://creativecommons.org/licenses/by/4.0/","abstract":"With disks and networks providing gigabytes per second ....\n","versions":[{"created":"Mon, 11 Jan 2021 20:31:27 GMT","version":"v1"},{"created":"Sat, 30 Jan 2021 23:57:29 GMT","version":"v2"}],"update_date":"2022-11-07","authors_parsed":[["Lemire","Daniel",""]],"tags":{"tag_1":{"name":"ClickHouse user","score":"A+","comment":"A good read, applicable to ClickHouse"},"28_03_2025":{"name":"professor X","score":10,"comment":"Didn't learn much","updates":[{"name":"professor X","comment":"Wolverine found more interesting"}]}}}
يمكننا الآن استنتاج أنواع البيانات للعمود الفرعي tags.
SELECT JSONAllPathsWithTypes(tags)
FROM arxiv
FORMAT PrettyJSONEachRow
{
"JSONAllPathsWithTypes(tags)": {
"28_03_2025.comment": "String",
"28_03_2025.name": "String",
"28_03_2025.score": "Int64",
"28_03_2025.updates": "Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))",
"tag_1.comment": "String",
"tag_1.name": "String",
"tag_1.score": "String"
}
}
1 row in set. Elapsed: 0.002 sec.