تصل بياناتك بصيغة JSON. يوفّر ClickHouse عدة طرق لتخزينها، بدءًا من الأعمدة ذات الأنواع المحددة بالكامل وصولًا إلى String الخام. ويعتمد الاختيار المناسب على مدى ثبات مخططك، وما إذا كنت تحتاج إلى استعلامات على مستوى الحقول.
النطاق: تغطي هذه الصفحة قرارات تصميم المخطط الخاصة بتخزين بيانات JSON. ولا تغطي تنسيقات إدخال/إخراج JSON، أو دوال JSON، أو بنية الاستعلام. وللاطلاع على مزيد من المعلومات حول نوع العمود JSON نفسه، راجع استخدم JSON حيثما كان مناسبًا.
يفترض: الإلمام بـ إنشاء جداول ClickHouse، وأساسيات MergeTree، وبنية أنواع الأعمدة.
قرار سريع
- إذا كان لكل حقل نوع معروف ومستقر، وكان المخطط نادر التغيّر → أعمدة محددة النوع
- إذا كانت معظم الحقول مستقرة، لكن بعض الأجزاء ديناميكيًا أو غير متوقع → هجين (أعمدة محددة النوع + JSON)
- إذا كانت البنية بأكملها ديناميكية، مع مفاتيح تظهر وتختفي بين السجلات → عمود JSON أصلي
- إذا كانت الحقول الديناميكية أزواج مفتاح-قيمة ذات نوع قيمة متسق (مثل tags النصية وmetrics الرقمية)
→
Mapبدلًا من JSON - إذا كنت تخزّن وتسترجع blob JSON فقط من دون استعلامات على مستوى الحقل → تخزين String غير مُفسَّر
تفاصيل النهج
أعمدة محددة النوع
متى تُستخدم: تكون بنية JSON معروفة بالكامل وقت التصميم. ولا تتغير الحقول والأنواع من سجل إلى آخر. ويمكن حتى تمثيل البُنى المتداخلة المعقدة (مثل مصفوفات الكائنات والخرائط المتداخلة) باستخدام الأنواع Array وTuple وNested.
الاعتبارات: تتطلب تغييرات المخطط استخدام ALTER TABLE. كما تُهمَل الحقول غير المتوقعة بصمت عند الإدراج ما لم يتم تحديث المخطط.
الإعداد والتحقق والمحاذير
الإعداد
CREATE TABLE events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`level` Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
`message` String,
`host` LowCardinality(String),
`duration_ms` UInt32
)
ENGINE = MergeTree
ORDER BY (service, timestamp)التحقق
-- تأكد من أن أنواع الأعمدة تطابق المتوقع
DESCRIBE TABLE events FORMAT Vertical
-- أدرِج بيانات واستعلم للتحقق من أن المخطط يتعامل مع بياناتك
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42}
SELECT service, level, duration_ms FROM events WHERE service = 'api'انتبه إلى
- إذا أدرجت بيانات JSON باستخدام
JSONEachRowوكان JSON يحتوي على حقول غير موجودة في المخطط، فإن ClickHouse يتجاهلها بصمت افتراضيًا. اضبطinput_format_skip_unknown_fieldsعلى0إذا كنت تريد ظهور أخطاء بدلًا من ذلك.
هجين (أعمدة محددة النوع + JSON)
متى يُستخدم: عندما تكون هناك مجموعة أساسية مستقرة من الحقول (الطوابع الزمنية، والمعرّفات، ورموز الحالة)، بينما يكون جزء من الحمولة ديناميكيًا. فكّر في السمات المعرّفة من المستخدم، أو الوسوم، أو البيانات الوصفية، أو حقول الامتداد التي تختلف من سجل إلى آخر.
المفاضلات: تحصل على الأداء الكامل في الأعمدة محددة النوع، مع مرونة في عمود JSON. لكن عمود JSON يظل يضيف عبئًا عند insert وتكلفة تخزين لجزئه الديناميكي.
الإعداد والتحقق والمحاذير
الإعداد
CREATE TABLE events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`level` Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
`message` String,
`host` LowCardinality(String),
`duration_ms` UInt32,
`attributes` JSON(
max_dynamic_paths = 256,
`http.status_code` UInt16,
`http.method` LowCardinality(String),
SKIP REGEXP 'debug\..*'
)
)
ENGINE = MergeTree
ORDER BY (service, timestamp)التحقق
-- أدرج بيانات نموذجية وافحص المسارات المستنتجة
INSERT INTO events FORMAT JSONEachRow
{"timestamp":"2025-03-19 10:00:00","service":"api","level":"INFO","message":"request handled","host":"node-1","duration_ms":42,"attributes":{"http.status_code":200,"http.method":"GET","user.region":"eu-west","custom_tag":"abc"}}
SELECT JSONAllPathsWithTypes(attributes)
FROM events
FORMAT PrettyJSONEachRowانتبه إلى
- استخدم تلميحات الأنواع لمسارات JSON التي تعرفها مسبقًا. تتجاوز هذه التلميحات عمود التمييز وتخزّن المسار مثل أي عمود محدد النوع عادي، بالأداء نفسه ومن دون عبء إضافي.
- استخدم
SKIPأوSKIP REGEXPللمسارات التي لا تستعلم عنها مطلقًا (مثل بيانات debug الوصفية ومعرّفات tracing الداخلية) لتوفير مساحة التخزين وتقليل عدد الأعمدة الفرعية. - اضبط
max_dynamic_pathsبما يتناسب مع عدد المسارات الفريدة التي تستعلم عنها فعليًا. تعمل القيمة الافتراضية (1024) في معظم الحالات. خفّضها إذا كان القسم الديناميكي لديك محدودًا. - لا تضبط
max_dynamic_pathsعلى قيمة تتجاوز 10,000. فالقيم المرتفعة تزيد استهلاك الموارد وتقلل الكفاءة.
عمود JSON الأصلي
متى يُستخدم: عندما تكون البنية غير قابلة للتنبؤ فعلًا، مع مفاتيح تظهر وتختفي بين السجلات. مثل المخططات التي ينشئها المستخدمون، أو أنظمة الملحقات، أو استيعاب البيانات في data lake عندما لا تتحكم في المخطط upstream.
المفاضلات: تكون عمليات الإدراج أبطأ من الأعمدة محددة النوع. كما أن قراءات الكائن الكامل أبطأ من String. وهناك عبء تخزيني إضافي ناتج عن إدارة الأعمدة الفرعية. لكنه يعمل جيدًا مع الاستعلامات على مستوى الحقول لمسارات محددة.
الإعداد والتحقق والمحاذير
الإعداد
CREATE TABLE dynamic_events
(
`id` UInt64,
`ts` DateTime DEFAULT now(),
`data` JSON(
max_dynamic_paths = 512,
`event_type` LowCardinality(String),
`version` UInt8
)
)
ENGINE = MergeTree
ORDER BY (data.event_type, ts)استخدم تنسيق JSONAsObject عند إدراج مستندات JSON كاملة في عمود JSON. فهو يتعامل مع كل سطر إدخال على أنه كائن JSON كامل يُربَط بالعمود.
التحقق
INSERT INTO dynamic_events (id, data) FORMAT JSONEachRow
{"id": 1, "data": {"event_type": "click", "version": 2, "page": "/home", "button_id": "cta-1"}}
{"id": 2, "data": {"event_type": "purchase", "version": 1, "item_id": "SKU-99", "amount": 49.99, "currency": "USD"}}
-- تحقّق من المسارات التي اكتشفها ClickHouse وأنواعها
SELECT JSONAllPathsWithTypes(data) FROM dynamic_events FORMAT PrettyJSONEachRow
-- الاستعلام عن مسار محدد
SELECT data.page FROM dynamic_events WHERE data.event_type = 'click'انتبه إلى
- من دون تلميحات للأنواع، يستنتج ClickHouse الأنواع لكل مسار استنادًا إلى أول القيم التي يراها. فإذا وصلت
scoreعلى شكل"10"(سلسلة نصية) في سجل، و10(عدد صحيح) في سجل آخر، فسيُنشئ المسار discriminator column وتصبح الاستعلامات أبطأ. أضف تلميحات للمسارات ذات الأنواع المعروفة. - عندما يتجاوز عدد المسارات
max_dynamic_paths، تنتقل قيم overflow إلى بنية بيانات مشتركة مع تراجع في query performance. راقب ذلك باستخدامJSONDynamicPaths()، وأبقِ الحد أقل من 10,000. - يدعم كل مسار ديناميكي حتى
max_dynamic_types(القيمة الافتراضية 32) من data types المميزة. وإذا تجاوز مسار واحد هذا الحد، فستعود الأنواع الإضافية تلقائيًا إلى تخزين متغير مشترك. ونادرًا ما يكون هذا مهمًا إلا إذا كانت بياناتك تحتوي على أنواع شديدة التباين للحقل نفسه.
تخزين String غير مُفسَّر
متى يُستخدم: تُخزَّن مستندات JSON وتُسترجَع كاملةً، ثم تُمرَّر إلى تطبيق، أو تُؤرشف، أو تُوجَّه إلى أنظمة لاحقة. لا توجد تصفية على مستوى الحقول أو تجميع داخل ClickHouse.
المفاضلات: أسرع عمليات إدراج وأبسط مخطط. لا توجد استعلامات على مستوى الحقول من دون تحليل وقت التشغيل (عائلة JSONExtract)، وهذا يكون بطيئًا عند التوسّع.
الإعداد والتحقق والمحاذير
الإعداد
CREATE TABLE raw_events
(
`id` UInt64,
`received` DateTime DEFAULT now(),
`payload` String
)
ENGINE = MergeTree
ORDER BY (received)التحقق
INSERT INTO raw_events (id, payload) VALUES
(1, '{"type":"click","page":"/home"}'),
(2, '{"type":"purchase","item":"SKU-99","amount":49.99}')
-- تأكيد سلامة البيانات بعد الكتابة ثم القراءة
SELECT payload FROM raw_events WHERE id = 1
-- التحقق من أنه لا يزال بإمكانك تحليل الحقول عند الحاجة بشكل مخصص
SELECT JSONExtractString(payload, 'type') AS event_type FROM raw_eventsانتبه إلى
- إذا تغيّرت المتطلبات واحتجت لاحقًا إلى استعلامات على مستوى الحقول، فستحتاج إلى إنشاء table جديدة تضم columns محددة النوع أو JSON columns، ثم backfill للبيانات. إذا كان هناك أي احتمال لأن تستعلم عن حقول منفردة، فابدأ باستخدام النهج الهجين بدلًا من ذلك.
- تُحلِّل دوال
JSONExtractالسلسلة في كل query. هذا مقبول للاستكشاف المخصص، لكنه غير مناسب لـ dashboards في بيئة production أو workloads ذات QPS مرتفع. - فكّر في استخدام codecs للضغط (
ZSTD) على عمود String إذا كانت payloads الخاصة بـ JSON كبيرة، إذ يضغطها بكفاءة.
المقارنة
| المعيار | الأعمدة محددة النوع | هجين | JSON أصلي | String |
|---|---|---|---|---|
| سرعة الإدراج | الأسرع | سريع | متوسطة | الأسرع |
| الاستعلامات على مستوى الحقول | الأسرع | سريعة (محددة النوع)؛ جيدة (JSON مع hint) |
جيدة (مع hint)؛ أبطأ (Dynamic) |
بطيئة (التحليل أثناء وقت التشغيل) |
| قراءة الكائن بالكامل | سريعة | متوسطة | بطيئة | الأسرع |
| كفاءة التخزين | الأفضل | جيدة | متوسطة | جيدة (ينضغط بكفاءة) |
| مرونة المخطط | لا توجد (ALTER TABLE) |
جزئية (نواة ثابتة، وذيل مرن) | كاملة | كاملة |
| التعقيد | منخفض | متوسط | متوسط–مرتفع | منخفض |
متى يكون Map خيارًا أفضل
إذا كانت حقولك الديناميكية تتكوّن من أزواج مفتاح-قيمة متجانسة — أي إن جميع القيم من النوع نفسه — فإن Map(String, T) أبسط وأكثر كفاءة من عمود JSON. ومن الأمثلة الشائعة: الوسوم النصية (Map(String, String))، والمقاييس العددية (Map(String, Float64))، أو مفاتيح تفعيل الميزات (Map(String, Bool)).
CREATE TABLE tagged_events
(
`timestamp` DateTime,
`service` LowCardinality(String),
`tags` Map(String, String) -- e.g., {"env": "prod", "region": "us-east-1", "team": "platform"}
)
ENGINE = MergeTree
ORDER BY (service, timestamp)يدعم Map التصفية على مستوى المفاتيح (tags['env'] = 'prod')، وتكلفة تخزينه أقل من JSON، كما يتجنب الكلفة الإضافية للأعمدة الفرعية في نوع JSON. لاحظ أن عمليات البحث عن المفاتيح تفحص الخريطة خطيًا افتراضيًا — وهذا مناسب لمجموعات الوسوم الصغيرة، ولكن إذا كانت خرائط Map تحتوي على أكثر من 100 مفتاح، ففكّر في استخدام تسلسل with_buckets. استخدم JSON عندما تكون القيم من أنواع مختلطة أو عندما تكون البنية متداخلة — واستخدم Map عندما تكون البيانات أزواج مفتاح-قيمة مسطحة ذات نوع قيم موحّد.
- استخدم JSON حيثما كان مناسبًا — متى تستخدم نوع عمود JSON بدلًا من الخيارات الأخرى
- مرجع نوع بيانات JSON — الصياغة الكاملة لتلميحات الأنواع، وSKIP، وmax_dynamic_paths، ودوال الاستبطان
- اختيار أنواع البيانات — إرشادات عامة لاختيار النوع المناسب
- A New Powerful JSON Data Type for ClickHouse — شرح متعمق لمعمارية تخزين نوع JSON
- مرجع تنسيقات JSON — تنسيقات الإدخال/الإخراج لبيانات JSON (JSONEachRow وJSONAsObject وما إلى ذلك)