يقدّم ClickHouse الآن نوع عمود JSON أصليًا، وهو مصمَّم للتعامل مع البيانات شبه المنظَّمة والديناميكية. ومن المهم توضيح أن هذا نوع عمود وليس تنسيق بيانات—فيمكنك إدراج JSON في ClickHouse كسلسلة نصية أو عبر التنسيقات المدعومة مثل JSONEachRow، لكن هذا لا يعني أنك تستخدم نوع عمود JSON. ولا ينبغي استخدام نوع JSON إلا عندما يكون هيكل بياناتك ديناميكيًا، لا لمجرد أنك تخزّن بيانات بتنسيق JSON.
متى تستخدم النوع JSON
صُمِّم النوع JSON للاستعلام عن حقول محددة داخل كائنات JSON وتصفيتها وتجميعها عندما تكون بنيتها ديناميكية أو غير متوقعة. ويحقق ذلك عبر تقسيم كائنات JSON إلى أعمدة فرعية منفصلة، ما يقلل بدرجة كبيرة من حجم البيانات المقروءة ويُسرّع الاستعلامات على الحقول المحددة مقارنةً ببدائل مثل Map أو تحليل السلاسل النصية.
لكن هذا ينطوي على بعض المقايضات المهمة:
- بطء عمليات
INSERT- إن تقسيم JSON إلى أعمدة فرعية، وإجراء استنتاج النوع، وإدارة هياكل تخزين مرنة، يجعل عمليات الإدراج أبطأ مقارنةً بتخزين JSON في عمودStringبسيط. - بطء عند قراءة الكائنات كاملةً - إذا كنت بحاجة إلى استرجاع مستندات JSON كاملة (بدلاً من حقول محددة)، فإن النوع
JSONيكون أبطأ من القراءة من عمودString. فالعبء الإضافي الناتج عن إعادة تكوين الكائنات من الأعمدة الفرعية المنفصلة لا يحقق أي فائدة عندما لا تُجري استعلامات على مستوى الحقول. - عبء تخزيني إضافي - إن الاحتفاظ بأعمدة فرعية منفصلة يضيف عبئًا بنيويًا مقارنةً بتخزين JSON كسلسلة نصية واحدة.
استخدم النوع JSON عندما:
- تكون بياناتك ذات بنية ديناميكية أو غير متوقعة، مع مفاتيح تختلف من مستند إلى آخر
- تتغير أنواع الحقول أو البُنى بمرور الوقت أو تختلف بين السجلات
- تحتاج إلى الاستعلام أو التصفية أو التجميع على مسارات محددة داخل كائنات JSON التي لا يمكنك توقّع بنيتها مسبقًا
- تتضمن حالة الاستخدام لديك بيانات شبه مهيكلة مثل السجلات أو الأحداث أو المحتوى الذي ينشئه المستخدم، مع بُنى غير متسقة
استخدم عمود String (أو الأنواع المهيكلة) عندما:
- تكون بنية بياناتك معروفة ومتسقة — في هذه الحالة، استخدم الأعمدة العادية أو الأنواع
TupleوArrayوDynamicوVariantبدلًا من ذلك - تُعامَل مستندات
JSONعلى أنها blobs معتمة لا تُخزَّن ولا تُسترجَع إلا كاملةً، من دون تحليل على مستوى الحقول - لا تحتاج إلى إجراء استعلامات أو تصفية على حقول
JSONالفردية داخل قاعدة البيانات - يكون
JSONمجرد تنسيق للنقل/التخزين، ولا يُحلَّل داخل ClickHouse
اعتبارات ونصائح لاستخدام JSON
يتيح نوع JSON تخزينًا عموديًا فعّالًا عبر تسطيح المسارات إلى أعمدة فرعية. لكن المرونة تتطلب أيضًا قدرًا من الانضباط. ولاستخدامه بفعالية:
- حدِّد أنواع المسارات باستخدام تلميحات في تعريف العمود لتحديد أنواع الأعمدة الفرعية المعروفة، وتجنّب استدلال الأنواع غير الضروري.
- تخطَّ بعض المسارات إذا لم تكن بحاجة إلى قيمها، باستخدام SKIP و SKIP REGEXP لتقليل التخزين وتحسين الأداء.
- تجنّب ضبط
max_dynamic_pathsعلى قيمة مرتفعة جدًا—إذ تؤدي القيم الكبيرة إلى زيادة استهلاك الموارد وتقليل الكفاءة. وكقاعدة عامة، اجعله أقل من 10,000.
الميزات المتقدمة
- يمكن استخدام أعمدة 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يمكننا إدراج في هذا الجدول وعرض المخطط المستنتج لاحقًا باستخدام الدالة 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_dateINSERT 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.