bodyالخاص بحزمDataوTotalsوExtremesوLogوProfileEventsفي بروتوكول native TCP (حزمةTableColumnsليست كتلة Native — إذ تحمل سلسلتين ثنائيتين، لذا يندرج تخطيطها ضمن مواصفات native protocol)؛- ناتج
SELECT ... FORMAT Nativeعبر HTTP؛ - تصديرات الملفات المكتوبة باستخدام
INTO OUTFILE ... FORMAT Native؛ - حمولات النسخ المتماثل بين الخوادم.
نظرة عامة
data الخاص بالعمود وفقًا لـ العائلة التي ينتمي إليها نوعه. والعائلات، بترتيب تصاعدي من حيث تعقيد فك الترميز، هي:
- الأنواع Fixed-width ترتّب
dataعلى شكل بايتات خام بحجمbytes_per_value × num_rows، من دون أي تأطير لكل صف. - الأنواع المركّبة (
Nullable,Array,Tuple,Map,Nested) لها بنية متكررة يمكن اشتقاقها بالكامل من سلسلة النوع، من دون بادئة إصدار ومن دون حالة ممتدة عبر الكتل. - الأنواع المُرقَّمة بالإصدار / ذات الحالة (
LowCardinality,JSON,Variant,Dynamic) تبدأ كل كتلة غير فارغة ببادئة إصدار/حالة للتسلسل. وعبر بنيةNativeعلى السلك، تكون هذه البادئة وأي قاموس لكل block — إذ لا تحمل الصيغة أي حالة عبر الكتل (تنشئ جهة الكتابة حالة تسلسل جديدة لكل block وتضبطlow_cardinality_max_dictionary_size = 0). أما الحالة عبر الكتل فهي مسألة تخص تخزين MergeTree على القرص، وليست جزءًا من البنية على السلك في Native.
البدائيات على مستوى التمثيل الثنائي
VarUInt
1 عندما تتبعها بايتات أخرى و0 في البايت الأخير.
ترميز القيمة
300:
0xAC 0x02:
الأعداد الصحيحة ثابتة العرض
على سبيل المثال، تُرمَّز القيمة UInt32
1 على الشكل 01 00 00 00، وتُرمَّز القيمة Int32 -1 على الشكل FF FF FF FF.
String
0x00، وقد تحتوي السلاسل على أي قيم بايت، بما في ذلك NUL المضمَّن. تُرمَّز السلسلة "ab" على شكل 02 61 62؛ ولفك ترميزها، اقرأ طول VarUInt (2)، ثم اقرأ هذا العدد من البايتات.
Bool
0x00 إلى false؛ وأي قيمة غير صفرية تشير إلى true (وقياسيًا 0x01).
بنية Block والعمود
البنية السلكية لـ Block
BlockInfo على القناة، لأن الـ writer مُعامَل وسيطيًا وفق revision:
-
في native TCP protocol، يكتب الخادم blocks وفق الـ revision المتفاوض عليه للاتصال (وهي قيمة كبيرة — إذ إن
DBMS_TCP_PROTOCOL_VERSIONتساوي54485في هذا الإصدار). وتُكتبBlockInfoكلما كانت هذه الـ revision أكبر من صفر، وهذا ما يحدث دائمًا في الاتصال الفعلي. كما يُكتب البايتhas_custom_serializationفي كل column (انظر wire layout للأعمدة) عند revision54454فما فوق. -
إن output format
Native— SELECT ... FORMAT Nativeعبر HTTP، وINTO OUTFILE ... FORMAT Native، وformatNativeالذي ينتجهclickhouse-client— يُسلسِل عند revision0افتراضيًا. وعند revision0تُحذف كلٌّ من البادئةBlockInfoوالبايتhas_custom_serialization، لذا تكون block مجردnum_columnsوnum_rowsوالأعمدة. عبر HTTP، لا تكون هذه الـ revision ثابتة: إذ يمكن للعميل رفعها باستخدام query parameter ?client_protocol_version=<n>، ويستخدم الخادم تلك القيمة بوصفها revision التسلسل للاستجابة. ومع قيمة مرتفعة بما يكفي، يتضمن خرج HTTP البادئةBlockInfo(تُكتب كلما كانت الـ revision أكبر من0) والبايتhas_custom_serialization(يُكتب عند revision54454فما فوق)، تمامًا كما في مسار TCP. لذلك يجب ألا يفترض العملاء أن كل payload منFORMAT Nativeعبر HTTP تكون عند revision0.
BlockInfo تصف payload لحزمة Data عبر TCP. أما query نفسها عند تنفيذها عبر FORMAT Native فتنتج الصيغة الأقصر المعروضة إلى جانبها.
BlockInfo
0. إن wire format ليس ذاتي الوصف: فمعرّف الحقل لا يشفّر طول قيمته ولا نوعها، لذلك يجب أن يعرف القارئ مسبقًا نوع كل معرّف حقل قد يصادفه. ويعتبر قارئ ClickHouse نفسه أي معرّف حقل غير معروف تلفًا، ويُطلق استثناء (UNKNOWN_BLOCK_INFO_FIELD). أما التوافق الأمامي فيُعالَج بدلًا من ذلك عبر مراجعة البروتوكول: إذ لا يكتب المرسِل حقلًا إلا إذا كانت المراجعة المتفاوض عليها تساوي على الأقل الحد الأدنى لمراجعة ذلك الحقل، وبذلك لا يرى المستقبِل الأقدم حقلًا لا يعرفه.
للحقليْن
1 و2 حد أدنى للمراجعة مقداره 0، لذا فهما موجودان كلما كُتب BlockInfo أصلًا. ولا يُكتب الحقل 3 إلا عند المراجعة 54480 فما فوق. تخطيط wire layout للحالة الشائعة (عندما تكون المراجعة أقل من 54480):
بنية العمود على مستوى wire
num_columns مرة داخل Block.
يعتمد مفكِّك الترميز على سلسلة
type. وغالبًا ما تتضمن سلاسل الأنواع معاملات بين قوسين؛ لذا يزيل مفكِّك الترميز اللاحقة (...) لتحديد النوع الأساسي، ثم يحلّل المعاملات لاتخاذ قرارات تتعلق بالحجم أو scale أو النوع الداخلي. ويتطلب parsing قائمة معاملات تحتوي على أنواع متداخلة (مثل Tuple داخل Array) مقسِّمًا للفواصل يراعي العمق ويتتبع تداخل الأقواس، بدلًا من تقسيم بسيط عند ,.
ترميز النوع الثنائييكون الحقل
type عبارة عن String نصية فقط في الوضع الافتراضي. وعند ضبط إعداد query output_format_native_encode_types_in_binary_format = 1، يصبح هذا الحقل بدلًا من ذلك ترميز نوع ثنائي — وهو الترميز نفسه القائم على الوسوم والمُوثَّق في الترميز الثنائي لأنواع البيانات — كما تستخدم قوائم نوع Dynamic المسطحة الترميز الثنائي نفسه لأسماء الأنواع الخاصة بها لكل نوع. وإذا كان مفكِّك الترميز يقرأ الحقل 2 دائمًا على أنه سلسلة مسبوقة بالطول، فسيتعامل مع أول وسم نوع ثنائي كما لو كان طول سلسلة، ويفقد التزامن؛ لذا يجب أن يعرف أي وضع يستخدمه stream.kind_stack والترميز المتناثر
kind_stack تسلسلًا غير افتراضي خاصًا بكل عمود:
تستخدم حمولة
COMBINATION تعدادًا مختلفًا. الصفوف الخمسة أعلاه هي رموز مضغوطة من بايت واحد. ويمثّل COMBINATION (0x05) آلية الهروب العامة لأي مكدس لا تغطيه هذه الرموز: إذ يتبعه count من نوع VarUInt ثم count من الإدخالات ذات البايت الواحد. وهذه الإدخالات ليست الرموز المضغوطة الواردة في الجدول، بل هي قيم ISerialization::Kind الخام:
تختلف قيم البايت عن الرموز المضغوطة: إذ تكون
REPLICATED هي 0x03 في هذا التعداد المتداخل، لكنها 0x04 كرمز مضغوط، ولا يوجد إدخال DETACHED_OVER_SPARSE — بل يظهر هذا التركيب على هيئة الإدخالين المتتاليين SPARSE وDETACHED. وأي decoder يواصل استخدام الجدول المضغوط للبايتات المتداخلة سيُسنِد 0x03/0x04 على نحو خاطئ ويفقد التزامنه.
إن count هو طول المكدس الكامل بما في ذلك الإدخال DEFAULT الأول الذي تبدأ به كل المكدسات. وتغطي الرموز المضغوطة بالفعل كل مكدس مكوَّن من إدخال واحد أو إدخالين، لذا يكون لدى COMBINATION دائمًا count لا يقل عن ثلاثة.
kind_stack تكراري لأعمدة Tuple. تمثل حمولة kind_stack أعلاه البايت (أو تسلسل COMBINATION) الخاص بمعلومات التسلسل الذاتية لعمود واحد. ويحمل Tuple كائن SerializationInfoTuple، الذي يكتب أولًا حمولة مكدس النوع الخاصة بالـ tuple نفسه ثم يكتب حمولة مكدس نوع كاملة لكل عنصر، بالترتيب؛ ويقرأ decoder البنية التكرارية نفسها عند الاسترجاع. لذلك، بالنسبة إلى Tuple(A, B, C) تكون بايتات الحقل 4 هي [tuple_kind][A_kind][B_kind][C_kind]، وتكون حمولة كل عنصر تكرارية بدورها إذا كان ذلك العنصر مركبًا أيضًا. ويُضبط البايت has_custom_serialization (الحقل 3) كلما كانت معلومات الـ tuple نفسه أو معلومات أي عنصر فيه غير افتراضية، لذلك فإن Tuple الذي يكون عنصره الخاص الوحيد متناثرًا أو replicated أو detached سيؤدي أيضًا إلى تضمين حمولة مكدس النوع. وأي decoder يقرأ فقط بايت التعداد الأحادي الأول لـ Tuple سيتوقف مبكرًا جدًا وسيقرأ بايتات نوع العناصر المتبقية على أنها بيانات عمود.
تنسيق wire المتناثر. عندما تكون kind_stack = 0x01، تكون data الخاصة بالعمود عبارة عن تيارين مكتوبين الواحد تلو الآخر ضمن تيار TCP مشترك واحد:
- تيار الإزاحات — سلسلة من قيم
VarUInt. وتكون كل قيمةvإحدى الحالتين:- القيمة
vمع كون البت العالي عند الموضع 62 غير مضبوط:(v & 0x3FFFFFFFFFFFFFFF)= عدد المواضع الافتراضية قبل القيمة غير الافتراضية الصريحة التالية. ويكون هذا الموضع غير الافتراضي هوcursor + group_size، حيثcursorهو الموضع الجاري؛ وبعد ذلك يتقدمcursorبمقدارgroup_size + 1. - القيمة
vمع ضبط البت 62 (END_OF_GRANULE_FLAG): تكون القيمة بعد إزالة العلامة هي عدد المواضع الافتراضية اللاحقة بعد آخر قيمة غير افتراضية. ويشير ذلك إلى نهاية تيار الإزاحات للـ block.
- القيمة
- تيار القيم —
countمن القيم غير الافتراضية مرمّزة بكثافة في النوع الداخلي، حيث إنcountهو عدد قيمVarUIntغير EOG المقروءة أعلاه.
num_rows، وذلك بملء كل موضع غير مصرّح به بالقيمة الافتراضية للنوع الداخلي (0 للأعداد الصحيحة وأعداد الفاصلة العائمة، و"" لـ String، و0 يومًا لـ Date، وهكذا).
يُعد العمود المتناثر Nullable(T) حالة خاصة، لأن القيمة الافتراضية لـ Nullable(T) هي NULL. ويحذف الترميز المتناثر بالكامل تدفق خريطة القيم الخالية المعتاد لـ Nullable: إذ يحدّد تدفق الإزاحة المواضع غير الافتراضية — أي المواضع غير NULL — بينما لا يحتوي تدفق القيم إلا على تلك القيم غير NULL بشكل كثيف في T، ويُعاد بناء كل موضع غير مصرّح به على أنه NULL. لذلك يجب على مفكّك الترميز ألا يبحث عن خريطة null في تدفق القيم، ويجب ألا يملأ الفجوات بقيمة 0 فعلية؛ بل يملؤها بـ NULL.
wire format لـ Replicated. عندما تكون kind_stack = 0x04، يكون العمود data قاموسًا: قائمة بقيم عناصر مميّزة، بالإضافة إلى فهرس لكل صف ضمن تلك القائمة (وهو نفس نمط lookup المستخدم في LowCardinality). وعندما يكون النوع الداخلي نفسه ذا إصدار — مثل LowCardinality(T) — تُكتب state prefix الخاصة به أولًا، قبل تدفق الفهرس: إذ يفوّض التسلسل Replicated مرحلة prefix phase إلى النوع الداخلي قبل كتابة num_rows. أما الأنواع الداخلية ذات البادئة الفارغة (الأنواع الطرفية والمركّبات العادية) فلا تضيف أي بايتات هنا.
elements[indexes[i]] لكل صف إخراج i. وتُعالَج الأنواع الداخلية المركبة بشكل تكراري: تُنشأ قائمة العناصر في النوع الداخلي أولاً، ثم تُفهرَس. وتشمل الأنواع الداخلية المدعومة الأنواع الطرفية، وNullable(T)، وArray(T)، وTuple(...)، وMap(K, V)، وNested(...) (مع توسيع كل حقل كما في Array)، وLowCardinality(T) (يُحتفَظ بالقاموس المشترك؛ ولا تُفهرَس إلا المفاتيح الخاصة بكل عنصر).
تنسيق wire المنفصل. يظهر كلٌّ من DETACHED (0x02) وDETACHED_OVER_SPARSE (0x03) فعلاً على wire — فهما ليسا داخليَّين بحتًا. في مسار TCP، عندما يكون الضغط ممكّنًا ويكون revision المتفاوض عليه على الأقل DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING (v54478)، يمر العمود بثلاث خطوات:
- يُغلَّف كل عمود مؤهَّل (غير
const، وغيرTuple، وفي block يحتوي على أكثر من صف واحد) داخلColumnBLOB، الذي يحتفظ بالعمود بعد أن جرت له عملية marshalling وضغطه مسبقًا خارج thread الرئيسي. - يُضاف
DETACHEDإلى مكدس kind للعمود المُغلَّف. - تُكتَب
dataالخاصة بالعمود على هيئة حجم blob من نوعVarUInt، متبوعًا بعدد مطابق تمامًا من بايتات blob.
{DEFAULT, SPARSE, DETACHED}، ويُسلسَل على هيئة DETACHED_OVER_SPARSE. ويقرأ client الذي يفك ترميز مثل هذا العمود طول blob وبايتاته، ثم يفك ضغط blob لاستعادة payload العمود الداخلي (راجع ملاحظة ColumnBLOB ضمن الضغط).
متغيرات الكتلة
أمثلة على مستوى البايت
BlockInfo والبايت has_custom_serialization. أما في FORMAT Native فتكون الكتل نفسها أقصر — ويُذكر الشكل المختصر المكافئ حيثما كان ذلك مفيدًا.
كتلة فارغة (مع BlockInfo)، بإجمالي 8 بايت:
SELECT 1 عن عمود واحد باسم "1" من النوع UInt8، وبعدد صفوف يساوي صفرًا. في البروتوكول ≥ 54454، يُضمَّن البايت has_custom_serialization:
FORMAT Native (المراجعة 0)، لا تتضمن كتلة النتيجة نفسها BlockInfo ولا البايت has_custom_serialization — ويبلغ حجم SELECT 1 FORMAT Native 11 بايت:
FORMAT Native: إذ إن تنسيق الإخراج لا يُصدر كتلًا فارغة.)
أنواع البيانات
data الخاصة بالعمود، وهي مُجمَّعة في أربع عائلات تتزايد فيها درجة تعقيد وحدة فك الترميز. وهناك نوعان — AggregateFunction(func, ...) و QBit(T, N) — صالحان كأنواع أعمدة Native، لكن لهما حمولات خاصة بالدالة أو بالنوع تخرج عن نطاق هذا القسم؛ ويُشار إليهما أدناه في المواضع التي قد يُلتبس فيها أمرهما ويُظن أنهما أسماء مستعارة.
الأنواع ذات العرض الثابت
M صفًا مقدار bytes_per_row × M بايتًا بالضبط على السلك، متسلسلةً من دون فواصل أو حشو.
أنواع الأعداد الصحيحة
UInt8–UInt256 وInt8–Int256 ترميزًا ثنائيًا مباشرًا لقيم الأعداد الصحيحة. تقرأ وحدة فك الترميز bytes_per_row × num_rows بايتًا وتفسّرها وفقًا للنوع.
عمود UInt32 يحتوي على [1, 256, 65536]:
Int32 يحتوي على [-1, 42]:
Float32 وFloat64
binary32) و8 بايتات بدقة مزدوجة (binary64)، وكلٌّ منها بترتيب little-endian. وتُحفَظ NaN و±Infinity و±0.0 والقيم دون المعيارية وتُستعاد كما هي دون تطبيع.
قيمة Float32 1.5 (0x3FC00000):
Float64 1.5 (0x3FF8000000000000):
BFloat16
Float32 وفق معيار IEEE 754 — بت إشارة واحد، و8 بتات للأسّ، و7 بتات للمانتيسا. حجم كل قيمة 2 بايت، بترتيب little-endian، وتحتفظ بالنمط الخام المكوَّن من 16 بتًا. لاستعادة القيمة الرقمية، وسِّعها مرة أخرى إلى Float32 بوضع النمط في النصف العلوي وتصفير النصف السفلي (أي إعادة تفسير bits << 16 على أنه Float32)؛ وعندها تستخدم القيمة الموسَّعة التنسيق النصي نفسه الخاص بـ Float32.
قيمة BFloat16 وهي 1.5 (النمط 0x3FC0، وهو النصف العلوي من Float32 0x3FC00000):
Bool
UInt8: بايت واحد لكل صف، 0x00 = false، 0x01 = true. وسلسلة النوع على مستوى النقل هي حرفيًا Bool (وليست UInt8)، لذلك يجب أن يتعرّف عليها decoder الذي يوجّه المعالجة بناءً على سلسلة النوع بشكل منفصل.
عمود Bool [true, false, true]:
Date و Date32
1970-01-01. ولا يتضمن أيٌّ منهما مكوّنًا للوقت.
قيمة
Date 1970-01-02 (يوم واحد):
Date32 وهي 1900-01-01 (-25567 يومًا):
DateTime
UInt32: طابع زمني Unix بالثواني، 4 بايتات بترتيب little-endian. قد يظهر النوع بالشكل DateTime أو DateTime('Timezone')؛ وتؤثر المنطقة الزمنية في العرض فقط، وليست جزءًا من القيمة الثنائية. يُنتج عمودا DateTime ذوا معامِلَي منطقة زمنية مختلفين بايتات متطابقة لنفس اللحظة. تزيل وحدة فك الترميز لاحقة المعامل (...) وتعالج العمود على أنه UInt32.
قيمة DateTime('UTC') 2024-03-15 14:30:00 UTC (الطابع الزمني 1710513000):
DateTime64(scale[, timezone])
Int64 بترتيب little-endian يمثّل وحدات tick بمقياس 10^-scale ثانية منذ حقبة Unix. تكون المَعلمة scale (0–9) ضمن سلسلة النوع وتحدّد وحدة الوقت:
يظهر النوع بالشكل
DateTime64(s) (المنطقة الزمنية الافتراضية الضمنية للخادم) أو DateTime64(s, 'TimezoneName') (منطقة زمنية صريحة، للعرض فقط). وتمثّل القيم السالبة وحدات tick التي تسبق الحقبة.
قيمة DateTime64(3, 'UTC') هي 2024-01-15 12:30:45.123 UTC (1705321845123 ms):
DateTime64(0) هي 2024-01-15 12:30:45 UTC (1705321845 s):
Time و Time64(scale)
Time هو عدد ثوانٍ موقَّع، 4 بايت من نوع Int32 وبترتيب little-endian؛ أما Time64(scale) فهو عدد ticks موقَّع وفق المقياس العشري المحدد (0–9)، 8 بايت من نوع Int64 وبترتيب little-endian — وله نفس بنية wire مثل DateTime64.
الصيغة النصية هي [-]HH:MM:SS[.fraction]، ولكن بخلاف DateTime فإن حقل الساعات لا يلتف ضمن يوم من 24 ساعة: بل يمثل إجمالي عدد الساعات، وقد يتجاوز 23. ويُحدَّد المقدار المعروض بحد أقصى قدره 999:59:59 (3599999 ثانية)؛ وأي مقدار أكبر يُعرَض عند هذا الحد مع تصفير الجزء الكسري (999:59:59.000). كما يقيِّد CAST القيمة المخزَّنة بهذا النطاق أيضًا، رغم أن العمليات الحسابية قد تنتج قيمًا خارج النطاق لا تُقيَّد إلا عند العرض. ولا يؤثر أيٌّ من ذلك في بايتات wire، إذ تكون مجرد عدد صحيح موقَّع عادي.
قيمة Time وهي 45296 (12:34:56):
Time64(3) بالقيمة 45296789 وحدة زمن (12:34:56.789):
Time وTime64 تجريبيتان، ويتطلبان تفعيل allow_experimental_time_time64_type = 1 على الخادم.Interval
Interval<Unit> — IntervalSecond وIntervalMinute وIntervalHour وIntervalDay وIntervalWeek وIntervalMonth وIntervalQuarter وIntervalYear وIntervalNanosecond وما إلى ذلك. تشترك جميع الوحدات في ترميز wire واحد: العدد على شكل Int64 موقّع من 8 بايتات بترتيب little-endian. تكون الوحدة موجودة فقط في سلسلة النوع — فهي لا تغيّر بايتات wire ولا الصيغة النصية، التي تكون مجرد عدد صحيح. ويتولى مسار فك ترميز واحد التعامل مع جميع الوحدات.
قيمة IntervalDay 5:
UUID
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx، حيث تُكتب البايتات تقليديًا بتنسيق big-endian. يأخذ نموذج wire هذه البايتات القياسية الستة عشر، ويقسّمها إلى نصفين من 8 بايتات، ثم يكتب كل نصف بتنسيق little-endian:
- بايتات Wire من 0..7 = البايتات القياسية من 0..7 بعد عكس ترتيبها.
- بايتات Wire من 8..15 = البايتات القياسية من 8..15 بعد عكس ترتيبها.
550e8400-e29b-41d4-a716-446655440000:
IPv4 وIPv6
IPv4 حجمه 4 بايتات، ويُرمَّز كقيمة UInt32 بترتيب little-endian تحمل العنوان المعياري ذي 32 بت (القيمة (a << 24) | (b << 16) | (c << 8) | d المشتقة من a.b.c.d). أما بايتات wire فهي بايتات ترتيب الشبكة ولكن معكوسة.
192.168.1.10 (القيمة المعيارية ذات 32 بت 0xC0A8010A):
IPv6 طوله 16 بايتًا، ويُكتب كما هو بترتيب البايتات على الشبكة من دون أي تبديل — وهو نفس ترتيب البايتات المستخدم في inet_pton(AF_INET6, ...).
2001:db8::1:
u32 لإجراء العمليات الحسابية وتنفيذ استعلامات النطاق بكفاءة، بينما يحتفظ IPv6 بتنسيق ترتيب الشبكة الشائع في معظم واجهات برمجة تطبيقات الشبكات.
Enum8 and Enum16
Int8 وInt16 على الترتيب: 1 أو 2 بايت لكل صف، وبترتيب بايتات little-endian وفق متممة الاثنين للمتغير ذي 16 بت. يوجد التعيين الكامل للقيم في سلسلة النوع:
(...) وتتعامل معها على أنها Int8 / Int16 — إذ إن بايتات wire ليست سوى فهرس عدد صحيح. أما العميل الذي يعرض التسمية فيحلّل خريطة 'name' = value من سلسلة النوع ويحتفظ بها إلى جانب العمود: فالعدد الصحيح وحده لا يكفي لاستعادة التسمية. وتعرض المخرجات النصية التسمية (active) بدلًا من الفهرس، وتكون بين علامتَي اقتباس مفردتَين ('active') عندما يكون enum متداخلًا داخل نوع مركّب. ولأن الخريطة لا يمكن استعادتها من عمود العدد الصحيح، فيجب الاحتفاظ بها مع قيم enum المتداخلة مثل Array(Enum8(...)) أو Map(Enum16(...), V).
عمود Enum8('active' = 1, 'inactive' = 2) بالقيم [active, inactive, active]:
30000 من النوع Enum16(...):
Decimal(P, S)
P؛ أما المقياس S فهو الأس السالب (أي عدد الخانات بعد الفاصلة العشرية). وكلاهما موجود في سلسلة النوع.
ترميز
wire هو العدد الصحيح الأساسي بترتيب little-endian وبصيغة مكمّل اثنين، وتكون القيمة العشرية المنطقية هي wire_integer × 10^(-S).
يُخرج ClickHouse دائمًا Decimal(P, S) بغضّ النظر عن كيفية التصريح عن النوع. فجميع الصيغ مثل Decimal32(S) وDecimal64(S) وما إلى ذلك تُحوَّل إلى Decimal(P, S) على wire (مع ضبط P على الحد الأقصى الطبيعي لذلك العرض: 9 و18 و38 و76). وأي decoder يتعرّف فقط على Decimal(P, S) سيغطي جميع الصيغ التي يُخرجها الخادم.
القيمة 123.4567 من النوع Decimal(9, 4) → العدد الصحيح الأساسي 1234567:
-1.5 من النوع Decimal(18, 1) → العدد الصحيح المساند -15:
Decimal(38, 4) بقيمة 123.4567 (إجمالي 16 بايت):
Nothing
Nothing لا يحمل أي قيم. وعمليًا، لا يظهر إلا بوصفه النوع الداخلي لـ Nullable(Nothing) — وهو ما يعيده الخادم لتعبير مثل SELECT NULL، حيث تكون القيمة الصحيحة الوحيدة هي غياب القيمة. ومن الناحية المفاهيمية، يُعدّ نوع وحدة.
على مستوى النقل، يشغل بايتًا نائبًا واحدًا لكل صف تمامًا. ويُخرج الخادم المحرف ASCII '0' (0x30)، لكن أداة فك التسلسل تتجاهل هذه البايتات — فالمحتوى غير معرّف، ويجب ألا تعتمد أدوات فك الترميز على أي قيمة محددة. وعدد البايتات المكتوبة هو num_rows × 1، لذا يحدد num_rows في ترويسة العمود بالكامل مقدار ما يجب استهلاكه.
ويبقي هذا البايت لكل صف على ثبات Block: فلكل عمود طول يمكن اشتقاقه من num_rows، لذلك تفحص أدوات فك الترميز البيانات إلى الأمام من دون بادئات طول لكل خلية. ويُبلغ Nullable المحيط دائمًا عن كل موضع على أنه NULL، لذا لا تُفحص هذه البايتات النائبة أبدًا.
عمود Nullable(Nothing) يحتوي على 3 صفوف (كلها NULL):
Nullable (راجع Nullable)؛ أما البايتات الثلاثة الداخلية فهي حمولة Nothing التي يتجاوزها مفكِّك الترميز.
الأنواع ذات الطول المتغير
String
String. عمود String هو تتابع من num_rows من تسلسلات البايت المسبوقة بطولها:
0x00. تكون String في ClickHouse موجَّهة للبايتات لا للنص: فلا يُفرض التحقق من صحة UTF-8، وقد تحتوي القيمة على أي بايتات، بما في ذلك NUL المضمَّن. وأداة فك الترميز التي تستهدف نوع سلسلة UTF-8 إمّا أن تتحقق من الصحة عند القراءة أو تتيح البايتات الخام للمستدعي. إجمالي البايتات التي يستهلكها العمود هو Σ (varuint_size(len_i) + len_i) على امتداد جميع الصفوف.
عمود يضم 3 سلاسل ["ab", "", "c"] (إجمالي 6 بايتات):
FixedString(N)
FixedString(N)، حيث إن N عدد صحيح موجب (على سبيل المثال، FixedString(16)). يكون العمود عبارة عن N × num_rows من البايتات الخام بالضبط، من دون بادئات طول ومن دون فواصل. ويستخرج مفكِّك الترميز القيمة N من سلسلة النوع ويستهلك هذا العدد من البايتات لكل صف.
عندما تُدرِج عبارة SQL قيمة أقصر من N بايتًا (على سبيل المثال، CAST('abc' AS FixedString(5)))، يُجري الخادم حشوًا إلى اليمين ببايتات NUL (0x00) حتى الطول المُعلَن. وتُعد بايتات الحشو هذه جزءًا من القيمة المخزَّنة وتُرسَل على الـ wire كما هي؛ أما إزالة هذا الحشو فهي من اختصاص جهة العميل. وكما هو الحال مع String، فإن FixedString(N) أقرب إلى مصفوفة بايتات منه إلى نص — ويُستخدم عادةً للمعرّفات ثابتة العرض، أو بايتات العناوين، أو بصمات hash.
القيمتان التاليتان من FixedString(3) هما ["abc", "de\0"] (إجمالي 6 بايتات):
الأنواع المركبة
- بنية ثابتة لكل schema. يتحدد التركيب بالكامل بواسطة type string وقت فك الترميز. ويكون لـ
Array(UInt32)دائمًا تخطيط التدفّقات نفسه من block إلى آخر. - لا تملك version prefix خاصًا بها. فالغلاف المركب نفسه لا يضيف أي بايت إصدار؛ كما أن framing الخاص به (
offsetsوnull-mapوتدفّقات العناصر) ثابت عبر إصدارات ClickHouse. وينطبق ذلك على الغلاف فقط — راجع ملاحظة prefix phase أدناه بشأن الأنواع الداخلية ذات الإصدارات. - لا تملك cross-block state خاصًا بها. يكون framing الخاص بالغلاف self-describing بالكامل على مستوى كل block؛ وأي مسألة تتعلق بـ cross-block state تأتي من نوع داخلي ذي إصدار، لا من الغلاف.
SerializationArray prefix phase الخاصة بنوعه الداخلي قبل كتابة array offsets، ويفعل Tuple وMap وNested وNullable الشيء نفسه عبر serializations العناصر الخاصة بها (ويشغّل Nullable الـ prefix الداخلي قبل null map الخاصة به).
لذلك، عندما يغلّف نوع مركب نوعًا ذا إصدار/يحافظ على الحالة (LowCardinality وVariant وDynamic وJSON)، فإن version/state prefix لذلك النوع الداخلي تُخرَج أولًا قبل offsets الخاصة بالغلاف وelement payload. فعلى سبيل المثال، يكون تخطيط Array(LowCardinality(String)) على النحو [LowCardinality state prefix] → [array offsets] → [flattened LowCardinality element payload]، وليس offsets-first.
وأي decoder يقرأ offsets قبل تشغيل inner prefix phase سيفقد التزامن عند التعامل مع أي نوع مركب يحتوي على LowCardinality أو Variant أو Dynamic أو JSON. وعندما يكون كل نوع داخلي مجرد leaf عادي أو نوع مركب آخر غير ذي إصدار، فلن تُخرِج prefix phase أي بايتات، وعندها ينطبق الوصف القائم على offsets-first أدناه حرفيًا.
Nullable(T)
Nullable(InnerType). أمثلة: Nullable(UInt32), Nullable(String), Nullable(FixedString(16)), Nullable(DateTime('UTC')).
مثل الأنواع المركّبة الأخرى، يفوِّض Nullable مرحلة البادئة إلى التسلسل الداخلي الخاص به قبل كتابة خريطة القيم NULL: فعندما يكون النوع الداخلي ذا إصدار، يُرسَل أولًا بادئة الحالة الخاصة بالنوع الداخلي. لذلك يبدأ Nullable(Tuple(LowCardinality(String))) ببادئة الحالة لـ LowCardinality، وليس بخريطة القيم NULL. وعندما يكون النوع الداخلي نوعًا طرفيًا أو نوعًا آخر غير ذي إصدار، لا تُنتج مرحلة البادئة أي بايتات.
يكون تخطيط wire عبارة عن مرحلة البادئة الداخلية (وهي فارغة ما لم يكن النوع الداخلي ذا إصدار)، يتبعها تدفقان متصلان، وتأتي خريطة NULL أولًا:
null-map) من num_rows بايتًا بالضبط، بايت واحد لكل صف:
يحتوي مجرى القيم على الترميز القياسي للنوع الداخلي لجميع صفوف
num_rows كلها، بما في ذلك مواضع القيم الفارغة. ويجب على أداة فك الترميز مع ذلك قراءة بايتات العنصر النائب عند مواضع القيم الفارغة لمتابعة التقدّم في المجرى، لكنها يجب أن ترجع إلى خريطة القيم الفارغة قبل تفسير أي قيمة منفردة. ويجوز للمرسلين كتابة أي بايتات عند مواضع القيم الفارغة، لذلك يجب ألا تعتمد أدوات فك الترميز على قيمة محددة للعنصر النائب.
قيم العنصر النائب حسب فئة النوع الداخلي:
يمكن أن يظهر
Nullable(T) داخل Array وTuple وMap وNested — ويشيع استخدام Array(Nullable(T)) وTuple(Nullable(T1), T2). ولا تقبل القابلية للقيم الفارغة التركيب مع نفسها: يرفض الخادم Nullable(Nullable(T)).
قيمة من النوع Nullable(UInt8) بثلاثة صفوف [5, NULL, 9] (6 بايتات إجمالًا):
Nullable(String) تضم ثلاثة صفوف ["hello", NULL, "world"] (15 بايت إجمالًا):
Array(T)
Array(InnerType). أمثلة: Array(UInt32), Array(String), Array(Nullable(UInt32)), Array(Array(UInt8)).
بنية wire هي مرحلة البادئة للنوع الداخلي (وتكون فارغة ما لم يكن النوع الداخلي ذا إصدار)، تليها سلسلتا تدفق متصلتان، مع الإزاحات أولًا:
num_rows قيمة UInt64 بترتيب little-endian تمامًا، وتمثّل كل قيمة منها موضع النهاية التراكمي في دفق القيم بعد عناصر ذلك الصف:
- فهرس بداية العناصر للصف
N=offsets[N - 1](أو0عندماN == 0). - فهرس نهاية العناصر (حصري) للصف
N=offsets[N]. - عدد عناصر الصف
N=offsets[N] - offsets[N - 1].
offsets[num_rows - 1] هو إجمالي عدد العناصر عبر جميع الصفوف، ويحتوي دفق القيم على هذا العدد من القيم الداخلية متسلسلةً واحدةً تلو الأخرى.
تكون الإزاحات رتيبة غير متناقصة؛ فالإزاحات المتساوية المتتالية تعني صفًا فارغًا، ويجب على مفكِّك الترميز رفض الإزاحات غير الرتيبة باعتبارها تلفًا. أما العمود الفارغ (num_rows == 0) فيكتب صفر بايت — فلا يوجد دفق إزاحات ولا دفق قيم. ويمكن أن تكون الأنواع الداخلية أي نوع، بما في ذلك الأنواع المركبة الأخرى: فجميع Array(Array(T)) و Array(Tuple(...)) و Array(Nullable(T)) صيغ صالحة.
Array(UInt32) مع الصفوف [[10, 20, 30], [], [40, 50]] (44 بايت إجمالًا):
0 للصف 0). وتدل الإزاحتان المتساويتان المتتاليتان على صف فارغ:
Array(String) بالصفوف [["a", "bb"], []] (إجمالي 20 بايتًا):
Array(Array(UInt32)) مع الصفوف [[[1,2]], [], [[3], [4,5]]] يحتوي على البنية المتداخلة نفسها:
- الإزاحات الخارجية:
[1, 1, 3]— الصف 0 يحتوي على مصفوفة داخلية واحدة، والصف 1 لا يحتوي على أي مصفوفة داخلية، والصف 2 يحتوي على 2. - يفك
Array(UInt32)الأوسط ترميز 3 صفوف بإزاحات[2, 3, 5]. - يفك
UInt32الأعمق ترميز 5 قيم:[1, 2, 3, 4, 5].
Tuple(T1, T2, …)
Tuple(T1, T2, ..., Tn). أمثلة: Tuple(UInt32, String), Tuple(Int32), Tuple(Array(UInt32), String), Tuple(UInt8, Tuple(Int32, String)). يدعم ClickHouse أيضًا named tuples عبر Tuple(a UInt32, b String)؛ الأسماء هي metadata فقط ولا تؤثر في wire format.
يتكوّن wire layout من prefix phase الخاصة بالعناصر (يساهم كل عنصر ذي إصدار في state prefix الخاص به، وفق ترتيب التعريف؛ وتكون فارغة للعناصر غير ذات الإصدار)، ثم N streams متسلسلة، stream واحدة لكل element type، وفق ترتيب التعريف:
num_rows. لا توجد بادئة طول، ولا تدفّق إزاحات، ولا فواصل بين التدفقات. يكتب العمود الفارغ (num_rows == 0) صفر بايت لكل تدفّق. ويمكن أن تكون أنواع العناصر من أي نوع، بما في ذلك الأنواع المركبة الأخرى — فجميع الصيغ Tuple(Tuple(...), ...) وTuple(Array(...), ...) وTuple(Nullable(T1), T2) صالحة.
كما أن الـ tuple الخالي من العناصر Tuple() صالح أيضًا — وينشأ من تعبيرات مثل SELECT tuple() أو CAST(x AS Tuple()). وبما أنه لا يحتوي على أي تدفقات للعناصر، فإنه يُسلسَل بدلًا من ذلك مثل Nothing: بايت عنصر نائب واحد (0x30، ASCII '0') لكل صف، وتتجاهله عملية إلغاء التسلسل. ويأتي عدد الصفوف من ترويسة الكتلة، تمامًا كما هو الحال مع Nothing.
Tuple(UInt8, UInt8) مع 3 صفوف (1,4), (2,5), (3,6):
[1, 2, 3] للعنصر 0 و[4, 5, 6] للعنصر 1.
Tuple(UInt32, String) مع صفَّين (10, "a")، (20, "bb") (13 بايتًا إجمالًا):
Map(K, V)
Map(KeyType, ValueType). أمثلة: Map(String, UInt32), Map(String, Array(UInt32)), Map(UInt8, Tuple(Int32, String)), Map(Array(String), Int8). لا يفرض تنسيق wire أي قيود على أيٍّ من النوعين — إذ يمكن أن يكون كلٌّ من K وV أيَّ نوع مدعوم، بما في ذلك الأنواع المركّبة. (اختلفت قواعد ClickHouse على مستوى SQL بشأن أنواع المفاتيح المقبولة عبر الإصدارات؛ راجع وثائق SQL الخاصة بإصدار الخادم المستهدف.)
يتطابق تخطيط wire بايتًا لبايت مع Array(Tuple(K, V))، لذا يبدأ بمرحلة البادئة الداخلية prefix phase (وتكون فارغة ما لم يكن K أو V ذا إصدار):
total_pairs = offsets[num_rows - 1] (أو 0 عندما num_rows == 0). ويملك تدفّق offsets الدلالات نفسها كما في Array. تكون المفاتيح مصطفّة موضعيًا مع القيم: الزوج i هو (keys[i], values[i]).
التمثيل داخل الذاكرة في ClickHouse لعمود من النوع Map هو مصفوفة من Tuple؛ لكن نظام الأنواع يقدّمه كنوع مستقل لتسهيل الاستخدام في SQL (m['key'], mapKeys, mapValues). ويكون wire format عبارة عن serialization مباشر لذلك التخزين، لذا فإن Map و Array(Tuple(K, V)) متكافئان تمامًا على مستوى البايتات.
تكون offsets رتيبة غير متناقصة، ويحتوي كلٌّ من تدفّقي المفاتيح والقيم على total_pairs قيمة بالضبط. لا يكتب العمود الفارغ أي بايتات. وداخل الصف الواحد تكون المفاتيح عادةً فريدة، لكن هذه قاعدة دلالية وليست قاعدة يفرضها wire: إذ يتيح wire format تمرير المفاتيح المكررة ذهابًا وإيابًا كما هي، ولا تُحسم التكرارات وفق دلالات جهة الخادوم إلا عندما يستهلك الصفَّ تابعٌ مدركٌ لـ Map.
Map(UInt8, UInt8) مع صفّين {1:10, 2:20}، {3:30} (22 بايتًا إجمالًا):
i بقراءة keys[i] وvalues[i] معًا.
Map(String, UInt32) مع صف واحد {'a':1, 'b':2} (20 بايت إجمالًا):
Nested(name1 T1, name2 T2, …)
Nested على إعداد flatten_nested على جهة الخادم، ما يؤدي إلى حالتين مختلفتين.
الحالة A: flatten_nested = 1 (إعداد الخادم default). عندما يُنشأ الجدول بالإعدادات الافتراضية، فإن Nested ليس نوعًا على مستوى wire. يخزّن الخادم العمود ويعرضه على شكل N من الأعمدة المتوازية من النوع Array(T_i) ذات الأسماء المنقوطة (outer.field1 وouter.field2 وما إلى ذلك). وعلى مستوى طبقة format، لا يوجد ما هو جديد — فكل عمود منقوط ليس إلا Array عاديًا:
flatten_nested = 0. عند إنشاء الجدول باستخدام flatten_nested = 0، يظهر العمود في التمثيل المنقول كعمود واحد ذي سلسلة نوع Nested(name1 T1, name2 T2, ...)، ويكون تخطيطه بعد سلسلة النوع مطابقًا على مستوى البايت لـ Array(Tuple(T1, T2, ..., Tn)) — بما في ذلك مرحلة البادئة الداخلية، لذا فإن أي حقل مُزوَّد بإصدار T_i يُخرِج بادئة حالته أولًا، قبل الإزاحات. ويستخدم المثال أدناه حقولًا بلا إصدار، لذا تكون مرحلة البادئة فارغة:
Nested بأسماء الحقول (a, b)، بينما لا يحتفظ Array(Tuple) بهذه الأسماء على شكل خانات مسماة.
سلسلة النوع للحالة B هي قائمة من أزواج (الاسم، النوع) مفصولة بفواصل. ويفصل أول فراغ بين الاسم ونوعه؛ وقد يحتوي النوع نفسه على فراغات إضافية وفواصل وأقواس، لذا يحتاج التحليل إلى أداة التقسيم نفسها المراعية للعمق والمستخدمة مع Tuple. بنية wire:
total_elements = offsets[num_rows - 1] (أو 0 عندما num_rows == 0). الإزاحات رتيبة غير متناقصة، ويحتوي كل تدفق حقل على total_elements قيمة بالضبط. يفرض الخادم، في وقت INSERT، أنه ضمن صف واحد يجب أن تحتوي جميع الحقول على العدد نفسه من العناصر. يكتب العمود الفارغ صفر بايت.
Nested(a UInt8, b String) مع صفَّين [(10,'x'),(20,'y')] و [(30,'z')] (25 بايت بعد سلسلة النوع):
الأسماء المستعارة للأنواع
لذلك يُفك ترميز عمود
Point تمامًا مثل Tuple(Float64, Float64) (ويُعرض بالشكل (1,2))، ويُفك ترميز Ring مثل Array(Tuple(Float64, Float64)) ([(0,0),(1,1)])، وهكذا صعودًا عبر التسلسل الهرمي.
ويُعد Geometry أيضًا اسمًا مستعارًا، لكنه يشير إلى Variant لا إلى مصفوفة متداخلة: إذ تكون الـ payload الخاصة به هي variant للأنواع الجغرافية الستة المذكورة أعلاه. ولا تحمل ترويسة العمود سوى سلسلة النوع Geometry — فهي لا تذكر الـ variant صراحةً — لذلك يجب على مفكّك الترميز توسيعه بنفسه. وكما هو الحال مع أي Variant، تتبع discriminators الترتيب القانوني المرتّب أبجديًا لأسماء geo المستعارة: 0 = LineString، 1 = MultiLineString، 2 = MultiPolygon، 3 = Point، 4 = Polygon، 5 = Ring. بعد ذلك، يُفك ترميز كل قيمة محددة عبر اسمها الجغرافي المستعار أعلاه (NULL يستخدم discriminator الخاص بـ Variant للقيمة NULL وهو 255).
SimpleAggregateFunction(func, T) هو اسم مستعار لنوع القيمة T. فهو يخزّن قيمة aggregate نهائية بالفعل، لذا فإن تمثيله على wire وطريقة عرضه يطابقان تمامًا ما لدى T (فمثلًا يُفك ترميز SimpleAggregateFunction(sum, UInt64) على أنه UInt64). ولا ينطبق ذلك إلا على الصيغة ذات نوع القيمة الواحد؛ أما النوع الأساسي نفسه فقد يكون مركبًا.
هناك نوعان مرتبطان ليسا اسمين مستعارين. وهما نوعا أعمدة صالحان في
Native — فبإمكان client، على سبيل المثال، تلقّي عمود AggregateFunction من combinator -State أو من aggregation موزعة — لكن لكلٍّ منهما payload متخصصة خاصة به، وهو ما يخرج عن نطاق هذه الصفحة:AggregateFunction(func, ...)يحتفظ بحالة aggregation وسيطة (وليس قيمة نهائية)؛ ويكون تخطيطه الثنائي خاصًا بدالة aggregate والإصدار.QBit(T, N)يخزّن متجهًا مع bit planes منقولة لأعباء عمل البحث المتجهي.
الأنواع ذات الإصدارات
Native على مستوى wire، تكون البادئة وأي Dictionary خاصة بكل block — ولا تحتفظ هذه الأنواع بأي حالة مشتركة بين الـ block (انظر ملاحظة البادئة لكل block أدناه)؛ ولا توجد حالة تسلسل مشتركة بين الـ block إلا في تدفّق MergeTree المخزَّن على القرص.
هذه الأنواع أكثر تعقيدًا بكثير من الأنواع المركّبة ذات البنية الثابتة، ويمكن للعميل الذي يستهدف استعلامات تحليلية بسيطة تأجيل التعامل معها.
إصدار التسلسل: المفهوم
تكتب معظم الأنواع ذات الإصدار هذا الإصدار بصيغة
UInt64 وبترتيب little-endian مباشرةً قبل أي بيانات أخرى في state prefix؛ فيما يستخدم عدد قليل منها VarUInt أو UInt8. يقرأ decoder الإصدار أولًا ويرفض القيم غير المعروفة — فالإصدار الأعلى يعني أن المُرسِل يستخدم تنسيقًا أحدث لا يفهمه decoder، وقراءته بصورة خاطئة تُفسد كل بايت لاحق.
يُصدَر state prefix في بداية كل block يزيد عدد صفوفه على صفر، مباشرةً قبل payload الخاص بذلك block.
لا يحتفظ Native writer وreader بحالة التسلسل عبر الـ blocks: إذ ينشئ NativeWriter حالة serialize جديدة ويكتب state prefix لكل block عمود غير فارغ يكتبه، كما ينشئ NativeReader حالة deserialize جديدة ويقرؤها لكل block غير فارغ يقرؤه (ويتجاوز كلاهما البادئة بالكامل عندما يكون rows == 0).
لذلك، لا تُنتِج header blocks (rows = 0) والـ blocks الفارغة أي شيء، ويجب على decoder أن يقرأ state prefix مرة أخرى في بداية كل block غير فارغ. أما decoder الذي يقرأ البادئة مرة واحدة فقط ويتعامل مع الـ blocks اللاحقة على أنها payload فقط، فسيقرأ بادئة block التالي على أنها بيانات ويفقد التزامن:
مرجع إصدار التسلسل
هناك بعض الأمور الجديرة بالملاحظة حول الجدول:
- القيم ليست متتالية. يستخدم
Dynamicالقيم1و2و3و4، حيث تأتيV3عند4وFLATTENEDعند3. وليس الرقم الأعلى بالضرورة أحدث. - بعض القيم خاصة بالتنسيق الأصلي فقط. توجد
Object::STRINGوObject::FLATTENEDوDynamic::FLATTENEDمن أجل التوافق مع البروتوكول الأصلي للعملاء الذين لا يطبّقون Object/Dynamic بالكامل. وهي لا تظهر في تخزين MergeTree على القرص. V3مخصّص أساسًا للتخزين على القرص. عادةً ما يرى العملاء الذين يستخدمون native TCP protocol القيمةFLATTENED(القيمة3) بدلًا منV3(القيمة4).
LowCardinality(T)
N من القيم الداخلية بقاموس صغير للقيم الفريدة، بالإضافة إلى N من الفهارس التي تشير إلى هذا القاموس.
سلسلة النوع: LowCardinality(InnerType). أمثلة: LowCardinality(String), LowCardinality(FixedString(4)), LowCardinality(Nullable(String)).
sharedDictionariesWithAdditionalKeys؛ أما القيم الأخرى فمحجوزة.
البيانات الوصفية لكل كتلة من نوع UInt64 هي حقل بتات:
في استجابة query نموذجية تحتوي على data block واحدة لكل عمود، تكون البيانات الوصفية
0x600 (HasAdditionalKeys + NeedUpdateDictionary).
قيم dict هي dict_size قيمة مُرمَّزة باستخدام النوع الداخلي T. ويحجز القاموس خانات أولية لقيم خاصة: فالعمود غير القابل لأن يكون NULL يحجز خانة واحدة (dict[0] تحتوي على القيمة الافتراضية للنوع الداخلي، مثل "" لـ String)؛ وتبدأ القيم المميزة الفعلية من dict[1].
بالنسبة إلى LowCardinality(Nullable(T))، يظل dict مُرمَّزًا على أنه T عادي (من دون تدفق null-map)، ولكن تُحجز خانتان: dict[0] هي وسم NULL وdict[1] هي القيمة الافتراضية للنوع الداخلي (مثل "" لـ String)؛ وتبدأ القيم المميزة الفعلية من dict[2]. ويشير مفتاح صف NULL إلى dict[0]، وتُكتب تلك الخانة على wire على شكل بايتات القيمة الافتراضية للنوع الداخلي.
المفاتيح هي فهارس داخل dict؛ وحجم كل فهرس هو 1 << key_type_code بايت (1 أو 2 أو 4 أو 8)، وتُعاد القيمة N على أنها dict[keys[N]].
keys_count هو عدد قيم LowCardinality عند المستوى التكراري الحالي، وليس بالضرورة عدد صفوف الكتلة. ففي عمود LowCardinality من المستوى الأعلى يتطابق العددان. ولكن عندما تكون LowCardinality ضمن نوع مركّب، يكون العدد هو عدد القيم المُسطَّحة الذي يمرّره النوع المركّب إلى المستوى الأدنى: ففي Array(LowCardinality(String)) الذي يحتوي على ثلاثة صفوف بإجمالي خمسة عناصر، تكون keys_count هي 5 لا 3؛ وفي Map(K, LowCardinality(V)) تكون هي العدد الإجمالي للأزواج، وهكذا. يجب على decoder أن يأخذ keys_count من هذا الحقل بدلًا من افتراض عدد صفوف الكتلة. وعندما يكون هذا العدد المُسطَّح صفرًا — على سبيل المثال، في كتلة تكون كل المصفوفات فيها فارغة — فإن مرحلة بيانات LowCardinality لا تكتب أي شيء على الإطلاق: فلا يوجد سوى بادئة الحالة (المُصدَرة في مرحلة بادئة الأنواع المركّبة)، من دون أي بيانات وصفية أو قاموس أو keys_count بعدها.
تُقرأ بادئة الحالة في بداية كل كتلة يزيد عدد الصفوف فيها على الصفر — أما كتل الترويسة (rows = 0) والكتل الفارغة فلا تُخرج شيئًا. وداخل الكتلة، تكون keys_count مساوية لعدد الصفوف، وتكون dict_size مساوية لعدد القيم في تدفق القاموس، ويشغل كل مفتاح 1 << key_type_code بايت.
في تنسيق
Native، ترسل كل كتلة قاموسًا مستقلًا ومحليًا خاصًا بها — ولا توجد حالة قاموس مشتركة بين الكتل. يضبط الكاتب في Native القيمة low_cardinality_max_dictionary_size = 0، لذلك لا ينشئ SerializationLowCardinality مطلقًا قاموسًا مشتركًا: إذ تكتب كل كتلة غير فارغة مفاتيحها كمفاتيح إضافية محلية على مستوى الكتلة مع عدم تعيين NeedGlobalDictionaryBit (البيانات الوصفية 0x600)، كما يرفض قارئ Native القيمة NeedGlobalDictionaryBit عندما تكون native_format هي true. لذلك يجب على أداة فك الترميز إعادة تعيين القاموس عند كل كتلة وقراءة إدخالات dict_size الموجودة في تلك الكتلة؛ لأن الاحتفاظ بقاموس من كتلة سابقة سيؤدي إلى قراءة مفاتيح الكتلة التالية على نحو خاطئ. (إن الإبقاء على قاموس LC عبر الكتل يتعلق بالتخزين على القرص في MergeTree، وليس ببنية Native على مستوى النقل.)LowCardinality(String) بالقيم ['a', 'b', 'a', 'c', 'b']:
dict[1], dict[2], dict[1], dict[3], dict[2] = ["a", "b", "a", "c", "b"].
يُظهر LowCardinality(Nullable(String)) بالقيم ['a', NULL, '', 'b'] كلتا الخانتين المحجوزتين — dict[0] لـ NULL وdict[1] للقيمة الافتراضية المتمثلة في السلسلة الفارغة:
dict[2] = "a"، وdict[0] = NULL، وdict[1] = ""، وdict[3] = "b"، أي ["a", NULL, "", "b"]. كلٌّ من dict[0] وdict[1] عبارة عن بايتات فارغة على الـwire؛ وكون القيمة NULL ناتج عن أن المفتاح يشير إلى الخانة 0، لا عن البايتات نفسها.
JSON (المستوى 1: استخدام String كخيار احتياطي)
JSON في ClickHouse على عدة ترميزات سلكية (راجع مرجع إصدار التسلسل). ويُعد المستوى 1 الأبسط: فعند ضبط الإعداد الخاص بكل استعلام output_format_native_write_json_as_string = 1، يقوم الخادوم بتسطيح كل قيمة JSON إلى نصها المتسلسل، ويُخرج العمود على هيئة String مع وسم ببادئة الحالة.
سلسلة النوع: JSON.
1 في هذا String fallback. وتشير القيم الأخرى إلى ترميزات JSON/Object مختلفة: 0 = V1، 2 = V2 (وهو الافتراضي عبر بروتوكول native TCP)، 3 = FLATTENED، 4 = V3 (انظر مرجع إصدار التسلسل). وأي مفكِّك ترميز يرى هنا قيمة غير 1 لا يكون بصدد String fallback. وتُقرأ البادئة في بداية كل كتلة تحتوي على صفوف > 0، ويكون دفق القيم عمود String قياسيًا لعدد num_rows من الصفوف.
قيمة JSON '{"a":1}' (صف واحد):
{"a":1}، مع إبقاء العدد الصحيح كعدد صحيح. وهذا النص ليس إلا قيمة String، لذا يتلقى العميل JSON كبيانات معتمة للنقل، لكنه لا يستعيد المسارات الفردية ولا أنواعها في ClickHouse؛ وللحفاظ على دقة الأنواع لكل مسار، يلزم استخدام ترميز المستوى 2 الموضح أدناه.
Variant(T1, T2, …)
NULL. ويحمل كل صف مميِّزًا عامًا بحجم بايت واحد يحدّد نوعه، ثم تُخزَّن القيم الخاصة بكل نوع بشكل متراص، في مقطع متصل واحد لكل نوع بديل.
سلسلة النوع: Variant(T1, T2, ...). يوحِّد الخادم الترتيب إلى الصيغة القياسية (تُرتَّب الأنواع البديلة حسب الاسم)، لذا فإن سلسلة النوع كما تُستقبَل تسرد الأنواع بالفعل وفق ترتيب المميِّز العام: يختار المميِّز 0 أول نوع مُدرج، و1 الثاني، وهكذا. وتعني 255 (NULL_DISCRIMINATOR) أن الصف هو NULL. ولا تكون عناصر Variant من النوع Nullable مطلقًا — فالمميِّز هو المسؤول عن NULL. أمثلة: Variant(String, UInt64), Variant(Array(UInt8), String).
تحمل بادئة الحالة وضع المميِّزات UInt64 LE: 0 = BASIC (يُكتب مميِّز كل صف كقيمة حرفية)، و1 = COMPACT (ترميز granule بأسلوب طول التشغيل). يستخدم الخادم BASIC عبر native protocol افتراضيًا (use_compact_variant_discriminators_serialization = false)؛ ولا يُحدَّد هنا إلا BASIC.
r ذو المميِّز d (≠ 255) يأخذ القيمة عند الفهرس counter[d] من مسار قيم النوع المتغيّر d، ثم تُزاد counter[d]. أما الصفوف ذات المميِّز 255 فهي NULL ولا تستهلك أي قيمة من أي مسار، لذا فإن مجموع العدّادات لكل نوع يساوي عدد الصفوف غير NULL.
تُقرأ بادئة الحالة (النمط UInt64) في بداية كل كتلة تحتوي على صفوف > 0؛ ولا تُخرج الترويسة والكتل الفارغة أي شيء. وكل مميِّز غير NULL يكون أصغر من عدد الأنواع المتغيّرة، ويُفك ترميز النوع المتغيّر i لعدد count[i] من الصفوف بالضبط.
عناصر Variant التي تكون Stateful بحد ذاتها (
LowCardinality, Variant, Dynamic, JSON) تُخرج بادئة الحالة الخاصة بها في مرحلة بادئة الحالة لكل عنصر، بعد النمط UInt64. أما الأنواع الورقية والتركيبات البسيطة (Array, Tuple, Map لأنواع ورقية) فبادئات حالتها فارغة ويمكن تركيبها بحرية.Variant(String, UInt64) بالقيم [42, 'hi', NULL] (يرتّب الترتيب canonical String قبل UInt64، لذا يكون المميِّز 0 = String و1 = UInt64):
42; row 1 = String run[0] = "hi"; row 2 = NULL.
يمثّل تدفّق discriminator الفهرس؛ إذ يسحب كل discriminator غير NULL القيمة التالية من التسلسل الكثيف الخاص بنوعه، بينما لا يستهلك 255 (NULL) شيئًا. ويُعيد هذا المرور نفسه أيضًا تكوين Dynamic، الذي يختلف فقط في كيفية ترميز NULL:
Dynamic
Variant، فإن مجموعة الأنواع لا تظهر في type string الخاص بالعمود، بل تُحمَل في state prefix.
سلسلة النوع: Dynamic أو Dynamic(max_types=N). يقيّد المعلَمة max_types عدد الأنواع المميّزة التي يتتبّعها العمود، لكنه لا يؤثر في wire format الموضَّح أدناه.
يحتوي Dynamic على أربعة ترميزات: V1 = 1، وV2 = 2، وFLATTENED = 3، وV3 = 4. ويعتمد الترميز الذي يرسله server على القناة وإعدادات query:
- عبر
clickhouse-clientوHTTPFORMAT Nativeتكون revision الخاصة بـ writer هي0(ما لم تُرفَع باستخدامclient_protocol_version)، لذا يكون الترميز الافتراضي هو V1. - عبر native TCP protocol عند revision المتفاوض عليها، يكون الترميز الافتراضي هو V2. ويُبقي writer الخاص بـ
Nativeالإحصاءات معطّلة، لذلك لا تتضمن حمولة V2 الافتراضية أي إحصاءات لكل variant — إذ تأتي بعد قائمة الأنواع مباشرةً بادئةVariantالمتداخلة والبيانات. (الإحصاءات لكل variant تتعلق بتخزين MergeTree على disk، وليست جزءًا من Native wire.) - يتجاوز إعداد query
output_format_native_use_flattened_dynamic_and_json_serialization = 1كليهما ويُخرج FLATTENED (version 3) بغضّ النظر عن revision.
النطاقتحدد هذه الصفحة تخطيط
FLATTENED فقط. أما التخطيطات الثنائية غير المسطّحة V1/V2/V3 فهي التمثيل الداخلي/المخزَّن على disk (قوائم أنواع مرمّزة ثنائيًا، وإحصاءات لكل variant)، وهي غير محددة هنا. ويجب على client الذي يريد فك ترميز Dynamic باستخدام هذه الصفحة أن يطلب FLATTENED عبر تعيين output_format_native_use_flattened_dynamic_and_json_serialization = 1؛ ويفترض التخطيط أدناه هذا الإعداد. ونظرًا إلى أن بايت version يأتي في بداية البادئة، يمكن لـ decoder اكتشاف الترميز الفعلي الذي استلمه ورفض V1/V2/V3 إذا كان لا يطبّق سوى FLATTENED.discriminator هو أصغر عدد صحيح غير موقّع يمكنه فهرسة num_types من الأنواع بالإضافة إلى خانة NULL — أي UInt8 عندما يكون num_types ≤ 255، ثم UInt16 وUInt32 وUInt64. وتكون NULL هي قيمة discriminator التي تساوي num_types نفسها، وهذا يختلف عن Variant، حيث تكون NULL هي القيمة الثابتة 255. وتتم إعادة البناء بالطريقة الكثيفة نفسها كما في Variant: احتفِظ بعدّاد لكل نوع، ويأخذ الصف r ذو discriminator d (≠ num_types) القيمة counter[d] من سلسلة النوع d.
تُقرأ بادئة الحالة (الإصدار + قائمة الأنواع) في بداية كل كتلة تحتوي على عدد صفوف > 0؛ أما الترويسة والكتل الفارغة فلا تُصدران شيئًا.
أنواع وقت التشغيل التي يكون
serialization الخاص بها محافظًا على الحالة (LowCardinality وVariant وDynamic وJSON) تحمل بادئات حالة متداخلة بعد قائمة أسماء الأنواع.Variant — إذ تُكتَب خانات المتغيّر العادية وفق ترتيب DataTypeVariant (اسم النوع)، لذا فإن ترتيب الإرسال لا يتبع ترتيب الإدراج. لكنها ليست دائمًا مرتبةً ترتيبًا عامًا، مع ذلك: فالأنواع التي فاضت إلى المتغيّر المشترك (على سبيل المثال ضمن Dynamic(max_types=N)) تُلحَق بعد الخانات العادية بحسب ترتيب ظهورها أول مرة، لذا قد يخرج ذيل القائمة عن ترتيب أسماء الأنواع. لذلك يجب على مفكِّك الترميز أن يتعامل مع قائمة الأنواع المُرسلة باعتبارها المرجع المعتمد لتعيين المميِّزات، وألا يعيد ترتيبها بنفسه. بالنسبة إلى الصفوف [42::UInt64, "hi", NULL] فالنوعان هما String و UInt64، وبما أن "String" يُرتَّب قبل "UInt64"، فإن المميِّزات تكون 0 = String، و1 = UInt64، و2 = NULL:
42; الصف 1 = String run[0] = "hi"; الصف 2 = NULL. تتبع قيم run لكل نوع ترتيب wire نفسه كما في قائمة الأنواع (String قبل UInt64).
JSON (المستوى 2: FLATTENED Object)
output_format_native_write_json_as_string = 0) مع تفعيل flag الخاص بالتسلسل flattened (output_format_native_use_flattened_dynamic_and_json_serialization = 1)؛ وعندها يُصدر server الإصدار 3 من serialization.
يوجد نوعان من المسارات:
- المسارات ذات النوع المعرَّف يُصرَّح بها في
type string، على سبيل المثالJSON(a UInt32, b String)، ويُفك ترميزها وفق النوع المصرَّح به. وإذا احتوى اسم المسار على نقاط، فيُكتب بين backticks فيtype string. - المسارات الديناميكية تُكتشف أثناء runtime، ويُفك ترميز كل منها كعمود Dynamic.
num_rows من القيم.
Dynamic الخاصة بالمسار الديناميكي (في مرحلة البادئات) منفصلة عن بياناته (في مرحلة البيانات). تُقرأ بادئة الحالة في بداية كل كتلة تحتوي على صفوف > 0، ويحتوي كل عمود مسار (سواء كان مُحدَّد النوع أو ديناميكيًا) على num_rows قيمة بالضبط. ويُركَّب كائن الصف r بقراءة قيمة كل مسار عند الفهرس r؛ أما المسار الديناميكي الذي يكون فيه المميِّز Dynamic هو NULL لذلك الصف، فلا يضيف أي مفتاح.
قيمة JSON {"a": 42, "b": "hi"} (صف واحد، وكلا المسارين ديناميكيان). يُستدل على العدد الصحيح في JSON على أنه Int64:
JSON غير المسطّح (V2/V3)
Object غير المسطّحة (V1/V2/V3) في تخزين MergeTree على القرص، وهي ما يرسله الخادم عبر القناة الثنائية عندما يكون خيار التسطيح معطّلًا — V1 عبر clickhouse-client / HTTP FORMAT Native (المراجعة 0)، وV2 عبر بروتوكول TCP الأصلي. وهي تتضمن عمود shared-data، لكنها غير موصوفة في هذه الصفحة. لاحظ أيضًا أنها لا تتضمن إحصاءات لكل مسار عبر قناة Native الثنائية: إذ يترك NativeWriter الإحصاءات معطّلة، لذلك لا تحتوي بادئة بنية Object على قسم للإحصاءات، وتأتي البايتات التي تليها مباشرة كبوادئ وبيانات typed/dynamic/shared-data. ولا تظهر الإحصاءات إلا في مسارات MergeTree على القرص التي تفعّلها. ولفك ترميز عمود JSON باستخدام هذه الصفحة، يجب على العميل اختيار أحد المستويات الموثّقة: اضبط output_format_native_write_json_as_string = 1 لاستخدام String fallback، أو اضبط output_format_native_use_flattened_dynamic_and_json_serialization = 1 (مع output_format_native_write_json_as_string = 0) لاستخدام تخطيط FLATTENED Object.
إطار الضغط
Native باستخدام تنسيق إطارات داخلي. إن بنية الإطار أدناه مستقلة عن طبقة النقل — فالإطارات نفسها تظهر سواء في native TCP protocol أو عبر HTTP — لكن طريقة طلب الضغط، وما يحيط بهذه الإطارات، يختلف باختلاف وسيلة النقل.
- بروتوكول TCP الأصلي. يكون الضغط اختياريًا لكل query على حدة عبر العلامة
compressionفي حزمة الاستعلام. عند تفعيله، يُغلَّف body كل حزمةDataوTotalsوExtremesوLogوProfileEvents— أي البايتات التي تأتي بعد السلسلةtable_name— ضمن تنسيق الإطار هذا. أما غلاف الحزمة نفسه، ورمز نوع الحزمة، والسلسلةtable_name، فلا تُضغط؛ إذ يكتبها server إلى التدفّق الخام. وكل ما يخرجهNativeWriterيذهب إلى التدفّق المضغوط، لذا تكون بادئةBlockInfoأول ما يظهر داخل الإطار، إلى جانب الأبعاد والأعمدة. لذلك يجب على client فك ضغط الإطار قبل أن يتمكن من قراءةBlockInfo. - HTTP. يقوم
SELECT ... FORMAT Native&compress=1بتغليف تدفّق بايتاتFORMAT Nativeبالكامل داخل الإطارات نفسها (إذ يستخدم server نفسCompressedWriteBufferالداخلي)، بينما يتوقع?decompress=1الإطارات نفسها في body الإدخال من نوعNative، مع فك ترميزها عبرCompressedReadBufferالمطابق. لا يوجد في هذا المسار نوع حزمة TCP أوtable_nameأو غلاف حزمة؛ فالحمولة المضغوطة بالكامل ليست سوى blocks مؤطرة منNative(وتظهر بادئةBlockInfoفقط إذا كانتrevisionالمتفاوض عليها أكبر من0، تمامًا كما في البنية غير المضغوطة أعلاه). يختلف هذا التأطير الداخليcompress/decompressعن ضغط نقل HTTP (Content-Encoding: gzip/zstd، الذي يُفعَّل بواسطةenable_http_compression)؛ إذ يغلّف الأخير الاستجابة على طبقة HTTP، وليس باستخدام تنسيق الإطار الموضّح أدناه.
FORMAT Native غير المضغوطة لا يزال بحاجة إلى إضافة طبقة الإطار هذه لقراءة استجابة HTTP مضغوطة من نوع Native أو لإرسال request body لـ decompress=1.
تنسيق الإطار
16 + compressed_size = 16 + 9 + body_size = 25 + body_size. لاحظ النطاقين: يشمل checksum الترويسة ذات الـ9 بايتات بالإضافة إلى المتن، بينما يحسب compressed_size الترويسة والمتن ولكن لا يشمل checksum نفسه:
قيم بايتات الطريقة
قيمة التحقق
حدود كل كتلة
CompressedWriteBuffer، الذي يُصدر إطارًا كلما امتلأ مخزنه المؤقت الداخلي (≈1 ميغابايت، DBMS_DEFAULT_BUFFER_SIZE)، ثم يُصدر إطارًا أخيرًا عند تفريغ الكتلة. لذلك تكون الكتلة الصغيرة إطارًا واحدًا، بينما تكون الكتلة الكبيرة عدة إطارات متتالية.
تسري هذه القاعدة في اتجاه واحد فقط: لأن المُرسِل يفرّغ المخزن المؤقت المضغوط في نهاية كل كتلة، فإن كل نهاية كتلة تتطابق مع حد إطار — لكن العكس غير صحيح. فحد الإطار الوسيط، الذي يُصدر عند امتلاء المخزن المؤقت في منتصف الكتلة، يقع في منتصف الكتلة وليس حدًا لها. لذلك يجب على وحدة فك الترميز استخدام أبعاد الكتلة نفسها (num_columns/num_rows) لتحديد موضع انتهائها؛ ويجب ألا تفترض أن كل إطار يمثل كتلة كاملة واحدة.
يعالج المستقبِل الإطارات كتدفّق: يقرأ 16 + 9 بايتًا، ثم يقرأ بالضبط compressed_size - 9 بايتًا من المتن، ويفك الضغط إلى uncompressed_size بايتًا بالضبط، ثم يمرّر هذه البايتات إلى وحدة فك ترميز الكتلة؛ وعندما تحتاج وحدة فك الترميز إلى أكثر مما يحتويه الإطار الحالي، تسحب الإطار التالي. وبما أن المُرسِل يفرّغ المخزن المؤقت لكل كتلة، فإنه بعد فك ترميز كتلة كاملة يصبح مخزن الإطار المؤقت فارغًا، وتبدأ الكتلة التالية عند إطار جديد.
في بروتوكول native TCP، يُكتب غلاف الحزمة — أي VarUInt الخاص بنوع الحزمة والسلسلة table_name — إلى التدفّق الخام، خارج الحمولة المضغوطة؛ ولا يُؤطَّر إلا متن الكتلة (BlockInfo + الأعمدة). أما مسار HTTP compress/decompress فلا يحتوي على مثل هذا الغلاف: فالتدفّق بأكمله يتكوّن من كتل مؤطَّرة.
التفاوض
compression: bool في Query packet تفعيل ذلك لهذا الـ query وحده. ويلتزم server بهذا الطلب، فيرسل أجسام Data/Totals/Extremes/Log/ProfileEvents مضغوطة طوال مدة الـ query (Log/ProfileEvents فقط في v54481+). كما يتوقع أيضًا أن تكون data blocks الصادرة من client — أي external tables، ووسم نهاية البيانات الفارغ، وصفوف INSERT — مؤطرة بالطريقة نفسها. وقد تختلف الـ queries اللاحقة على الـ connection نفسها.
عبر HTTP، لا توجد Query packet: إذ يحدد query parameter compress=1 إخراجًا مؤطرًا لذلك الطلب، بينما يصرّح decompress=1 بأن request body مؤطر. ويُكتب خرج compress=1 باستخدام codec الافتراضي لدى server (LZ4) بدلًا من network_compression_method؛ أما قارئ decompress=1 فيأخذ الـ codec من بايت method في كل frame، لذا يُقبل أي codec عند الإدخال.
عند تفعيل الضغط، قد يمرّر server أيضًا columns عبر مسار ترتيب الكتل المتوازي /
ColumnBLOB (PARALLEL_BLOCK_MARSHALLING، v54478) للـ blocks التي تحتوي على أكثر من row واحدة. لذلك يجب أن يكون أي تنفيذ يضغط بيانات INSERT مهيأً للتعامل مع هذا المسار (أو تعطيله صراحةً) لتجنّب stream غير متزامن.المسرد
Native، الذي يُجري serialization عند revision 0. راجع BlockInfo.
Column body — البايتات في Column التي تحتوي على values الفعلية، بعد ترويسة column (الاسم والنوع والبايت has_custom_serialization). ويكون layout خاصًا بكل type. راجع column wire layout.
Composite type — type مبني من type داخلي واحد أو أكثر، ويُرمَّز على شكل streams متعددة لكل column. ويكون wire format ثابتًا وغير مُرقّم بالإصدارات. راجع composite types.
Dictionary (LowCardinality) — مصفوفة القيم الفريدة التي يشير إليها column من النوع LowCardinality(T) عبر فهارس صحيحة. راجع LowCardinality.
Empty block — Block تكون فيه num_columns = 0 وnum_rows = 0. ويُستخدم كقيمة sentinel: وسم client-side لنهاية الإدخال، ووسم server-side لحدود stream. راجع block variants.
Header block — Block تكون فيه num_columns > 0 وnum_rows = 0، ويرسله server كأول حزمة Data في استجابة query. ويُعلن schema الخاصة بالنتيجة. راجع block variants.
Inner type — النوع الذي يغلّفه composite. فـ Array(UInt32) له inner type هو UInt32، أما Nullable(T) فالـ inner type فيه هو T.
Offsets stream — مصفوفة UInt64 الخاصة بمواضع النهاية التراكمية التي تستخدمها Array وMap وNested لتحديد حدود العناصر لكل row. راجع Array.
Placeholder value — البايتات المكتوبة في مواضع null ضمن values stream الخاصة بـ column من النوع Nullable(T). يقرأها decoder لتقديم stream، لكنه يتجاهل محتواها. راجع Nullable.
Result block — Block تكون فيه num_rows > 0 وتحمل rows الفعلية الخاصة بـ query result. راجع block variants.
Schema block — مرادف لـ header block، ويُستخدم عند وصف phase الخاصة بـ INSERT، حيث يوضّح schema block للعميل أشكال columns المتوقعة.
Serialization version — رقم version على مستوى on-wire لكل type، تستخدمه الأنواع المُرقّمة بالإصدارات للتصريح بمتغيّر encoding الذي يلي ذلك. وهو يختلف عن protocol version. راجع serialization version: concept.
State prefix — البايتات التي تسبق payload الخاصة بكل block لنوع مُرقّم بالإصدارات. وتحمل serialization version وmetadata الخاصة بالقاموس لكل block (في حالة LowCardinality). وتُصدر في بداية كل block فيه rows > 0، ولا يُحتفَظ بها عبر blocks.
Stream — امتداد متصل من البايتات داخل Column body، يرمّز مكوّنًا فرعيًا منطقيًا واحدًا (null-map، أو offsets array، أو values stream). وتجمع الأنواع متعددة الـ streams بين streamين أو أكثر لكل column.