Skip to main content
عميل C# الرسمي للاتصال بـ ClickHouse. تتوفّر الشيفرة المصدرية للعميل في مستودع GitHub. طوّره في الأصل Oleg V. Kozlyuk. توفّر المكتبة واجهتَي برمجة تطبيقات رئيسيتين:
  • ClickHouseClient (موصى به): عميل عالي المستوى وآمن للاستخدام من عدة خيوط، ومصمَّم للاستخدام بنمط singleton. يوفّر واجهة برمجة تطبيقات غير متزامنة وبسيطة للاستعلامات وعمليات الإدراج المجمّع. وهو الأنسب لمعظم التطبيقات.
  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): تجريدات قياسية لقواعد البيانات في .NET. وهي مطلوبة لتكامل ORM ‏(Dapper وLinq2db) وعندما تحتاج إلى التوافق مع ADO.NET. تُعد ClickHouseBulkCopy فئة مساعدة لإدراج البيانات بكفاءة باستخدام اتصال ADO.NET. الفئة ClickHouseBulkCopy مُهمَلة وستُزال في إصدار مستقبلي؛ استخدم ClickHouseClient.InsertBinaryAsync بدلاً منها.
تشترك كلتا الواجهتين في مجمّع اتصالات HTTP الأساسي نفسه، ويمكن استخدامهما معًا داخل التطبيق نفسه.

دليل الترحيل

  1. حدّث ملف .csproj لاستخدام اسم الحزمة الجديد ClickHouse.Driver وأحدث إصدار على NuGet.
  2. حدّث جميع مراجع ClickHouse.Client إلى ClickHouse.Driver في شيفرة مشروعك.

إصدارات ‎.NET‎ المدعومة

يدعم ClickHouse.Driver إصدارات ‎.NET‎ التالية:
  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

إصدارات ClickHouse المدعومة

يدعم العميل رسميًا الإصدارات الثلاثة الأخيرة، بالإضافة إلى آخر إصدارين طويلَي الدعم (LTS).

التثبيت

ثبّت الحزمة من NuGet:
أو باستخدام مدير حزم NuGet:

البدء السريع

التهيئة

هناك طريقتان لتهيئة اتصالك بـ ClickHouse:
  • سلسلة الاتصال: أزواج مفتاح/قيمة مفصولة بفواصل منقوطة، تحدد المضيف وبيانات اعتماد المصادقة وخيارات الاتصال الأخرى.
  • كائن ClickHouseClientSettings: كائن تهيئة مضبوط الأنواع يمكن تحميله من ملفات التهيئة أو تعيينه في الشيفرة.
فيما يلي قائمة كاملة بجميع الإعدادات وقيمها الافتراضية وتأثيراتها.

إعدادات الاتصال

تنسيق البيانات والتسلسل

إدارة الجلسات

تؤدي العلامة UseSession إلى الاحتفاظ بجلسة الخادم، مما يتيح استخدام عبارات SET والجداول المؤقتة. ستُعاد تهيئة الجلسات بعد 60 ثانية من عدم النشاط (المهلة الافتراضية). ويمكن تمديد مدة الجلسة عبر تعيين إعدادات الجلسة باستخدام عبارات ClickHouse أو إعدادات الخادم.تتيح الفئة ClickHouseConnection عادةً التشغيل المتوازي (أي يمكن لعدة خيوط تنفيذ الاستعلامات بالتزامن). ومع ذلك، فإن تمكين العلامة UseSession يقيّد ذلك باستعلام نشط واحد فقط لكل اتصال في أي لحظة (وهذا قيد على جهة الخادم).

الأمان

تهيئة عميل HTTP

التسجيل وتصحيح الأخطاء

الإعدادات المخصصة والأدوار

عند استخدام سلسلة اتصال لتعيين إعدادات مخصصة، استخدم البادئة set_، مثل “set_max_threads=4”. أما عند استخدام كائن ClickHouseClientSettings، فلا تستخدم البادئة set_.للاطلاع على القائمة الكاملة بالإعدادات المتاحة، راجع هنا.

أمثلة لسلسلة الاتصال

اتصال بسيط

باستخدام إعدادات ClickHouse مخصّصة


QueryOptions

يتيح لك QueryOptions تجاوز الإعدادات على مستوى العميل لكل استعلام على حدة. جميع الخصائص اختيارية، ولا تستبدل القيم الافتراضية للعميل إلا عند تحديدها. مثال:

InsertOptions

يوسّع InsertOptions ‏QueryOptions بإعدادات خاصة بعمليات الإدراج المجمّع عبر InsertBinaryAsync. تتوفّر أيضًا جميع خصائص QueryOptions في InsertOptions. مثال:

تخطي استعلام فحص المخطط

بشكل افتراضي، يرسل InsertBinaryAsync استعلام SELECT ... WHERE 1=0 قبل كل عملية إدراج لاكتشاف أنواع الأعمدة. في السيناريوهات ذات معدل النقل المرتفع، يمكنك التخلص من هذا العبء الإضافي باستخدام خيارين: الخيار 1: تحديد أنواع الأعمدة صراحةً عندما تكون على دراية بمخطط الجدول وقت التجميع، مرّره مباشرةً عبر ColumnTypes. عندها لن يُرسل أي استعلام للمخطط على الإطلاق:
الخيار 2: تخزين المخطط مؤقتًا عند الإدراج في الجدول نفسه بشكل متكرر، اضبط UseSchemaCache = true للاستعلام عن المخطط مرة واحدة ثم إعادة استخدامه في عمليات الإدراج اللاحقة ضمن مثيل ClickHouseClient نفسه:
  • يحظى ColumnTypes بالأولوية على UseSchemaCache. وإذا تم تعيينهما معًا، فستُستخدم الأنواع المحددة صراحةً.
  • لا تكتشف ذاكرة التخزين المؤقت للمخطط تغييرات ALTER TABLE. إذا عدّلت مخطط الجدول، فأنشئ ClickHouseClient جديدًا أو تجنّب استخدام UseSchemaCache لهذا الجدول.
  • يقتصر نطاق ذاكرة التخزين المؤقت على instance الخاصة بـ ClickHouseClient، ويُعرَّف مفتاحها بواسطة (database, table). وتشترك المجموعات الفرعية المختلفة من الأعمدة في الجدول نفسه في مخطط واحد مخزَّن مؤقتًا.

ClickHouseClient

تُعد ClickHouseClient واجهة برمجة التطبيقات الموصى بها للتفاعل مع ClickHouse. وهي آمنة للاستخدام من عدة خيوط، ومصممة للاستخدام كـ singleton، وتدير داخليًا تجمّع اتصالات HTTP.

إنشاء عميل

أنشئ ClickHouseClient باستخدام سلسلة اتصال أو كائن ClickHouseClientSettings. راجع قسم التهيئة للاطّلاع على الخيارات المتاحة. تتوفّر تفاصيل خدمة ClickHouse Cloud الخاصة بك في وحدة تحكم ClickHouse Cloud. حدّد خدمة وانقر على Connect: اختر C#. ستُعرض تفاصيل الاتصال أدناه. إذا كنت تستخدم ClickHouse مُدارًا ذاتيًا، فسيُحدِّد مسؤول ClickHouse لديك تفاصيل الاتصال. باستخدام سلسلة اتصال:
أو باستخدام ClickHouseClientSettings:
في حالات استخدام حقن التبعية، استخدم IHttpClientFactory:
صُمِّم ClickHouseClient ليكون طويل الأمد ويُستخدم بشكل مشترك عبر تطبيقك. أنشِئه مرة واحدة فقط (عادةً بصفته singleton) وأعِد استخدامه في جميع عمليات قاعدة البيانات. يتولى العميل إدارة تجميع اتصالات HTTP داخليًا.

تنفيذ الاستعلامات

استخدم ExecuteNonQueryAsync مع التعليمات التي لا تُرجع نتائج:
استخدم ExecuteScalarAsync لاسترجاع قيمة واحدة:

إدخال البيانات

عمليات الإدراج باستخدام المعلمات

أدرِج البيانات باستخدام استعلامات ذات معلمات عبر ExecuteNonQueryAsync. يجب تحديد أنواع المعلمات في SQL باستخدام الصياغة {name:Type}:

عمليات الإدراج المجمّعة

استخدم InsertBinaryAsync لإدراج أعداد كبيرة من الصفوف بكفاءة. فهو يمرّر البيانات بتنسيق الصفوف الثنائي الأصلي في ClickHouse، ويدعم تحميل الدُفعات بالتوازي، ويتجنب أخطاء “URL طويل جدًا” التي قد تحدث مع الاستعلامات ذات المعلمات.
لمجموعات البيانات الكبيرة، اضبط التجميع على دفعات ومستوى التوازي باستخدام InsertOptions:
  • يجلب العميل تلقائيًا بنية الجدول عبر SELECT * FROM <table> WHERE 1=0 قبل الإدراج. يجب أن تتوافق القيم المُمرَّرة مع أنواع الأعمدة المستهدفة. لتجاوز هذا الاستعلام، استخدم InsertOptions.ColumnTypes أو InsertOptions.UseSchemaCache.
  • عندما تكون قيمة MaxDegreeOfParallelism > 1، تُحمَّل الدفعات بالتوازي. لا تتوافق الجلسات مع الإدراج المتوازي؛ لذا عطّل الجلسات أو عيّن MaxDegreeOfParallelism = 1.
  • استخدم RowBinaryFormat.RowBinaryWithDefaults في InsertOptions.Format إذا كنت تريد أن يطبّق الخادم قيم DEFAULT على الأعمدة غير المُمرَّرة.

عمليات إدراج POCO

بدلاً من إنشاء مصفوفات object[]، يمكنك إدراج كائنات POCO محددة النوع مباشرةً. سجّل النوع مرة واحدة، ثم مرّر IEnumerable<T>:
افتراضيًا، تُعيَّن جميع الخصائص العامة القابلة للقراءة إلى أعمدة باستخدام مطابقة صارمة للأسماء تراعي حالة الأحرف. يمكنك تخصيص هذا التعيين باستخدام السمات:
عندما تحدد جميع الخصائص المعينة Type صريحًا، يُتخطّى استعلام فحص المخطط بالكامل. وعندما تكون بعض الخصائص فقط ذات أنواع صريحة، يعود برنامج التشغيل إلى فحص المخطط لمجموعة الأعمدة الكاملة. يدعم InsertBinaryAsync<T> خيارات InsertOptions نفسها (التجميع على دفعات، التوازي، التخزين المؤقت للمخطط) مثل التحميل الزائد object[].
بخلاف التحميل الزائد object[]، لا يقبل InsertBinaryAsync<T> قائمة أعمدة صريحة. تُحدَّد الأعمدة بواسطة الخصائص المعينة للنوع المسجل. للتحكم في الأعمدة التي تُدرج، استخدم [ClickHouseNotMapped] لاستبعاد الخصائص أو [ClickHouseColumn(Name = "...")] لإعادة تسميتها.إذا تم تعيين ColumnTypes في InsertOptions، فستتجاوز سمات POCO.

تطور المخطط

تعمل عمليات الإدراج الخاصة بـ POCO بسلاسة عند إضافة أعمدة إلى الجدول المستهدف بعد تسجيل النوع. ونظرًا إلى أن برنامج التشغيل لا يُدرج سوى الأعمدة المرتبطة بـ POCO، فإن أي أعمدة جديدة تحتوي على DEFAULT (أو تعبيرات افتراضية أخرى) يملؤها الخادم تلقائيًا. ولا حاجة إلى أي تغييرات في الشيفرة أو إلى إعادة التسجيل.

موضع استعلام الإدراج

يكتب الإدراج الثنائي عبارة INSERT INTO ... FORMAT ... الخاصة به في السطر الأول من نص الطلب، قبل الصفوف. ويُضغط نص الطلب افتراضيًا، لذا فإن آليات التوجيه والتسجيل التي تفحص عنوان URL فقط لا ترى هذه العبارة. اضبط InsertOptions.QueryPlacement على InsertQueryPlacement.Url لإرسال العبارة بدلًا من ذلك في معامل URL باسم query، بحيث يقتصر نص الطلب على الصفوف وحدها:
استخدمه عندما يقوم proxy أو load balancer أو gateway بالتوجيه أو الفحص بناءً على الـ parameter query، أو عندما تريد ظهور الـ statement في سجلات الوصول وأدوات الـ observability. وهو خيار اختياري لأن الـ statement يُحتسب عندئذٍ ضمن طول الـ URL. والحد الفعلي هو الأدنى بين ما يفرضه الـ runtime الخاص بـ .NET والوسيط والـ server. في .NET 6 وحتى .NET 9، يحصر System.Uri عنوان URI الكامل المُرمَّز للطلب في 65,519 محرفًا؛ ويطلق الـ driver استثناء InvalidOperationException يعيد توجيهك إلى InsertQueryPlacement.Body عند تجاوز هذا الحد. أما http_max_uri_size في ClickHouse فقيمته الافتراضية 1 ميبي بايت، وقد يفرض الوسيط حدًا أدنى منها. في وضع الـ body، لا يخضع الـ statement ولا الـ rows لأي حد من هذا القبيل يتعلق بطول الـ URL؛ لكن قد تظهر خيارات الطلب الأخرى في الـ URL. هذا الإعداد مستقل عن Compressor: إذ يُرمَّز الـ body بالطريقة نفسها في كلا الوضعين.

قراءة البيانات

استخدم ExecuteReaderAsync لتنفيذ استعلامات SELECT. يوفّر ClickHouseDataReader المُعاد وصولًا مُحدَّد النوع إلى أعمدة النتائج عبر طرق مثل GetInt64() وGetString() وGetFieldValue<T>(). استدعِ Read() للانتقال إلى الصف التالي. وتُرجع false عند عدم وجود المزيد من الصفوف. ويمكنك الوصول إلى الأعمدة حسب الفهرس (ابتداءً من 0) أو حسب اسم العمود.

القراءة باستخدام POCO

بدلًا من قراءة الأعمدة حسب الفهرس أو الاسم، يمكنك تمرير نتائج الاستعلام مباشرةً إلى فئاتك الخاصة. سجّل النوع مرة واحدة في العميل، ثم استخدم QueryAsync<T>:
يُعِدّ RegisterPocoType<T>() كلاً من عمليات ربط الإدراج والقراءة، ويتحقق من كليهما مسبقًا. أما RegisterBinaryInsertType<T>() فلم يتغير، ويظل مخصصًا للإدراج فقط حفاظًا على التوافق مع الإصدارات السابقة. يجب أن يتضمن النوع المسجَّل ما يلي:
  • مُنشئًا عامًا بدون معاملات.
  • خاصية عامة واحدة على الأقل لها مُعدِّل set عام وليس init. الخصائص required مدعومة.
مطابقة الأعمدة حسّاسة لحالة الأحرف. تترك أعمدة النتائج المفقودة الخصائص على قيمتها الافتراضية، بينما يتم تجاهل أعمدة النتائج الإضافية. لا يقوم driver بتوسيع القيم أو تضييقها. وباستثناء التمثيلات البديلة المذكورة أدناه، يجب أن يكون نوع إطار العمل الخاص بالعمود قابلاً للإسناد إلى نوع الخاصية، ويؤدي عدم التطابق إلى ظهور InvalidOperationException. ولذلك فإن خاصية من النوع object تقبل أي عمود. يقرأ QueryAsync<T> كلاً من هذه الأعمدة مباشرةً إلى خاصية مطابقة: يقبل كل صف أيضاً الصيغة القابلة لقيمة NULL من نوع خاصيته (long? وDateOnly? وما إلى ذلك)، سواء أكان العمود Nullable(...) أم لا. أما الخاصية من نوع قيمي غير قابل لقيمة NULL على عمود Nullable(T) فتُقبل عند التسجيل، لكنها تُطلق استثناءً عند وصول قيمة NULL. تُطابَق الأغلفة مثل LowCardinality(T) وSimpleAggregateFunction(f, T) وObject(T) تماماً كما لو كانت T. الأعمدة المركّبة مدعومة أيضاً، وتأخذ نوع إطار العمل المذكور في مرجع أنواع القراءة: Array(T) إلى T[]، وTuple(...) إلى System.Tuple<...>، وNested(...) إلى Tuple<...>[]، وJSON إلى JsonObject (أو string في وضع JsonReadMode=String)، وVariant/Dynamic إلى object. ويمثّل العمود من نوع Map(K, V) حالة خاصة: فالخاصية من نوع List<KeyValuePair<K, V>> أو KeyValuePair<K, V>[] تُقرأ عبر المسار الخالي من التغليف (box-free)، وتحافظ على الترتيب على السلك وعلى أي مفاتيح مكرّرة، في كلا وضعي MapReadMode. أما خاصية Dictionary<K, V> فتعمل في الوضع الافتراضي فقط. ويجب أن يتطابق نوعا المفتاح والقيمة تماماً، لذا يتطلب Map(String, Nullable(Int32)) النوع KeyValuePair<string, int?>. وحين يتيح العمود أكثر من نوع خاصية (عمود DateTime بصيغة DateTime أو DateTimeOffset أو DateOnly، وعمود String بصيغة string أو byte[])، فإن نوع الخاصية المُعلَن هو ما يحدّد التمثيل. وهذه التمثيلات البديلة تخصّ مسار POCO، لذا يوفّرها QueryAsync<T> بينما لا يوفّرها MapTo<T>. عند التكرار على القارئ يدويًا، استخدم ClickHouseDataReader.MapTo<T>() لتحويل الصف الحالي إلى POCO مسجَّل دون تحريك القارئ إلى الصف التالي:
استخدم MapTo<T> عندما تحتاج إلى إدارة حلقة القارئ بنفسك — على سبيل المثال لمزج الوصول المباشر إلى الأعمدة مع تحويل الصفوف إلى كائنات POCO. فهو يقرأ الصف عبر القيم المُعلَّبة الخاصة بالقارئ، لذا لا يوفر أنواع الخصائص البديلة المذكورة أعلاه، كما أنه يستهلك ذاكرة أكثر من QueryAsync<T>. ويُفضَّل استخدام QueryAsync<T> إذا كنت تحتاج إلى الصفوف فقط؛ راجع اختيار مسار التحويل إلى كائنات للاطلاع على الأرقام. يُطبَّق محوّل قيم القراءة المُعرَّف على مستوى العميل أو لكل استعلام على كلا المسارين، ولا يعطّل القراءة الخالية من التغليف. يحوّل الـ driver كل عمود باستخدام التحميل الزائد المطابق للطريقة التي قرأ بها العمود: ConvertValue<T> المُنمَّط للعمود الخالي من التغليف، وConvertValue المُغلَّف للعمود المركّب. احرص على تنفيذ التحميلين الزائدين بشكل متسق، وإلا فسيعطي العمود نفسه نتائج مختلفة باختلاف المسار. عند تهيئة LoggerFactory، يُصدر كلٌّ من RegisterPocoType<T>() وRegisterBinaryInsertType<T>() سجلًا على مستوى Debug (ضمن الفئة ClickHouse.Driver.Client) يبيّن الخصائص التي طابقت الأعمدة، وتلك التي جرى تخطيها وسبب ذلك. راجع التسجيل والتشخيصات.

معلمات SQL

في ClickHouse، الصيغة القياسية لمعلمات الاستعلام في استعلامات SQL هي {parameter_name:DataType}. أمثلة:
تُمرَّر معلمات SQL ‏‘bind’ كمعلمات استعلام في HTTP URI، لذا فإن استخدام عدد كبير جدًا منها قد يؤدي إلى ظهور استثناء “URL طويل جدًا”. استخدم InsertBinaryAsync لإدراج كميات كبيرة من البيانات لتجنّب هذا القيد.

العناصر النائبة @name بأسلوب ADO

يقبل الـ driver أيضًا العناصر النائبة @name التي تُصدرها أدوات ORM مثل Dapper. وهي مجرد تسهيل على جهة العميل: فقبل إرسال الطلب، يُعاد كتابة كل عنصر منها إلى الصيغة {name:ResolvedType}، بحيث لا يرى الـ server علامة @ مطلقًا. راجع تحديد النوع لمعرفة كيفية اختيار النوع. ويُفضّل استخدام الصيغة الصريحة {name:Type} كلما أمكن ذلك. أما @name الذي لا يقابله أي parameter فيُترك كما هو ليرفضه الـ server. والمطابقة حساسة لحالة الأحرف، لذا فإن @ID لا يرتبط بـ parameter باسم id.
لتعطيل إعادة الكتابة، فعّل مفتاح AppContext المسمى ClickHouse.Driver.DisableReplacingParameters قبل أول استخدام للـ driver. عندئذٍ تتوقف إعادة كتابة النص فقط، أما الـ parameters فتُرسل كما هي، ومن ثمّ تظل الاستعلامات المكتوبة بصيغة {name:Type} الأصلية تعمل.

معلمات Identifier

يتيح لك نوع المعلمة Identifier ربط اسم قاعدة بيانات أو جدول أو عمود بأمان بدلًا من استخدام قيمة حرفية نصية بين علامتَي اقتباس. استخدمه بصيغة {name:Identifier} في SQL، أو عبر تعيين ClickHouseDbParameter.ClickHouseType = "Identifier":
تُرسَل القيمة كما هي حرفيًا، ويستبدلها الخادم كمُعرّف SQL خام، مع تطبيق الإحاطة بعلامات backtick وإفلات الأحرف الخاصة وفق آليته الخاصة. ويمكن نقل المعرّفات التي تحتوي على أحرف خاصة (بما في ذلك علامات backtick) ذهابًا وإيابًا بأمان.

معرّف الاستعلام

يُخصَّص لكل استعلام معرّف فريد query_id يمكن استخدامه لجلب البيانات من جدول system.query_log أو لإلغاء الاستعلامات طويلة التشغيل. يمكنك تحديد معرّف استعلام مخصّص عبر QueryOptions:
إذا كنت تحدد QueryId مخصصًا، فتأكد من أنه فريد في كل استدعاء. ويُعد GUID عشوائيًا خيارًا جيدًا.

تعيين نوع المعلمة المخصّص

عند استخدام المعلمات بأسلوب @ (مثل WHERE id = @id)، يستنتج برنامج التشغيل تلقائيًا نوع ClickHouse من نوع القيمة في ‎.NET. على سبيل المثال، يُعيَّن int إلى Int32.
سلوك معلمات DateTime المستنتجةبالنسبة إلى المعلمات بأسلوب @ التي لا تحتوي على تلميح {name:Type} في SQL ولم يُعيَّن لها ClickHouseType، تُستنتج القيم التي تمثل نقطة زمنية على أنها DateTime('UTC') بدلًا من DateTime مجردة. ويُرسَل DateTime الذي تكون فيه قيمة Kind هي Utc أو Local، وجميع قيم DateTimeOffset، على هيئة DateTime('UTC')، مع الحفاظ على النقطة الزمنية عبر أي server timezone.تكون للتلميحات الصريحة ({name:DateTime}) أولوية أعلى من الاستدلال، وهي الطريقة الموصى بها لبناء الاستعلامات.
لتجاوز هذه الإعدادات الافتراضية، عيّن ParameterTypeResolver في ClickHouseClientSettings. ويكون ذلك مفيدًا عندما تريد استخدام DateTime64(3) لجميع معلمات DateTime بدقة الملّي ثانية، أو استخدام قيمة scale محددة لجميع قيم Decimal، من دون تعيين ClickHouseType لكل معلمة على حدة. استخدام DictionaryParameterTypeResolver لتعيينات الأنواع البسيطة:
IParameterTypeResolver مخصّص للحالات المتقدمة: للاستدلال المعتمد على القيمة أو المستند إلى الاسم، نفِّذ الواجهة IParameterTypeResolver مباشرةً. أرجِع null للانتقال إلى الاستدلال الافتراضي:
يمكنك أيضًا تعيين محلِّل لاستعلام واحد عبر QueryOptions.ParameterTypeResolver. وعند تعيينه، تكون له أولوية أعلى من المحلِّل على مستوى العميل. ترتيب أولوية تحديد النوع: المحلِّل ليس سوى خطوة ضمن سلسلة ترتيب الأولوية. من الأعلى إلى الأدنى أولوية:
  1. تعيين ClickHouseType صراحةً على المعلَمة
  2. تلميح نوع SQL من الصيغة {name:Type} في الاستعلام
  3. IParameterTypeResolver (من QueryOptions.ParameterTypeResolver، مع الرجوع إلى ClickHouseClientSettings.ParameterTypeResolver)
  4. استنتاج النوع المضمَّن (TypeConverter.ToClickHouseType)
يعمل المحلِّل أيضًا مع مسار ADO.NET ClickHouseConnection — إذ ترث الاتصالات المُنشأة من العميل هذه الإعدادات.

تنسيق مخصص لقيم المعلمات

IParameterFormatter هو خطاف يحدّد كيفية تسلسل قيم المعلمات. استخدمه عندما لا يتوافق التنسيق المضمّن (مثل دقة DateTime، والإعدادات المحلية لـ Decimal، وإفلات السلاسل النصية، وتمثيل الأرقام) مع ما يتوقعه المخطط أو الأدوات اللاحقة. عيّن ParameterFormatter في ClickHouseClientSettings لتثبيت منسّق لجميع الاستعلامات المعلَّمة بمعلمات. يتلقى المنسّق القيمة، واسم نوع ClickHouse الذي جرى تحديده، واسم المعلمة، ويُرجع التمثيل النصي الذي يُرسَل إلى الخادم. أعد null للرجوع إلى المنسّق الافتراضي. استخدام DictionaryParameterFormatter للتنسيق البسيط لكل نوع CLR على حدة:
IParameterFormatter مخصّص للحالات المتقدمة:
يمكنك أيضًا تعيين منسّق لكل استعلام عبر QueryOptions.ParameterFormatter. وعند تعيينه، تكون له الأولوية على المنسّق على مستوى العميل. القيم المركّبة: يُطبَّق المنسّق على معلمات المجموعات ذات المستوى الأعلى، وكذلك على كل عنصر داخل القيم المركّبة (Array, Tuple, Map, Nullable, LowCardinality, Variant). على سبيل المثال، تؤدي مطابقة typeof(int) إلى تنسيق كل عنصر Int32 داخل Array(Int32) على حدة. الإحاطة بعلامات الاقتباس المفردة في السياقات المركّبة: بالنسبة إلى أنواع ClickHouse الشبيهة بالسلاسل النصية (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) المضمّنة داخل قيمة حرفية مركّبة، يحيط برنامج التشغيل مخرجات المنسّق بعلامات اقتباس مفردة، لكنه لا يجري إفلاتًا لمحتواها. إذا كانت السلسلة التي يعيدها المنسّق تحتوي على علامة اقتباس مفردة أو شرطة مائلة عكسية غير مُفلَتة، فستصبح القيمة الحرفية المركّبة غير صحيحة، وسيرفض الخادم الاستعلام. تُستخدم معلمات السلاسل النصية ذات المستوى الأعلى (غير المضمّنة داخل قيمة مركّبة) كما هي، من دون إحاطة، لذا لا يلزم الإفلات هنا. أولوية المنسّق:
  1. IParameterFormatter (من QueryOptions.ParameterFormatter، مع الرجوع إلى ClickHouseClientSettings.ParameterFormatter). إذا أعاد قيمة غير null، فستُستخدم هذه القيمة.
  2. التنسيق المضمّن الخاص بالنوع في HttpParameterFormatter.
لا يُستشار المنسّق لقيم null أو DBNull؛ إذ تُسلسَل هذه القيم دائمًا باعتبارها مؤشر ClickHouse للقيم null (\N).

تحويل مخصص لقيم القراءة

يتيح لك IReadValueConverter تحويل القيم التي يعيدها قارئ البيانات بعد إلغاء التسلسل، من دون تغيير نوع CLR الخاص بها. ومن الاستخدامات الشائعة: تعيين DateTime.Kind = Utc لعمود DateTime لا يحتوي على timezone، أو إزالة المسافات الزائدة من السلاسل النصية أو تطبيعها، أو إجراء معالجة لاحقة على عمود JSON قبل أن يصل إلى شيفرة التطبيق. عيّن ReadValueConverter في ClickHouseClientSettings لتطبيق محوّل على جميع عمليات القراءة. ويُستدعى المحوّل مرة واحدة لكل عمود في كل صف عبر كلٍّ من المسار المغلّف (GetValue) والمسار العام (GetFieldValue<T>). وعند عدم تعيين أي محوّل، لا توجد أي كلفة إضافية — إذ يعيد القارئ القيم مباشرةً. استخدام DictionaryReadValueConverter لإجراء تحويل بسيط لكل نوع CLR:
تمرّ القيم التي لم يُسجَّل نوع CLR الخاص بها وقت التشغيل باستخدام For<T> كما هي من دون تغيير. ويجري التوجيه وفق نوع CLR المطابق تمامًا، لذا سجّل النوع الفعلي الذي يُنتجه القارئ (على سبيل المثال، For<JsonObject> لعمود JSON في JsonReadMode.Binary). IReadValueConverter مخصّص للحالات المتقدمة: إذا كنت بحاجة إلى التوجيه استنادًا إلى سلسلة النوع من جانب ClickHouse (على سبيل المثال، للتمييز بين DateTime وDateTime('UTC') — إذ يظهر كلاهما على أنهما نوع CLR نفسه)، فنفّذ IReadValueConverter مباشرةً:
يجب أن يحافظ المحوّل على نوع CLR وقت التشغيل؛ إذ لا يتم تمرير البيانات الوصفية للأعمدة (GetFieldType, GetSchemaTable) عبره، ويجب أن تظل متسقة مع ما يتم إرجاعه. يمكنك أيضًا تعيين محوّل لكل استعلام عبر QueryOptions.ReadValueConverter؛ وعند تعيينه، تكون له الأولوية على المحوّل على مستوى العميل. حدود الإرسال: يُستدعى المحوّل مرة واحدة لكل عمود مع قيمة الخلية كاملة بعد فك التسلسل، وهو لا يتعمق تكراريًا داخل الحاويات المركبة. بالنسبة إلى عمود Array(Int32)، تكون القيمة المُمرَّرة هي int[]؛ وبالنسبة إلى Tuple(Int32, String)، تكون ITuple. أي تحميل زائد يُنفَّذ: يجب أن يتوافق التحميلان الزائدان، لأن التحميل الذي يستدعيه الـ driver يعتمد على الطريقة التي قرأ بها المستدعي العمود:
  • ConvertValue<T> — الوصولات المُحدَّدة النوع GetByte، وGetSByte، وGetInt16/32/64، وGetUInt16/32/64، وGetFloat، وGetDouble، وGetGuid، وGetDateTime، وGetIPAddress، وGetBigInteger وGetFieldValue<T>، إضافةً إلى كل عمود خالٍ من التغليف في مسار قراءة POCO.
  • ConvertValue (المغلّف) — GetValue، وGetValues، والمُفهرِسات، وGetChar، وGetTuple، و المسارات القسرية في GetBoolean وGetDecimal وGetString.
لا يُشغّل IsDBNull أي محوّل على الإطلاق: فهو يقرأ راية القيمة الفارغة مباشرةً، لذا لا يمكن لأي محوّل أن يغيّر ما إذا كانت القيمة تُعدّ فارغة. وكذلك يتجاوزه TryGetEnumOrdinal — راجع قراءة الترتيب في enum. يعمل المحوّل مع مسار ClickHouseConnection في ADO.NET — وترث الاتصالات المُنشأة من العميل هذه الإعدادات.

البث الخام

استخدم ExecuteRawResultAsync لبث نتائج الاستعلام مباشرةً بتنسيق محدد، متجاوزًا قارئ البيانات. يفيد ذلك عند تصدير البيانات إلى ملفات أو تمريرها إلى أنظمة أخرى:
التنسيقات الشائعة: JSONEachRow، CSV، TSV، Parquet، Native. راجع توثيق التنسيقات للتعرّف على جميع الخيارات.

ضغط النقل لكل استعلام

افتراضيًا، يتفاوض العميل على zstd, lz4, gzip, deflate عندما تكون Compression=true (وهو الإعداد الافتراضي في سلسلة الاتصال)، ويتولى فك ترميز الدفق بنفسه تلقائيًا وبشفافية. بالنسبة إلى عمليات التصدير الخام (مثل Parquet وArrow وNative)، قد ترغب في التفاوض على codec مختلف (مثل zstd أو lz4) لتحقيق مفاضلة بين استهلاك CPU وعرض النطاق الترددي من دون تغيير الإعداد على مستوى الاتصال بالكامل. يضبط كلٌّ من QueryOptions.AcceptEncoding وClickHouseCommand.AcceptEncoding ترويسة HTTP ‏Accept-Encoding لطلب واحد، مع استبدال أي قيمة افتراضية كانت معيّنة، ويفرضان enable_http_compression=1 على URL (وهو ما يتطلبه ClickHouse قبل أن يلتزم بـ Accept-Encoding).

تهيئة HttpClient

لا شيء يحتاج إلى تهيئة: فالـ HttpClient الذي ينشئه الـ driver يترك AutomaticDecompression عند DecompressionMethods.None ويتولى الـ driver فك ترميز الاستجابات بنفسه، لذا لا يُحذف Content-Encoding من دون علمك، ويصلك الـ body الخام تمامًا كما أرسله الـ server.
إذا وفّرت HttpClient خاصًا بك، فاترك AutomaticDecompression معطّلًا أيضًا. فهو ليس إعدادًا يخص جانب الاستجابة فحسب: فعند الإرسال يقوم الـ handler بـإضافة كل خوارزمية موجودة في قناعه وغائبة عن Accept-Encoding الصادر. لذا فإن handler يحمل GZip | Deflate يحوّل AcceptEncoding = "lz4" الصريح إلى lz4, gzip, deflate، ويحوّل "identity" الصريحة إلى identity, gzip, deflate في تنسيق النقل — وبما أن ClickHouse يحسم الـ header وفق أفضلية codec ثابتة خاصة به (متجاهلًا الترتيب وقيم q)، فقد يردّ بـ codec لم تطلبه قط، ثم يفك الـ handler ترميزه ويحذفه فلا تلاحظ حتى أن ذلك قد حدث. أما ترك القناع معطّلًا فيُبقي العرض المقدَّم مطابقًا تمامًا لما اخترته.
إذا طلب AcceptEncoding codec لا يستطيع الـ driver فك ترميزه (snappy)، فإن ExecuteRawResultAsync وحدها هي الآمنة. أما ExecuteReaderAsync وExecuteScalarAsync وExecuteNonQueryAsync فتفشل برمي NotSupportedException يذكر اسم الـ codec (وكانت سابقًا تحلل الـ compressed bytes على أنها format النتيجة فتنتج مهملات).

أجسام رسائل الخطأ

عندما يستجيب الخادم بحالة 4xx/5xx وكان قد جرى تعيين enable_http_compression=1، فإنه يضغط جسم الخطأ باستخدام الـ codec نفسه الذي كان سيستخدمه في الاستجابة الناجحة. ويفك برنامج التشغيل ترميز هذه الأجسام لكل codec يدعمه (lz4, zstd, gzip, deflate, br/brotli)، بحيث تكون الرسالة الظاهرة في ClickHouseServerException قابلة للقراءة. أما ما عدا ذلك (snappy, …) فيُرجع رسالة بديلة تذكر اسم الـ codec وتشير إلى system.query_log للاطلاع على نص الخطأ الأصلي.

فك ضغط الاستجابة

لا يطلب Accept-Encoding من الخادم سوى ضغط الاستجابة — ويبقى فك ترميزها مهمة جهة أخرى. ويتولى driver ذلك بنفسه اعتماداً على Content-Encoding الخاص بالاستجابة، ولذلك تعمل جميع واجهات القراءة المعتادة (ExecuteReaderAsync، ExecuteScalarAsync، ExecuteNonQueryAsync، QueryAsync<T>، Dapper، EF Core، linq2db) مع الاستجابة المضغوطة دون الحاجة إلى أي تهيئة. وهو يفك ترميز lz4 وzstd وgzip وdeflate وbr؛ أما snappy فغير مدعوم. افتراضياً يُعلن driver عن zstd, lz4, gzip, deflate، ويردّ ClickHouse بـ zstd. ولاختيار غير ذلك، عيّن Accept-Encoding بنفسك — على مستوى client بالكامل:
لكل استعلام، وهو ما تكون له الأسبقية:
أو في connection string، لمستخدمي ORM الذين لا يتعاملون مطلقًا مع ClickHouseClientSettings:
كما أنّ ضبطه يفرض enable_http_compression=1 في الـ URL، وهو ما يشترطه ClickHouse قبل أن يأخذ الـ header بعين الاعتبار أصلًا — بما في ذلك عندما تكون UseCompression بقيمة false، إذ إنّ تسمية codec صراحةً تُعدّ طلبًا له. وفي حال عدم ضبط أي قيمة، فإنّ UseCompression=false لا ترسل أي Accept-Encoding إطلاقًا. يمكن ضبط Accept-Encoding في أربعة مواضع، ويُعتمد أوّل موضع منها يسمّي codec:
  1. QueryOptions.AcceptEncoding (أو ClickHouseCommand.AcceptEncoding)
  2. CustomHeaders["Accept-Encoding"] على مستوى الاستعلام
  3. CustomHeaders["Accept-Encoding"] على مستوى العميل
  4. ClickHouseClientSettings.AcceptEncoding، أو الكلمة المفتاحية AcceptEncoding في سلسلة الاتصال
وإذا لم يسمِّ أيٌّ منها codec، يرسل الـ driver قائمته الافتراضية. أمّا القيمة التي لا تسمّي أي codec (null، أو فارغة، أو مسافات بيضاء، أو فواصل فقط) فتُعدّ غير مضبوطة ويُنتقل إلى الموضع التالي. ولتعطيل الضغط، استخدم identity. الخادم، لا العميل، هو من يختار الـ codec. يفحص ClickHouse قيمة Accept-Encoding بحثًا عن الرموز وفق ترتيب أفضلية ثابت خاص به — zstd > br > lz4 > snappy > gzip > deflate — متجاهلًا الترتيب الذي تسردها به وأي قيم q. وبذلك يكون الـ header إعلانًا عن القدرات لا طلبًا مُلزمًا، والسبيل الوحيد للتأثير في الاختيار هو استبعاد بعض الرموز. وتتضمّن القائمة الافتراضية zstd، لذا يُجاب على الاستعلام الافتراضي بضغط zstd، بينما تعمل بقية الرموز كخيار احتياطي. ويمكن فكّ ترميز br غير أنّه غير مُعلن عنه افتراضيًا. أمّا المقارنة بين الـ codecs من حيث حجم الـ payload واستهلاك CPU على الخادم وعلى العميل، فتتوقّف على بياناتك ووصلتك وعلى قيمة http_zlib_compression_level في الخادم (القيمة الافتراضية المُرفقة: 3) — راجع ضبط الضغط.
  • http_zlib_compression_level. ينطبق هذا الإعداد على كل codec خاص بـ HTTP، وقيمته الافتراضية هي 3. وينبغي ضبط هذه القيمة استنادًا إلى بياناتك وسرعة الوصلة واستهلاك CPU.
  • عميل مقيَّد بـ CPU على وصلة سريعة. يفكّ الـ driver ترميز جسم الاستجابة على الـ thread المستدعي، لذا عندما لا تكون الشبكة هي عنق الزجاجة، قد تصبح سرعة فكّ الترميز في جهة العميل هي العامل المحدِّد.
اطلب codec مختلفًا على مستوى الاستعلام، أو على مستوى العميل بأكمله، متى انطبقت أي من الحالتين:
ولأن القرار يُتخذ بناءً على الاستجابة، فإن الـ body يُفك ضغطه كلما أشار رأس Content-Encoding الخاص به إلى ذلك، أيًّا كان ما طُلب: فإن كان الرأس غائبًا أو قيمته identity يمرّ المحتوى دون تغيير، وإن كان codec مدعومًا فيُفك ضغطه، وأي شيء آخر يُثير error يذكر اسمه. ولا خطر من فك الضغط مرتين — فإذا كان handler مُقدَّم من الـ caller قد فكّ ضغط الـ body عبر AutomaticDecompression، فإنه يزيل أيضًا Content-Encoding، وبذلك يرى الـ driver بيانات plaintext فيتركها كما هي. النتائج الخام لا تُعلن عن أي codec. تُسلّمك ExecuteRawResultAsync (وكذلك PostStreamAsync / InsertRawStreamAsync العامتان) الـ body كما هو حرفيًا، لذا ما لم تحدّد codec بنفسك فهي لا تطلب أي codec إطلاقًا — فلا شيء في الـ driver يفك ترميز مثل هذا الـ body، ومن ثَمّ فإن تقديم codec هناك سيحوّل عملية التصدير إلى File مضغوط دون أن تدري. لذا فالقاعدة بسيطة ولا تتأثر بكيفية تهيئة أي HttpClient: الـ body الحرفي يصل تمامًا كما أرسله الـ server، والـ server يرسل plaintext ما لم تطلب codec. وطلبُ codec (على مستوى الـ client بالكامل أو لكل query) هو الطريقة التي تصدّر بها compressed bytes عن قصد. ويظل تحديد AcceptEncoding صراحةً (على أي من المستويين) ساريًا على الـ requests الخام، كما تفك ClickHouseRawResult.ReadDecompressedStreamAsync() ترميز النتيجة متى أردت ذلك؛ أما ReadAsStreamAsync وReadAsByteArrayAsync وReadAsStringAsync وCopyToAsync فتُعيد دائمًا البايتات تمامًا كما وصلت.
اقرأ الـ stream المُعاد حتى نهايته قبل خروجه من الـ scope، كما في المثال أعلاه. فعندما تكون الاستجابة مضغوطة فعلاً، تحصل على decoder مُنشأ باستخدام leaveOpen، ومن ثم فإن التخلص منه يُبقي الاستجابة سليمة؛ أما عندما تكون غير مضغوطة، فتحصل على stream محتوى HTTP ذاته، وبالتالي فإن التخلص منه يُنهي الـ body. وفي كلتا الحالتين يمتلك ClickHouseRawResult الاستجابة — فلا تستدعِ أعضاء القراءة الأخرى فيه بعد التخلص من الـ stream. والتخلص من ClickHouseRawResult مطلوب دائماً وكافٍ بذاته: فهو يحرّر الاستجابة وأي decoder أُدرج هنا (إذ تحتفظ الـ decoders بـ buffers من الـ pool). ولذلك فإن await using أعلاه اختياري، ولا ضرر من إبقائه. والاستدعاءات المتكررة الـ sequential تُعيد الـ stream نفسه؛ كما أن النوع غير آمن للاستخدام بشكل concurrent. راجع Select_007_ResponseCompression.cs للاطلاع على مثال قابل للتشغيل.

ضغط الإدراج (الطلب)

Zstd هو برنامج الترميز Default لعمليات الإدراج: تكون القيمة الأولية لـ InsertOptions.Compressor هي ZstdCompressor.Default، أي zstd بالمستوى 3. اضبطه على ضاغط آخر لتغيير برنامج الترميز، أو على null لإرسال الـ body دون ضغط.
يأتي الـ driver مزوّدًا بأربعة codecs. لكل منها instance باسم Default، ومُنشئ (constructor) يقبل المستوى وحجم الـ write buffer:
شارك نسخ الضاغط (instances). كل Default هو نسخة مشتركة واحدة، والضواغط الأربعة جميعها آمنة للاستخدام من عدة مسارات تنفيذ (threads) في الوقت نفسه — وهو ما يحدث عندما تكون قيمة InsertOptions.MaxDegreeOfParallelism أكبر من 1، إذ تستخدم كل عملية insert ضاغطًا واحدًا لكل batch. ولا ينفّذ أيٌّ منها IDisposable. أنشئ نسختك الخاصة مرة واحدة وأعد استخدامها، بالطريقة نفسها التي يُستخدم بها Default.
الواجهة IClickHouseCompressor عامة، ولا يتطلب تنفيذها سوى توفير عضوين اثنين:
يجب أن يقبل الخادم قيمة Content-Encoding التي تحددها. أما بقية الأعضاء — Decompress وMethodByte وMaxEncodedLength وEncode وDecode — فلها تطبيقات افتراضية تُطلق NotSupportedException، لذا تجاوز ما يحتاجه codec الخاص بك فقط. نفّذ Decompress لفك ترميز أجسام الاستجابات إلى جانب ضغط الطلبات، وأطلق InvalidDataException من التدفق الذي يُعيده عندما يكون الجسم تالفًا أو بتنسيق خاطئ. يتحكم InsertOptions.Compressor في عمليات الإدراج الثنائية فقط. أما أجسام الطلبات الأخرى في driver فتُضغط وفق قواعد مختلفة، ولا يمر أي منها عبره:
  • كل طلب بنص SQL (ExecuteReaderAsync، ExecuteScalarAsync، ExecuteNonQueryAsync، QueryAsync<T>، ExecuteRawResultAsync، طبقة ADO.NET) يرسل عبارته مع Content-Encoding: gzip متى كانت قيمة UseCompression هي true — أي افتراضيًا. وcodec غير قابل للضبط هنا: فـAcceptEncoding يوجّه الاستجابة فقط، وبالتالي فالخيار إما gzip أو لا شيء. أما Compression=false فيرسل العبارة دون ضغط. والعبارات صغيرة الحجم، لذا نادرًا ما يستحق الأمر عناء التفكير — لكن من المفيد معرفته عند مراقبة الطلبات عبر proxy أو أثناء packet capture.
  • الجسم متعدد الأجزاء — أي query تُرسل parameters الخاصة به على هيئة form data (UseFormDataParameters=true) — يُرسل دائمًا دون ضغط، أيًا كانت قيمة UseCompression.
  • الرفع الخام (InsertRawStreamAsync، PostStreamAsync) يعتمد على flag خاص بكل استدعاء، ولا يأخذ في الحسبان UseCompression ولا InsertOptions.Compressor: gzip عند ضبط الـflag، ودون ضغط فيما عدا ذلك. ولاحظ أن parameter المسمى useCompression في InsertRawStreamAsync قيمته الافتراضية true، لذا يُضغط الرفع الخام بـgzip ما لم تمرر false — حتى مع ضبط Compression=false على العميل.

ضبط الضغط

يقايض الضغط استهلاك CPU مقابل توفير البايتات، وما إذا كان ذلك مجديًا يعتمد بشكل شبه كامل على سرعة وصلتك مقارنةً بسرعة تنفيذ الـ codec. ولا يوجد إعداد واحد يناسب الجميع.

الرقم الوحيد الذي يحسم الأمر

يستحق الضغط العناء طالما أن الـ codec أسرع من الشبكة. هذا الـ threshold أقل مما يتوقعه معظم الناس في مسار القراءة، لأن ClickHouse يضغط استجابات HTTP على خيط واحد داخل الـ buffer الخاص بالمخرجات. ووفقًا لقياسات أُجريت على service في ClickHouse Cloud بـ 16 vCPU (hits، RowBinary، المستوى 3)، ينتج الـ server مخرجات مضغوطة بمعدل يتراوح تقريبًا بين 100 و200 ميغابايت/ثانية. لذا، مع النتائج الكبيرة، وبافتراض معالجة query واحد في كل مرة، تنتفي جدوى الضغط عند حدود 100 ميغابايت/ثانية تقريبًا. وعادةً ما يتجاوز stream واحد من HTTPS داخل region سحابي واحد هذا الحد، بينما يبقى دونه أي اتصال يعبر الـ public internet أو شبكة VPN أو حدود region. أما مسار الـ insert فيحتمل الضغط حتى سرعات اتصال أعلى، لأن الـ client لديك يضغط على core مخصص له، وهو عادةً أسرع من ضغط استجابة الـ server.

دليل تقريبي حسب النشر

ثلاثة أمور لا يعكسها هذا الجدول:
  • تكلفة الخروج (Egress): إذا كنت تُحاسَب على نقل البيانات، فللبايتات ثمن يتجاوز زمن الاستجابة، وهذا يدفع نحو ضغط أعلى بغض النظر عن سرعة الوصلة.
  • النتائج الصغيرة: كل ما سبق يخص الحمولات الكبيرة. أما في الاستجابات الصغيرة فلا يكاد يكون لاختيار الـ codec أثر، ويغلب عليها العبء المصاحب لكل طلب.
  • عمليات الإدراج المتوازية ترفع عتبات الإدراج. كل رقم إنتاجية أعلاه يخص خيط تنفيذ واحدًا. القيمة الافتراضية لـ InsertOptions.MaxDegreeOfParallelism هي 1، لكن رفعها يضغط الدفعات على التوازي، فيتناسب معدل الترميز الإجمالي للعميل تقريبًا مع عدد الأنوية التي تخصصها له. لذا قد يظل الضغط مجديًا لإدراج متوازٍ على وصلة سريعة، عند سرعات تتجاوز بكثير تلك التي يتوقف عندها الضغط عن كونه مجديًا في الإدراج أحادي الخيط. تعامل مع صفوف الإدراج في الجدول باعتبارها حدًا أدنى، وإذا كنت تُدرج على دفعات متوازية أصلًا، فأعد الاختبار قبل أن تستنتج أن وصلتك أسرع من أن تستفيد من الضغط.
أما مسار القراءة فلا يتوازى إلا عبر استعلامات متعددة.

اختيار الـ codec

المستويات

يُتحكَّم في ضغط الاستجابة عبر إعداد خادم واحد هو http_zlib_compression_level، وهو ينطبق على كل codec في HTTP وليس على zlib وحده. قيمته الافتراضية 3. لا تغيّره ما لم يكن لديك سبب مبني على قياس فعلي. فوق القيمة الافتراضية، يمنحك تقليصًا ضئيلًا جدًا في الحجم مقابل استهلاك كبير في CPU (فمع zstd، الانتقال من 3 إلى 6 يضاعف تقريبًا استهلاك CPU على الخادم مقابل توفير نحو 14% من البايتات فقط)، ويصبح سلوك br سيئًا للغاية. أما دون القيمة الافتراضية، عند المستوى 1، فالصورة تختلف فعلًا: يصبح lz4 أقل كلفة بكثير، ويفقد zstd أفضليته عليه من حيث CPU. ويمكنك ضبطه لكل query إن احتجت إلى ذلك:

قياس نقطة التقاطع لديك

أسرع طريقة لتحسين اختيارك لـ codec ومستوى الضغط هي قياس زمن تنفيذ الاستعلام نفسه مع عدد من الـ codecs ثم المقارنة بين النتائج.
وللاطلاع على الجانب الخاص بالخادم من الصورة نفسها، اقرأ ProfileEvents من system.query_log — واضبط QueryOptions.QueryId حتى تتمكن من العثور على الصف:
هناك فخ إذا أجريت هذا الاختبار المرجعي بنفسك: استخدام LIMIT n وحده دون ORDER BY يُرجع صفوفاً مختلفة في كل تشغيل، لذا يضغط كل تكرار بيانات مختلفة وتصبح النسب مجرد ضوضاء. قارن دائماً مقابل مجموعة نتائج ثابتة.

الإدراج من raw stream

استخدم InsertRawStreamAsync لإدراج البيانات مباشرة من ملف أو من تدفقات الذاكرة بصيغ مثل CSV أو JSON أو Parquet أو أي صيغة مدعومة في ClickHouse. الإدراج من ملف CSV:
يأخذ الـ driver ملكية الـ stream. تتخلّص InsertRawStreamAsync وPostStreamAsync من الـ stream الذي تمرّره إليها بمجرد انتهاء الطلب، سواء نجح أو فشل. لا تتخلّص منه بنفسك ولا تُعِد استخدامه بعد ذلك — ولهذا السبب لا يغلّف المثال أعلاه الـ FileStream داخل using.فأي using خاص بك سيُنفَّذ بعد أن يكون الـ driver قد تخلّص من الـ stream فعلياً. وبالنسبة إلى FileStream أو MemoryStream يكون هذا الاستدعاء الثاني غير ضار، أما مع stream يُعيد Dispose الخاص به buffer مأخوذاً من pool أو يُنقص عدّاد المراجع، فسيؤدي ذلك إلى تحرير الـ resource مرتين.ولا تنتقل الملكية إلا بعد قبول الوسائط: فإذا أطلق الاستدعاء ArgumentException أو ArgumentNullException بسبب غياب table أو stream أو format، فإن الـ stream يبقى ملكك.
راجع توثيق إعدادات الـ formats للاطلاع على خيارات التحكم في سلوك استيعاب البيانات.

المزيد من الأمثلة

للاطلاع على المزيد من أمثلة الاستخدام العملية، راجع دليل الأمثلة في مستودع GitHub.

ADO.NET

توفّر المكتبة دعمًا كاملًا لـ ADO.NET من خلال ClickHouseConnection وClickHouseCommand وClickHouseDataReader. وتُعد واجهة برمجة التطبيقات هذه ضرورية للتكامل مع ORM ‏(Dapper وLinq2db)، وكذلك عند الحاجة إلى طبقات تجريد قواعد البيانات القياسية في .NET.

إدارة دورة الحياة باستخدام ClickHouseDataSource

أنشئ الاتصالات دائمًا عبر ClickHouseDataSource لضمان الإدارة السليمة لدورة الحياة وتجميع الاتصالات. يدير DataSource مثيل ClickHouseClient واحدًا داخليًا، وتشترك جميع الاتصالات في تجمّع الاتصالات HTTP الخاص به.
في حال استخدام حقن التبعيات:
لا تُنشئ ClickHouseConnection مباشرةً في شيفرة الإنتاج. فكل إنشاء مباشر له ينشئ عميل HTTP جديدًا وتجمّع اتصالات جديدًا، ما قد يؤدي إلى استنفاد المقابس عند ارتفاع الحمل:
بدلًا من ذلك، استخدم دائمًا ClickHouseDataSource أو شارِك مثيلًا واحدًا من ClickHouseClient.

استخدام ClickHouseCommand

أنشئ أوامر باستخدام اتصال لتنفيذ SQL:
طرق الأوامر:
  • ExecuteNonQueryAsync() - لعبارات INSERT وUPDATE وDELETE وDDL
  • ExecuteScalarAsync() - يعيد أول عمود من أول صف
  • ExecuteReaderAsync() - يعيد ClickHouseDataReader للتنقّل بين النتائج

استخدام ClickHouseDataReader

يتيح ClickHouseDataReader الوصول إلى نتائج الاستعلام مع الحفاظ على أنواع البيانات:

قراءة الترتيب الرقمي للـ enum

يُجسَّد العمود من نوع Enum8 أو Enum16 على هيئة الـ label الخاص به: فيُرجع GetFieldType النوع string، ويمنحك كلٌّ من GetString وGetValue وGetFieldValue<string> قيمة الـ label. أما الـ accessors الرقمية فتُطلق الاستثناء InvalidCastException عند استخدامها مع عمود enum، لأن القيمة المخزّنة هي سلسلة نصية. استخدم TryGetEnumOrdinal للحصول على الرقم الكامن خلف الـ label:
تُعيد true وتضبط value في حالة عمود Enum8/Enum16، وكذلك في حالة عمود Nullable(Enum...) تكون خليته not null. أمّا في حالة خلية NULL أو أي عمود ليس من نوع enum، فتُعيد false مع ضبط value على 0. القيمة الترتيبية هي القيمة signed القادمة من الـ wire، لذا يمكن أن تكون سالبة، كما يمكن أن تتجاوز القيمة الترتيبية لـ Enum16 حجم بايت واحد.

أفضل الممارسات

مدة الاتصال وتجميع الاتصالات

يستخدم ClickHouse.Driver المكوّن System.Net.Http.HttpClient في الخلفية. ويحتوي HttpClient على تجمّع اتصالات لكل endpoint. ونتيجة لذلك:
  • تُمرَّر جلسات قاعدة البيانات عبر اتصالات HTTP التي يديرها تجمّع الاتصالات.
  • يُعيد التجمّع استخدام اتصالات HTTP تلقائيًا.
  • قد تظل الاتصالات مفتوحة حتى بعد التخلّص من الكائنات ClickHouseClient أو ClickHouseConnection.
الأنماط الموصى بها:
عند استخدام HttpClient أو HttpClientFactory مخصّص، تأكد من ضبط PooledConnectionIdleTimeout على قيمة أقل من keep_alive_timeout الخاص بالخادم، لتجنّب الأخطاء الناتجة عن الاتصالات نصف المغلقة. القيمة الافتراضية لـ keep_alive_timeout في Cloud deployments هي 10 ثوانٍ.
تجنّب إنشاء عدة مثيلات من ClickHouseClient أو مثيلات ClickHouseConnection مستقلة من دون HttpClient مشترك. فكل مثيل ينشئ تجمّع الاتصالات الخاص به.

التعامل مع DateTime

  1. استخدم UTC كلما أمكن. خزّن الطوابع الزمنية في أعمدة DateTime('UTC') واستخدم DateTimeKind.Utc في الشيفرة الخاصة بك. هذا يزيل أي التباس متعلق بالمنطقة الزمنية.
  2. استخدم DateTimeOffset للتعامل الصريح مع المنطقة الزمنية. فهو يمثّل دائمًا لحظة زمنية محددة ويتضمن معلومات الإزاحة.
  3. حدّد المنطقة الزمنية في تلميحات النوع في SQL. عند استخدام المعلمات مع قيم DateTime من النوع Unspecified والموجّهة إلى أعمدة غير UTC، ضمّن المنطقة الزمنية في SQL:

عمليات الإدراج غير المتزامنة

تنقل عمليات الإدراج غير المتزامنة مسؤولية التجميع من العميل إلى الخادم. فبدلًا من اشتراط التجميع من جهة العميل، يخزّن الخادم البيانات الواردة مؤقتًا ثم يفرّغها إلى التخزين وفقًا لعتبات قابلة للتهيئة. ويكون هذا مفيدًا في السيناريوهات عالية التزامن، مثل أحمال عمل observability، حيث يرسل العديد من الوكلاء حمولات صغيرة. فعِّل عمليات الإدراج غير المتزامنة عبر CustomSettings أو سلسلة الاتصال:
وضعان (يحددهما wait_for_async_insert):
مع wait_for_async_insert=0، لا تظهر الأخطاء إلا أثناء التفريغ ولا يمكن ربطها بعملية الإدراج الأصلية. كما أن العميل لا يوفّر ضغطًا عكسيًا، مما يعرّض الخادم لخطر زيادة الحمل.
الإعدادات الأساسية:

الجلسات

لا تُفعِّل الجلسات إلا عند الحاجة إلى ميزات من جهة الخادم تحتفظ بالحالة، مثل:
  • الجداول المؤقتة (CREATE TEMPORARY TABLE)
  • الحفاظ على سياق الاستعلام عبر عدة تعليمات
  • إعدادات على مستوى الجلسة (SET max_threads = 4)
عند تفعيل الجلسات، تُعالَج الطلبات تسلسليًا لمنع الاستخدام المتزامن للجلسة نفسها. ويضيف ذلك حملًا إضافيًا إلى أحمال العمل التي لا تتطلب حالة الجلسة.
استخدام ADO.NET (للتوافق مع أطر ORM):

أنواع البيانات المدعومة

يدعم ClickHouse.Driver جميع أنواع بيانات ClickHouse. تُبيّن الجداول أدناه أوجه التوافق بين أنواع ClickHouse وأنواع ‎.NET الأصلية عند قراءة البيانات من قاعدة البيانات.

تعيين الأنواع: عند القراءة من ClickHouse

أنواع الأعداد الصحيحة


أنواع الأعداد ذات الفاصلة العائمة


الأنواع العشرية

يُتحكَّم في تحويل النوع Decimal من خلال الإعداد UseCustomDecimals.

نوع Boolean


أنواع السلاسل النصية

افتراضيًا، يُرجَع كلٌّ من العمودين String وFixedString(N) على هيئة string. عيّن ReadStringsAsByteArrays=true في سلسلة الاتصال لقراءتهما على هيئة byte[] بدلًا من ذلك. يفيد هذا عند تخزين بيانات ثنائية قد لا تكون بتنسيق UTF-8 صالح.يمتد أثر هذا الإعداد إلى السلاسل النصية المتداخلة داخل الأنواع الأخرى أيضًا، فتُقرأ Array(String) على هيئة byte[][] وتُقرأ Map(String, String) على هيئة Dictionary<byte[], byte[]> — بما في ذلك المفاتيح. الاستثناء الوحيد هو عمود JSON، إذ تبقى أوراقه النصية نصًا دائمًا؛ انظر نوع JSON.

أنواع التاريخ والوقت

يخزّن ClickHouse قيم DateTime وDateTime64 داخليًا على هيئة طوابع زمنية Unix (بالثواني أو بوحدات أصغر من الثانية منذ حقبة Unix). وعلى الرغم من أن التخزين يكون دائمًا بتوقيت UTC، فقد تكون للأعمدة منطقة زمنية مرتبطة بها تؤثر في كيفية عرض القيم وتفسيرها. عند قراءة قيم DateTime، تُضبط الخاصية DateTime.Kind بناءً على المنطقة الزمنية للعمود: بالنسبة إلى الأعمدة التي لا تستخدم UTC، تمثل قيمة DateTime المُعادة الوقت المحلي في تلك المنطقة الزمنية. استخدم ClickHouseDataReader.GetDateTimeOffset() للحصول على DateTimeOffset مع الإزاحة الصحيحة لتلك المنطقة الزمنية:
بالنسبة إلى الأعمدة التي لا تتضمن منطقة زمنية صريحة (أي DateTime بدلًا من DateTime('Europe/Amsterdam'))، يعيد برنامج التشغيل قيمة DateTime مع Kind=Unspecified. وهذا يحافظ على الوقت المحلي كما تظهره الساعة تمامًا كما هو مخزّن، من دون افتراض أي منطقة زمنية. إذا كنت بحاجة إلى سلوكٍ مدركٍ للمنطقة الزمنية للأعمدة التي لا تتضمن مناطق زمنية صريحة، فإما أن:
  1. تستخدم مناطق زمنية صريحة في تعريفات الأعمدة: DateTime('UTC') أو DateTime('Europe/Amsterdam')
  2. تطبّق المنطقة الزمنية بنفسك بعد القراءة.

نوع JSON

يُحدَّد نوع الإرجاع لأعمدة JSON من خلال الإعداد JsonReadMode:
  • Binary (الافتراضي): يعيد System.Text.Json.Nodes.JsonObject. يوفّر وصولًا منظّمًا إلى بيانات JSON، لكن أنواع ClickHouse المتخصصة (مثل عناوين IP وUUIDs والقيم العشرية الكبيرة) تُحوَّل إلى تمثيلاتها النصية داخل بنية JSON.
  • String: يعيد JSON الخام كسلسلة string. ويحافظ على تمثيل JSON كما هو تمامًا من ClickHouse، وهو ما يكون مفيدًا عندما تحتاج إلى تمرير JSON كما هو دون تحليله، أو عندما تريد التعامل مع فك التسلسل بنفسك.
None هو وضع ثالث. يقرأ البيانات تمامًا كما يفعل Binary، لكنه لا يرسل أي server setting مع الـ query — استخدمه مع connection غير مسموح لها بتعيين أي منها. المسار المُعلَن في نوع العمود هو مسار محدد النوع؛ أما أي مسار آخر في المستند فهو مسار ديناميكي. ويختلف النوعان عندما تكون القيمة null. يظهر المسار محدد النوع دائمًا في JsonObject. وإذا أُعلن على أنه Nullable(T) أو Dynamic، فإنه يُعاد بقيمة JSON null سواء كانت القيمة المخزنة null أو لم يتضمّن المستند هذا المسار أصلًا — ولا يمكن التمييز بين الحالتين:
عند التصريح عن المسار بنوع غير قابل للقيم الفارغة (non-nullable)، يأخذ المسار الغائب القيمة الافتراضية لذلك النوع — فـ JSON(x String) يعطي {"x":""}، وJSON(x Int64) يعطي {"x":0}. أما المسار الديناميكي الذي تكون قيمته null فيُحذف من الكائن بالكامل، لذا تُرجع ContainsKey القيمة false بشأنه. وقراءة {"x":null} من عمود JSON عادي تعطي {}. أما المسارات المحددة النوع المتداخلة فتُنشئ العناصر الأصلية الخاصة بها، لذا يُنتج JSON(a.b Nullable(Int64)) القيمة {"a":{"b":null}} حتى مع مستند فارغ.
هذا ما يعرضه الـ server نفسه، لذا أصبح وضعا Binary وString متطابقين الآن. قبل الإصدار 1.4.0 كان المسار المحدد النوع الذي يحمل null يُحذف من JsonObject، ما جعل {"x":null} تُقرأ على أنها {} — وفي حالة مسار متداخل مثل JSON(a.b Nullable(Int64)) كانت شجرة a الفرعية بأكملها تختفي.
تُعاد الأوراق النصية داخل عمود JSON دائمًا كنص، أيًا كانت قيمة ReadStringsAsByteArrays، إذ لا يملك JsonValue صيغة مصفوفة بايتات، ومن ثمّ فإن byte[] سيظهر بترميز base64. وينطبق ذلك على String وFixedString، وعلى ما يُغلَّف منها بـ LowCardinality أو Nullable أو SimpleAggregateFunction، وعلى السلاسل النصية داخل Array وMap، بما في ذلك مفاتيح الخريطة.
أما مصفوفة البايتات التي يتعذّر على قارئ JSON معرفة نوعها فتظهر فعلًا بترميز base64: فالمسار من النوع Variant أو Dynamic يحمل قيمةً لا يُعرف نوعها إلا على مستوى كل صف، لذا فإن سلسلة نصية ضمن Variant(Array(UInt8), String) تُعاد مُرمَّزة بـ base64. وهذا لا يتغير في كلا الإعدادين.أما نوع مفتاح خريطة JSON الذي ليس String تمامًا — مثل Map(LowCardinality(String), String) — فيُطلق NotSupportedException.
يقبل ClickHouse عمودًا يعلن مسارًا كقيمة وكأصل لمسار آخر في آنٍ واحد، على سبيل المثال JSON(a Int64, a.b Int64). وبما أن كلا المسارين موجود في كل صف، فإن الخادم يعرض الصف بمفتاح مكرر: {"a":0,"a":{"b":7}}. ولا يمكن لـ JsonObject أن يحمل قيمتين لمفتاح واحد، لذا يطرح JsonReadMode.Binary استثناء SerializationException يذكر فيه المسارين. وينطبق الأمر نفسه عندما تكون القيمة من نوع Map، كما في JSON(a Map(String, Int64)) المقروء من صف يحتوي أيضًا على a.b ديناميكي. ولا ينطبق ذلك إلا عندما يحمل الطرفان قيمة في الصف نفسه. أما الطرف الذي لا يحمل شيئًا — سواء كان NULL، أو كائنًا فارغًا، أو شجرة فرعية جميع قيمها NULL — فيفسح المجال للطرف الذي يحمل البيانات، أيًّا كان المسار الذي يرسله الخادم أولًا. وعليه فإن التداخل المُعلن بأنواع Nullable يملأ طرفًا واحدًا في كل صف ويُقرأ دون خطأ: إذ يعطي JSON(a Nullable(Int64), a.b Nullable(Int64)) النتيجتين {"a":5} و{"a":{"b":7}} كما هو متوقع. اقرأ هذا النوع من الأعمدة باستخدام JsonReadMode.String للحصول على نص JSON كما أرسله الخادم دون تغيير، بما في ذلك المفتاح المكرر. اضبط AllowDuplicateJsonKeys لمواصلة قراءة العمود كـ JsonObject بدلًا من طرح استثناء. عندئذٍ يحتفظ الـ driver بآخر القيمتين ورودًا في الصف ويُسقط الأخرى، فتكون النتيجة منقوصة: إذ يُقرأ JSON(a Int64, a.b Int64) الذي يحمل {"a.b":7} على أنه {"a":0}. أما المسار الذي يحمل قيمة ويحمل أصله قيمة scalar أو مصفوفة فيظل يطرح استثناءً، لأنه لا يمكن وضع شجرة فرعية تحت أيٍّ منهما.

Map type

النوع Map(K, V) في ClickHouse هو فعليًا Array(Tuple(K, V))، ويمكنه أن يضم عدة مدخلات تحمل المفتاح نفسه، وهو ما لا يتيحه Dictionary. لذلك، في الوضع الافتراضي، لا يحتفظ المفتاح المتكرر إلا بقيمته الأخيرة وتُسقَط الأزواج السابقة. ويحدد الإعداد MapReadMode التمثيل المستخدم:
  • Dictionary (الافتراضي): يُرجع Dictionary<K, V>.
  • KeyValuePairs: يُرجع List<KeyValuePair<K, V>> بالترتيب الذي أرسل به الخادم الأزواج، فيُحتفظ بكل زوج، بما في ذلك المدخلات التي تتكرر فيها المفاتيح.
يحدد الوضع نوع إطار العمل المستخدم لعمود Map، لذا فهو ينطبق أيضًا على GetFieldValue<T>، وعلى أنواع الـ schema التي يُبلغ عنها الـ driver، وعلى تعيين خصائص POCO. كما ينطبق أينما ظهر الـ map في شجرة نوع العمود — بما في ذلك Array(Map(...)) وMap(K, Map(...)) وTuple(..., Map(...)) وDynamic. كلا التمثيلين مقبول في مسار الكتابة في أي من الوضعين — راجع كتابة maps.

أنواع أخرى

سيُحوَّل النوعان Dynamic وVariant إلى النوع المقابل للنوع الأساسي الفعلي في كل صف.

أنواع Geometry

نوع Geometry هو نوع Variant يمكن أن يحتوي على أيٍّ من أنواع Geometry. وسيُحوَّل إلى النوع المقابل.

تعيين الأنواع: الكتابة إلى ClickHouse

عند إدراج البيانات، يحوّل برنامج التشغيل أنواع .NET إلى أنواع ClickHouse المقابلة لها. وتبيّن الجداول أدناه أنواع .NET المقبولة لكل نوع عمود في ClickHouse.

أنواع الأعداد الصحيحة


أنواع الأعداد ذات الفاصلة العائمة


النوع المنطقي


أنواع String


أنواع التاريخ والوقت

القيم خارج النطاقفي مسار الكتابة الثنائي، تؤدي قيم Date وDate32 وDateTime وDateTime32 الواقعة خارج نطاقها المدعوم إلى إطلاق ArgumentOutOfRangeException عند Write، مع ذكر نوع العمود والنطاق المدعوم. في السابق، كان يمكن اقتطاع القيم خارج النطاق بصمت عبر عدد صحيح 32-بت ثم يعيد الخادم تفسيرها، مما ينتج عنه طوابع زمنية حقيقية لكنها غير صحيحة.
يراعي برنامج التشغيل DateTime.Kind عند كتابة القيم: تحافظ قيم DateTimeOffset دائمًا على اللحظة الزمنية الدقيقة. مثال: DateTime بتوقيت UTC (تُحفَظ اللحظة الزمنية كما هي)
مثال: DateTime غير محدد (التوقيت المحلي)
التوصية: للحصول على أبسط سلوك وأكثره قابلية للتنبؤ، استخدم DateTimeKind.Utc أو DateTimeOffset في جميع عمليات DateTime. يضمن ذلك أن تعمل شيفرتك بشكل متسق بغض النظر عن المنطقة الزمنية للخادم أو العميل أو العمود.

معلمات HTTP مقابل Bulk Copy

يوجد فرق مهم بين ربط معلمات HTTP وBulk Copy عند كتابة قيم DateTime من النوع Unspecified: Bulk Copy يعرف المنطقة الزمنية للعمود المستهدف ويفسّر قيم Unspecified بشكل صحيح وفقًا لتلك المنطقة الزمنية. HTTP Parameters لا تعرف تلقائيًا المنطقة الزمنية للعمود. يجب تحديدها في تلميح نوع SQL:

أنواع Decimal


نوع JSON

يتحكم إعداد JsonWriteMode في السلوك عند كتابة JSON:
  • String (الافتراضي): يقبل string وJsonObject وJsonNode أو أي كائن. تُسلسَل جميع المدخلات عبر System.Text.Json.JsonSerializer وتُرسل كسلاسل JSON لتحليلها على جهة الخادم. هذا هو الوضع الأكثر مرونة ويعمل من دون تسجيل النوع.
  • Binary: لا يقبل إلا أنواع POCO المسجّلة. تُحوَّل البيانات إلى تنسيق JSON الثنائي الخاص بـ ClickHouse على جهة العميل مع دعم كامل لتلميحات النوع. ويتطلب استدعاء connection.RegisterJsonSerializationType<T>() قبل الاستخدام. وتؤدي كتابة قيم string أو JsonNode في هذا الوضع إلى طرح ArgumentException.
عندما يحتوي عمود JSON على تلميحات للأنواع (مثل JSON(id UInt64, price Decimal128(2)))، يستخدم برنامج التشغيل هذه التلميحات لتسلسل القيم مع الحفاظ الكامل على سلامة الأنواع. وهذا يحافظ على الدقة في أنواع مثل UInt64 وDecimal وUUID وDateTime64، والتي قد تفقد دقتها لولا ذلك عند تسلسلها بصيغة JSON عامة. يمكن كتابة كائنات POCO في أعمدة JSON بطريقتين وفقًا لـ JsonWriteMode: String mode (الافتراضي): تُسلسَل كائنات POCO عبر System.Text.Json.JsonSerializer. لا يتطلب ذلك تسجيل الأنواع. هذا هو النهج الأبسط، كما أنه يعمل مع الكائنات المجهولة. Binary mode: تُسلسَل كائنات POCO باستخدام تنسيق JSON الثنائي الخاص ببرنامج التشغيل مع دعم كامل لتلميحات النوع. يجب تسجيل الأنواع باستخدام connection.RegisterJsonSerializationType<T>() قبل الاستخدام. يدعم هذا الوضع تعيينات مسارات مخصّصة عبر السمات:
  • [ClickHouseJsonPath("path")]: يربط خاصيةً بمسار JSON مخصّص. ويكون ذلك مفيدًا مع البُنى المتداخلة أو عندما يختلف اسم الخاصية عن مفتاح JSON المطلوب. يعمل فقط في Binary mode.
  • [ClickHouseJsonIgnore]: يستبعد خاصيةً من التسلسل. يعمل فقط في Binary mode.
مطابقة أسماء الخصائص مع تلميحات نوع العمود حسّاسة لحالة الأحرف. فالخاصية UserId لن تتطابق إلا مع تلميح مُعرَّف باسم UserId، وليس userid. وهذا يتوافق مع سلوك ClickHouse الذي يسمح بوجود مسارات مثل userName وUserName معًا كحقول منفصلة. القيود (في Binary mode فقط):
  • يجب تسجيل أنواع POCO على الاتصال باستخدام connection.RegisterJsonSerializationType<T>() قبل إجراء التسلسل. وستؤدي محاولة تسلسل نوع غير مسجّل إلى ظهور الاستثناء ClickHouseJsonSerializationException.
  • تتطلب خصائص القاموس والمصفوفة/القائمة تلميحات نوع في تعريف العمود لكي تُسلسَل بشكل صحيح. ومن دون هذه التلميحات، استخدم String mode بدلًا من ذلك.
  • لا تُكتَب القيم الخالية في خصائص POCO إلا إذا كان للمسار تلميح نوع Nullable(T) في تعريف العمود. ولا يسمح ClickHouse بأنواع Nullable داخل مسارات JSON الديناميكية، لذلك يتم تخطي الخصائص الخالية غير المزوّدة بتلميحات.
  • يتم تجاهل السمات ClickHouseJsonPath وClickHouseJsonIgnore في String mode (إذ لا تعمل إلا في Binary mode).

أنواع أخرى


أنواع Geometry


غير مدعوم عند الكتابة


التعامل مع النوع Nested

يمكن قراءة النوع Nested في ClickHouse (Nested(...)) وكتابته باستخدام دلالات المصفوفات.

التسجيل والتشخيص

يتكامل عميل ClickHouse لـ .NET مع تجريدات Microsoft.Extensions.Logging لتوفير تسجيل خفيف الوزن يُفعَّل عند الحاجة. وعند تمكينه، يُصدر برنامج التشغيل رسائل منظَّمة لأحداث دورة حياة الاتصال، وتنفيذ الأوامر، وعمليات النقل، وعمليات الإدراج المجمّع. ويظل التسجيل اختياريًا بالكامل—فالتطبيقات التي لا تُعدّ مُسجِّلًا تواصل العمل دون أي عبء إضافي.

البدء السريع

استخدام appsettings.json

يمكنك ضبط مستويات التسجيل باستخدام إعدادات .NET القياسية:

استخدام تهيئة داخل الذاكرة

يمكنك أيضًا ضبط مستوى تفصيل التسجيل لكل فئة في الشيفرة:

الفئات والبواعث

يستخدم برنامج التشغيل فئات مخصّصة بحيث يمكنك ضبط مستويات السجل بدقة لكل مكوّن:

مثال: تشخيص مشكلات الاتصال

سيُسجِّل هذا ما يلي:
  • اختيار HTTP client factory ‏(التجمّع default مقابل اتصال واحد)
  • تهيئة HTTP handler ‏(SocketsHttpHandler أو HttpClientHandler)
  • إعدادات connection pool ‏(MaxConnectionsPerServer وPooledConnectionLifetime وما إلى ذلك)
  • إعدادات timeout ‏(ConnectTimeout وExpect100ContinueTimeout وما إلى ذلك)
  • تهيئة SSL/TLS
  • أحداث فتح الاتصال وإغلاقه
  • تتبّع معرّف الجلسة

وضع Debug: تتبّع الشبكة والتشخيص

للمساعدة في تشخيص مشكلات الشبكة، تتضمن مكتبة برنامج التشغيل أداة مساعدة تُمكّن التتبّع منخفض المستوى للمكوّنات الداخلية للشبكات في .NET. ولتمكينه، يجب تمرير LoggerFactory مع تعيين المستوى إلى Trace، وضبط EnableDebugMode على true (أو تمكينه يدويًا عبر الصنف ClickHouse.Driver.Diagnostic.TraceHelper). ستُسجَّل الأحداث ضمن الفئة ClickHouse.Driver.NetTrace. تحذير: سيؤدي ذلك إلى إنشاء سجلات شديدة التفصيل، وسيؤثر في الأداء. لا يُنصح بتمكين وضع Debug في بيئة الإنتاج.

OpenTelemetry

يوفّر برنامج التشغيل دعمًا مدمجًا للتتبّع الموزّع في OpenTelemetry عبر واجهة برمجة تطبيقات .NET System.Diagnostics.Activity. وعند تمكينه، يُنشئ برنامج التشغيل spans لعمليات قاعدة البيانات يمكن تصديرها إلى أنظمة observability الخلفية مثل Jaeger أو ClickHouse نفسه (عبر OpenTelemetry Collector).

تمكين التتبّع

في تطبيقات ASP.NET Core، أضِف ActivitySource الخاص ببرنامج تشغيل ClickHouse إلى تهيئة OpenTelemetry:
لتطبيقات سطر الأوامر، أو للاختبار، أو للإعداد اليدوي:

سمات span

يتضمن كل span سمات قاعدة البيانات القياسية في OpenTelemetry، بالإضافة إلى إحصاءات query الخاصة بـ ClickHouse التي يمكن استخدامها في debugging.

خيارات التكوين

تحكَّم في سلوك التتبّع باستخدام ClickHouseDiagnosticsOptions:
قد يؤدي تفعيل IncludeSqlInActivityTags إلى كشف بيانات حساسة ضمن التتبعات الخاصة بك. استخدمه بحذر في بيئات الإنتاج.

إعدادات TLS

عند الاتصال بـ ClickHouse عبر HTTPS، يمكنك تهيئة سلوك TLS/SSL بعدة طرق.

التحقق المخصص من الشهادات

بالنسبة إلى بيئات الإنتاج التي تتطلب منطقًا مخصصًا للتحقق من الشهادات، وفّر HttpClient خاصًا بك مع معالج ServerCertificateCustomValidationCallback مُعدّ:
اعتبارات مهمة عند استخدام HttpClient مخصّص
  • فك الضغط التلقائي: اترك AutomaticDecompression معطّلًا. فالـ driver يفكّ ترميز الاستجابات المضغوطة بنفسه، لذا لا حاجة إليه — بل إن تمكينه ينقلب ضدّك من جهة الطلب: فعند الإرسال يضيف المعالج أيضًا كل خوارزمية في قناعه إلى ترويسة Accept-Encoding الصادرة، فيوسّع ما أعلن عنه الـ driver، ما يتيح لـ ClickHouse الردّ باستخدام codec لم تطلبه. راجع فك ضغط الاستجابة.
  • مهلة الخمول: اضبط PooledConnectionIdleTimeout على قيمة أقل من keep_alive_timeout الخاص بالخادم (10 ثوانٍ في ClickHouse Cloud) لتجنّب أخطاء الاتصال الناتجة عن الاتصالات شبه المفتوحة.

ضبط الأداء

يوضّح هذا القسم كيفية استخدام الـ client لتحقيق الأداء الأمثل، والخيارات المختلفة التي يمكنك ضبطها لرفع كفاءة الـ client بما يلائم حالة الاستخدام الخاصة بك.

لمحة سريعة

| إذا كنت | افعل هذا | |---|---|---| | تقرأ الصفوف إلى كائنات POCO | استخدم QueryAsync<T> بدلاً من MapTo<T> | | تنفّذ عمليات إدراج كبيرة | ارفع قيمة InsertOptions.BatchSize | | تشغّل تطبيق طرفية أو worker كثيف الإدراج | فعّل Server GC | | تقرأ نتائج كبيرة عبر الشبكة | أبقِ ضغط الاستجابة مفعّلاً (وهو الوضع الافتراضي) | | تُدرج عبر اتصال سريع | جرّب InsertOptions.Compressor = null | | تُدرج في الجدول نفسه مرات عديدة | استخدم UseSchemaCache أو ColumnTypes | | تقرأ نتائج ضخمة جداً | ارفع قيمة ReadBufferSize |

القراءة: اختر مسار التجسيد

هناك ثلاث طرق للحصول على صف من النتيجة، وتكلفتها ليست واحدة. فبعض المسارات تُغلِّف القيم في كائنات (boxing)، ما يزيد التخصيصات ويقلّل الأداء. لقراءة 1,000,000 صف من 105 أعمدة من مجموعة بيانات hits:
تحصل أطر ORM على المسار السريع عند استخدامها accessors ذات أنواع محددة. يسجّل linq2db الدوال GetInt64 وGetDouble وGetDateTime لكل عمود، فتتم القراءة دون تغليف (boxing). أما الشيفرة التي تقرأ عبر GetValue (بما في ذلك نتيجة dynamic من Dapper) فتغلّف كل قيمة. وإذا كان استعلام ORM كثير التنفيذ ويقرأ عبر GetValue، فاستخدم QueryAsync<T> لهذا الاستعلام تحديدًا.

الإدراج: batch size والتوازي

يُعدّ batch size أكبر عامل تحكّم منفرد في throughput الإدراج. القيمة الافتراضية لـ InsertOptions.BatchSize هي 100,000 row. استخدم batches كبيرة. في عملية إدراج بحجم 1,000,000 row، أدّت زيادة حجم كل batch من 10,000 إلى 100,000 row إلى النتائج التالية: إذا تعذّر عليك التحكّم في batch size (مثلًا عندما يرسل عدد كبير من producers الصغيرة rows بشكل مستقل) فاستخدم async inserts ودع الخادم يتولّى batching. الرفع المتوازي. القيمة الافتراضية لـ InsertOptions.MaxDegreeOfParallelism هي 1. زِدها لإرسال batches بالتوازي. ويظهر أثر ذلك بوضوح أكبر عند تفعيل الضغط، لأن كل batch يُضغط عندئذٍ على thread خاص به. ولا تعمل sessions مع الإدراج المتوازي: إما أن تُعطّل sessions، أو تُبقي MaxDegreeOfParallelism = 1. أزل schema probe. يرسل كل استدعاء لـ InsertBinaryAsync أولًا query بالشكل SELECT ... WHERE 1=0 لتحديد column types. راجع تخطّي schema probe query للتخلّص من هذه الرحلة الذهابية والإيابية باستخدام ColumnTypes أو UseSchemaCache.
ينطبق مسار الإدراج الخالي من التغليف (box-free) على format الافتراضي RowBinary. أما RowBinaryWithDefaults فيتعيّن عليه فحص كل value للعثور على وسم DBDefault، لذا يظل على المسار الأبطأ.

الضغط: الاتجاهان لا يتفقان

يقايض الضغط وحدة المعالجة المركزية بالبايتات. وتتوقف جدوى هذه المقايضة على اتجاه النقل، وعرض النطاق الترددي لاتصالك بخادم ClickHouse، وكيفية تفاعل بياناتك مع خوارزمية الضغط التي اخترتها، وما إذا كنت تدفع مقابل كل بايت يُنقل. عمليات القراءة: أبقِ الضغط مفعّلًا، إلا إذا كان الخادم يعمل على الجهاز نفسه. وهذا هو الإعداد الافتراضي. وبالمقارنة مع عدم استخدام الضغط، أعطى zstd عند المستوى 1 النتائج التالية: عمليات الإدراج: قِس قبل أن تضغط. فقد لا تكون الوفورات كافية لتبرير تفعيله. وضع في اعتبارك أيضًا أن فك الضغط يضيف حملًا إضافيًا على الخادم؛ وهو حمل متواضع مع Zstd وLZ4 لكنه قد يكون مرتفعًا مع خوارزميات أخرى (مثل Brotli). لإيقاف ضغط عمليات الإدراج:
لاختيار الـ codec ومستويات الضغط وكيفية تحديد نقطة التقاطع المناسبة لحالتك، راجع ضبط الضغط.

المخازن المؤقتة

يحدّد ReadBufferSize حجم المخزن المؤقت الذي يقرأ استجابات HTTP، وقيمته الافتراضية 64 كيبيبايت. يستعير المشغّل (driver) هذا المخزن المؤقت من تجمّع مشترك ويعيده إليه عند التخلّص من القارئ، لذا لا يجري تخصيص ذاكرة جديد مع كل استعلام. زِد هذه القيمة لتقليل عدد مرات إعادة ملء المخزن المؤقت مع النتائج الكبيرة. ويحتفظ المشغّل بمخزن مؤقت واحد لكل قارئ مفتوح في الوقت نفسه، لذا يرتفع استهلاك الذاكرة كلما زاد حجم المخزن المؤقت وزاد عدد القرّاء المتزامنين.
تخلّص دائمًا من القارئ (reader). فعند التخلص منه، يعيد القارئ الـ buffer الخاص به إلى الـ pool ويحرّر اتصال HTTP. أما ترك القارئ دون تخلص فلا يعيد الـ buffer إلى الـ pool وقد يُبقي اتصال HTTP غير متاح؛ كما أن عملية garbage collection الاعتيادية ليست بديلًا عن التخلص الصريح.

بيئة التشغيل وجامع المهملات (GC)

فعّل Server GC في التطبيقات كثيفة الإدراج. فمع الشيفرة نفسها، والعدد نفسه من البايتات المخصصة، كان Workstation GC أبطأ بنسبة تصل إلى 97% في عمليات الإدراج مقارنةً بـ Server GC.
تضبط مشاريع ASP.NET Core هذا الإعداد مسبقًا، أما تطبيقات وحدة التحكم وخدمات الـ worker ومعظم صور الحاويات فلا تضبطه. والسبب هو حجم ميزانية الجيل 0؛ إذ يستخدم Workstation GC ميزانية صغيرة، ولذلك لا تنتهي حياة الـ buffers قصيرة العمر التي ينشئها الـ insert في الجيل 0، بل تنتقل إلى الجيل 1، ما يزيد من الترقية ويؤدي إلى عمل أكبر بكثير على الجيل 2. ففي إحدى حالات الـ insert، بلغ عدد عمليات جمع الجيل 2 لكل 1,000 عملية 4,000 مع Server GC مقابل 73,000 مع Workstation GC.
إن Server GC إعداد يخص الـ throughput لا الـ latency. ففي القياسات نفسها، أمضى Server GC أقل من نصف إجمالي الوقت في حالة توقف مؤقت، لكن فترات التوقف المفردة لديه كانت أطول (المئين الخامس والتسعون 114.6 مللي ثانية مقابل 61.9 مللي ثانية). فإذا كانت خدمتك حساسة تجاه الـ tail latency، فقِس كلا الوضعين قبل الاختيار.

زمن الاستجابة: أعد استخدام الاتصالات

يستغرق إنشاء اتصال TCP جديد وتنفيذ مصافحة TLS وقتًا طويلًا نسبيًا. وإعادة استخدام الاتصالات تقلّل بشكل ملحوظ من زمن استجابة استعلاماتك.
  • لا تُنشئ عميلًا لكل طلب، فكل عميل جديد له HttpClient خاص به يُنشئ تجمّع اتصالات جديدًا، ويتكبّد كلفة المصافحة من جديد. استخدم ClickHouseClient واحدًا طوال عمر التطبيق؛ فهو آمن للاستخدام من خيوط متعددة ومصمّم للاستخدام كنسخة مفردة (singleton).
  • في ADO.NET وأطر ORM، استخدم ClickHouseDataSource بحيث تتشارك جميع الاتصالات تجمّعًا واحدًا.
للاطلاع على المجموعة الكاملة من الأنماط، راجع عمر الاتصال وتجميع الاتصالات.

قِس بنفسك

في كثير من الحالات، يعتمد الأداء على بنية بياناتك، وسرعة اتصالك بالخادم، وما إذا كنت تريد المفاضلة بين استهلاك CPU في العميل واستهلاكه في الخادم (أو العكس)، وقيود عتادك، وما إلى ذلك. لذلك يُنصح بأن تقيس الأداء بنفسك استنادًا إلى بياناتك وبيئتك. لمعرفة الجزء الذي ينجزه الخادم من العمل، عيّن QueryOptions.QueryId ثم اقرأ العدادات:

دعم ORM

تتطلب أطر ORM واجهة ADO.NET (ClickHouseConnection). ولإدارة دورة حياة الاتصالات على النحو الصحيح، أنشئ الاتصالات باستخدام ClickHouseDataSource:

Dapper

يعمل ClickHouse.Driver مع Dapper. ويحوّل برنامج التشغيل تلقائيًا صياغة @parameter الخاصة بـ Dapper إلى الصياغة الأصلية في ClickHouse {parameter:Type}، مع استنتاج الأنواع من قيم .NET. استخدم ClickHouseDataSource لإدارة مدة بقاء الاتصال على نحو صحيح:

أنماط تمرير المَعلمات

جميع أنماط مَعلمات Dapper القياسية مدعومة: الكائنات المجهولة:
فئات POCO:
القاموس:
DynamicParameters (من قاموس أو كائن مجهول الاسم):

الاستعلام عن POCOs

يربط Dapper الأعمدة بالخصائص حسب الاسم (من دون حساسية لحالة الأحرف):

صياغة المعلمات الأصلية في ClickHouse

عندما تحتاج إلى تحكم صريح في النوع، استخدم مباشرةً في SQL صياغة {param:Type} الخاصة بـ ClickHouse مع Dictionary<string, object> لقيم المعلمات. لا تجمع بين صياغة @param وصياغة {param:Type} للمعلمة نفسها.

WHERE IN

تعمل ميزة توسيع IN المدمجة في Dapper:
يعيد Dapper صياغة ذلك إلى WHERE id IN (@Ids1, @Ids2, @Ids3)، ويحوّل برنامج التشغيل كل معلمة موسَّعة. كما تعمل الدالة has() في ClickHouse أيضًا مع معلمة من النوع Array:

معالِجات الأنواع المخصّصة

تتطلّب بعض أنواع ClickHouse، مثل ITuple وBigInteger وClickHouseDecimal، تسجيل معالِجات عند بدء التشغيل:
راجع مثال Dapper للاطلاع على مثال لتنفيذ معالج أنواع.

Dapper.Contrib

يعمل كلٌّ من GetAll<T>() وGet<T>(id). أما Insert<T>() فلا يعمل، إذ يولّد صياغة SQL Server (SCOPE_IDENTITY, []). ويُوصى باستخدام الطريقة الأصلية InsertBinaryAsync في ClickHouseClient بدلًا من ذلك.
يجب أن تتطابق أسماء الخصائص تمامًا مع أسماء أعمدة ClickHouse (تراعي حالة الأحرف).

القيود

Linq2db

يتوافق برنامج التشغيل هذا مع linq2db، وهو ORM خفيف الوزن وموفّر LINQ لـ .NET. راجع موقع المشروع للاطلاع على وثائق تفصيلية. مثال على الاستخدام: أنشئ DataConnection باستخدام موفّر ClickHouse:
يمكن تحديد تعيينات الجداول باستخدام السمات أو التهيئة بأسلوب Fluent. إذا كانت أسماء الفئة والخاصية لديك تطابق تمامًا أسماء الجدول والعمود، فلا حاجة إلى أي تهيئة:
الاستعلام عن:
النسخ المجمّع: استخدم BulkCopyAsync لإجراء عمليات إدراج مجمّعة بكفاءة.

Entity Framework Core

موفّر Entity Framework Core الرسمي لـ ClickHouse. اربط فئات C# بجداول ClickHouse، ونفّذ الاستعلامات باستخدام LINQ، وأدرِج البيانات عبر SaveChanges — وكل ذلك باستخدام أنماط EF Core المألوفة.
هذا الموفّر قيد التطوير النشط. يدعم الإصدار الحالي استعلامات LINQ (بما في ذلك عمليات JOIN، والاستعلامات الفرعية، وعمليات المجموعات)، وINSERT عبر SaveChanges / BulkInsertAsync، وعمليات الترحيل مع دعم DDL الكامل (CREATE / ALTER / DROP)، وتهيئة محرك الجدول الخاصة بـ ClickHouse. لا يدعم UPDATE / DELETE.

التثبيت

يتطلب .NET 10.0 وEF Core 10.

البدء السريع

عرّف الكيان وDbContext، ثم أجرِ استعلامًا باستخدام LINQ:

الأنواع المدعومة

استخدم ClickHouseDecimal (من ClickHouse.Driver.Numerics) بدلًا من decimal عندما تحتاج إلى الدقة الكاملة لأعمدة Decimal128/Decimal256، لأن decimal في .NET يقتصر على 28–29 رقمًا معنويًا.

عمليات LINQ المدعومة

الاستعلامات: Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking GROUP BY والتجميع: GroupBy مع Count, LongCount, Sum, Average, Min, Max — بما في ذلك HAVING (استخدام .Where() بعد .GroupBy())، وإجراء عدة عمليات تجميع ضمن إسقاط واحد، واستخدام OrderBy على نتائج التجميع. عمليات JOIN: Join (INNER)، وأنماط GroupJoin/SelectMany‏ (LEFT وCROSS). تُرجِع LEFT JOIN قيمة null فعلية للصفوف غير المتطابقة (راجع دلالات null في LEFT JOIN أدناه). الاستعلامات الفرعية: Contains / IN المترابطة، وAny / EXISTS، وAll، والاستعلامات الفرعية scalar ضمن الإسقاطات. عمليات المجموعات: Concat (→ UNION ALL)، وUnion (→ UNION DISTINCT)، وIntersect، وExcept. المجموعات المحلية المضمنة: تُترجَم عمليات join وContains على المجموعات الموجودة في الذاكرة (int[], List<T>, إلخ) إلى سلسلة من عمليات UNION. طرق String: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat (والمعامل +). الدوال الرياضية: تُترجَم طرق Math وMathF القياسية إلى ما يقابلها في ClickHouse — بما يشمل الدوال الحسابية واللوغاريتمية والمثلثية ودوال الأدوات المساعدة. يحقن الموفّر set_join_use_nulls=1 تلقائيًا في كل مسار اتصال لمواءمة توقّعات Entity Framework بشأن سلوك JOIN. إذا كان خادم ClickHouse أو ملف التعريف لديك يمنع تغيير هذا الإعداد (على سبيل المثال، ملف تعريف بقيمة readonly=1)، فأوقِف هذا السلوك باستخدام:
عند تفعيل خيار إلغاء الاشتراك، يعيد LEFT JOIN القيم الافتراضية لأعمدة ClickHouse، ولا يعود اكتشاف التنقل في EF المعتمد على null يعمل كما هو متوقع. استخدم مقارنات صريحة مع 0 / "" بدلًا من == null.

إدراج البيانات

يستخدم SaveChanges واجهة برمجة تطبيقات InsertBinaryAsync الأصلية لبرنامج التشغيل — بترميز RowBinary مع جسم طلب مضغوط، وهو أكثر كفاءة بكثير من عبارات SQL ذات المعلمات:
تنتقل الكيانات من Added إلى Unchanged بعد الحفظ، تمامًا كما يحدث مع أي موفّر آخر لـ EF Core. يمكن ضبط حجم الدفعة (الافتراضي 1000):

الإدراج المجمّع

لعمليات التحميل عالية الإنتاجية، استخدم BulkInsertAsync بدلًا من SaveChanges. هذه طريقة امتداد لـ DbContext تتجاوز بالكامل متتبّع التغييرات في EF Core، وآلية حلّ الهوية، وإدارة الحالة — إذ تستدعي مباشرةً InsertBinaryAsync الخاصة ببرنامج التشغيل باستخدام ترميز RowBinary وجسم طلب مضغوط. وهذا يجعلها مناسبة لتحميل مجموعات بيانات كبيرة عندما لا تحتاج إلى تتبّع الكيانات بعد الإدراج:
يمكن أن يكون الإدخال أي IEnumerable<T> — إذ يمر عبر الكيانات دون تحميلها جميعًا إلى الذاكرة. قيمة الإرجاع هي عدد الصفوف المُدرجة. لا تُرفَق الكيانات بـ DbContext بعد الإدراج، لذلك لا يحدث انتقال في الحالة من Added إلى Unchanged.

التعدادات

يمكن تعيين أعمدة Enum8/Enum16 في ClickHouse كخصائص string أو كأنواع enum في C#. وعند استخدام تعدادات C#، يُجري الموفّر التحويل تلقائيًا بين التعداد وتمثيله النصي:

تحويلات الأنواع المخصّصة

يتيح لك نظام ValueConverter في EF Core تعيين الأنواع المخصّصة إلى أنواع يدعمها الموفّر بالفعل. ولا يرى الموفّر نوعك المخصّص مطلقًا، إذ يتولّى EF Core التحويل عند نقطة الفصل. التحويل على مستوى كل خاصية:
فئة مُحوِّل قابلة لإعادة الاستخدام:

تعليقات توضيحية لنوع العمود

بالنسبة إلى الأنواع القياسية مثل string وint وDateTime وغيرها، يستنتج الموفّر نوع ClickHouse تلقائيًا. أمّا الأنواع ذات المعلمات والأغلفة، فيلزم تحديد نوع ClickHouse صراحةً. استخدام التعليقات التوضيحية للبيانات (السمات):
استخدام أسلوب fluent API في OnModelCreating:
الأغلفة المتداخلة مثل Array(Nullable(Int32)) وLowCardinality(Nullable(String)) مدعومة — إذ يفكّ الموفّر تغليف Nullable وLowCardinality تلقائيًا عند كل مستوى من مستويات التداخل.

أعمدة Variant وDynamic

تُقابِل أعمدة ClickHouse Variant(T1, T2, ...) وDynamic النوع object في .NET. وبما أن object عام جدًا بحيث لا يتيح استنتاج النوع تلقائيًا، يجب التصريح بنوع التخزين صراحةً باستخدام .HasColumnType():
عند القراءة، يُفك تسلسل القيمة تلقائيًا إلى نوع .NET الموافق للمميِّز المخزَّن (مثل string وulong وulong[]).

أعمدة JSON

يدعم المزوّد نوع العمود Json في ClickHouse، ويعيّنه إلى System.Text.Json.Nodes.JsonNode (بشكل أساسي) أو string (عبر ValueConverter تلقائي):
تتم قراءة JSON وكتابته من خلال كلٍّ من SaveChanges وBulkInsertAsync:
إذا كنت تفضّل سلاسل JSON الخام، فاضبط الخاصية لتكون string مع نوع عمود Json — يطبّق الموفّر ValueConverter تلقائيًا:
  • عدم ترجمة JSON path — لا يُترجَم entity.Data["name"] في LINQ إلى صياغة SQL data.name الخاصة بـ ClickHouse. طبّق عامل التصفية على الأعمدة غير JSON وافحص JSON في الذاكرة.
  • دلالات NULL — يعيد JSON type في ClickHouse القيمة {} (كائنًا فارغًا) لقيم NULL بدلًا من SQL NULL.
  • دقة الأعداد الصحيحة — يخزّن JSON في ClickHouse جميع الأعداد الصحيحة بصيغة Int64. عند القراءة عبر JsonNode، استخدم GetValue<long>() بدلًا من GetValue<int>().

محركات الجداول

اضبط محركات جداول ClickHouse والعبارات الخاصة بكل محرك عبر واجهة ToTable(name, t => ...) بأسلوب الاستدعاءات المتسلسلة. عند عدم ضبط أي محرك، يستخدم الموفّر MergeTree افتراضيًا، مع استمداد ORDER BY من المفتاح الأساسي للكيان.
عائلات المحركات المدعومة: بنود المحرّك: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. ترتبط جميعها بمنشئ المحرّك المُعاد من HasXxxEngine(). ميزات على مستوى العمود: HasCodec, HasTtl, HasComment, HasDefault — جميعها تدخل في عمليات الترحيل. فهارس تخطي البيانات — عبر HasIndex(...).HasSkippingIndexType(...):
يُتجاهَل الفهرس القياسي (غير القائم على تخطي البيانات) بصمت، إذ لا يوجد في ClickHouse ما يقابله. أما الفهارس الفريدة فتؤدي إلى ظهور استثناء، لأن ClickHouse لا يفرض التفرد.

عمليات الترحيل

سير العمل القياسي لعمليات الترحيل في EF Core:
العمليات المدعومة:

قيود الترحيل

وبالإضافة إلى عمليات الترحيل، لا يزال الموفّر لا يدعم أيضًا ما يلي:
  • UPDATE / DELETE
  • المعاملات: BeginTransaction عملية no-op. لا يوجد دعم لمعاملات ACID في ClickHouse.
  • ترجمة استعلامات JSON path: ‏entity.Data["key"] في LINQ لا يُترجم إلى صياغة SQL الخاصة بـ ClickHouse، أي data.key. استخدم التصفية على أعمدة غير JSON وافحص JSON في الذاكرة.

القيود

Tuples التي تضم 8 عناصر أو أكثر وبها Tuple متداخلة في الموضع الأخير

تستخدم أنواع C# ValueTuple التي تضم أكثر من 7 عناصر نمط تداخل يُنشئه المصرّف: إذ تكون الوسيطة العامة الثامنة (TRest) هي نفسها ValueTuple تحتوي على العناصر المتبقية. على سبيل المثال، يُصرَّف (int, int, int, int, int, int, int, string, string) إلى ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. يؤدي ذلك إلى التباس عندما يكون عمود ClickHouse عبارة عن Tuple من 8 عناصر ويكون العنصر الأخير فيه هو نفسه Tuple — مثل Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). لا يستطيع برنامج التشغيل التمييز بين:
  • Tuple مسطحة من 9 عناصر (تداخل TRest الذي يُنشئه المصرّف)
  • Tuple من 8 عناصر يكون عنصرها الأخير Tuple(String, String) متداخلة
كلتاهما تنتجان نوع .NET نفسه: ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. يتعامل برنامج التشغيل مع الوسيطة الثامنة على أنها TRest (أي يُسطّحها)، ما يعني أن حالة Tuple ذات 8 عناصر مع Tuple متداخلة ستُسلسَل بصورة غير صحيحة. ينطبق ذلك على كلٍّ من System.Tuple وValueTuple لأن كليهما يستخدم تداخل TRest عند وجود أكثر من 7 عناصر. أما Tuples التي تضم 7 عناصر أو أقل، أو التي لا يكون عنصرها الأخير نفسه Tuple، فلا تتأثر. الحل البديل: لفّ Tuple الداخلية بطبقة إضافية حتى يتمكن برنامج التشغيل من تمييزها عن تداخل TRest:

أعمدة AggregateFunction

لا يمكن الاستعلام عن الأعمدة من النوع AggregateFunction(...) أو الإدراج فيها مباشرةً. للإدراج:
للاختيار:

آخر تعديل في ٢٦ سبتمبر ٢٠٢٦