-
ClickHouseClient(موصى به): عميل عالي المستوى وآمن للاستخدام من عدة خيوط، ومصمَّم للاستخدام بنمط singleton. يوفّر واجهة برمجة تطبيقات غير متزامنة وبسيطة للاستعلامات وعمليات الإدراج المجمّع. وهو الأنسب لمعظم التطبيقات. -
ADO.NET (
ClickHouseDataSource,ClickHouseConnection,ClickHouseCommand): تجريدات قياسية لقواعد البيانات في .NET. وهي مطلوبة لتكامل ORM (Dapper وLinq2db) وعندما تحتاج إلى التوافق مع ADO.NET. تُعدClickHouseBulkCopyفئة مساعدة لإدراج البيانات بكفاءة باستخدام اتصال ADO.NET. الفئةClickHouseBulkCopyمُهمَلة وستُزال في إصدار مستقبلي؛ استخدمClickHouseClient.InsertBinaryAsyncبدلاً منها.
دليل الترحيل
- حدّث ملف
.csprojلاستخدام اسم الحزمة الجديدClickHouse.Driverوأحدث إصدار على NuGet. - حدّث جميع مراجع
ClickHouse.ClientإلىClickHouse.Driverفي شيفرة مشروعك.
إصدارات .NET المدعومة
يدعمClickHouse.Driver إصدارات .NET التالية:
- .NET 6.0
- .NET 8.0
- .NET 9.0
- .NET 10.0
إصدارات ClickHouse المدعومة
يدعم العميل رسميًا الإصدارات الثلاثة الأخيرة، بالإضافة إلى آخر إصدارين طويلَي الدعم (LTS).التثبيت
ثبّت الحزمة من 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. عندها لن يُرسل أي استعلام للمخطط على الإطلاق:
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، بحيث يقتصر نص الطلب على الصفوف وحدها:
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مدعومة.
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":
backtick وإفلات الأحرف الخاصة وفق آليته الخاصة. ويمكن نقل المعرّفات التي تحتوي على أحرف خاصة (بما في ذلك علامات backtick) ذهابًا وإيابًا بأمان.
معرّف الاستعلام
يُخصَّص لكل استعلام معرّف فريدquery_id يمكن استخدامه لجلب البيانات من جدول system.query_log أو لإلغاء الاستعلامات طويلة التشغيل. يمكنك تحديد معرّف استعلام مخصّص عبر QueryOptions:
تعيين نوع المعلمة المخصّص
عند استخدام المعلمات بأسلوب@ (مثل WHERE id = @id)، يستنتج برنامج التشغيل تلقائيًا نوع ClickHouse من نوع القيمة في .NET. على سبيل المثال، يُعيَّن int إلى Int32.
لتجاوز هذه الإعدادات الافتراضية، عيّن ParameterTypeResolver في ClickHouseClientSettings. ويكون ذلك مفيدًا عندما تريد استخدام DateTime64(3) لجميع معلمات DateTime بدقة الملّي ثانية، أو استخدام قيمة scale محددة لجميع قيم Decimal، من دون تعيين ClickHouseType لكل معلمة على حدة.
استخدام DictionaryParameterTypeResolver لتعيينات الأنواع البسيطة:
IParameterTypeResolver مخصّص للحالات المتقدمة:
للاستدلال المعتمد على القيمة أو المستند إلى الاسم، نفِّذ الواجهة IParameterTypeResolver مباشرةً. أرجِع null للانتقال إلى الاستدلال الافتراضي:
QueryOptions.ParameterTypeResolver. وعند تعيينه، تكون له أولوية أعلى من المحلِّل على مستوى العميل.
ترتيب أولوية تحديد النوع:
المحلِّل ليس سوى خطوة ضمن سلسلة ترتيب الأولوية. من الأعلى إلى الأدنى أولوية:
- تعيين
ClickHouseTypeصراحةً على المعلَمة - تلميح نوع SQL من الصيغة
{name:Type}في الاستعلام IParameterTypeResolver(منQueryOptions.ParameterTypeResolver، مع الرجوع إلىClickHouseClientSettings.ParameterTypeResolver)- استنتاج النوع المضمَّن (
TypeConverter.ToClickHouseType)
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) المضمّنة داخل قيمة حرفية مركّبة، يحيط برنامج التشغيل مخرجات المنسّق بعلامات اقتباس مفردة، لكنه لا يجري إفلاتًا لمحتواها. إذا كانت السلسلة التي يعيدها المنسّق تحتوي على علامة اقتباس مفردة أو شرطة مائلة عكسية غير مُفلَتة، فستصبح القيمة الحرفية المركّبة غير صحيحة، وسيرفض الخادم الاستعلام.
تُستخدم معلمات السلاسل النصية ذات المستوى الأعلى (غير المضمّنة داخل قيمة مركّبة) كما هي، من دون إحاطة، لذا لا يلزم الإفلات هنا.
أولوية المنسّق:
IParameterFormatter(منQueryOptions.ParameterFormatter، مع الرجوع إلىClickHouseClientSettings.ParameterFormatter). إذا أعاد قيمة غيرnull، فستُستخدم هذه القيمة.- التنسيق المضمّن الخاص بالنوع في
HttpParameterFormatter.
null أو DBNull؛ إذ تُسلسَل هذه القيم دائمًا باعتبارها مؤشر ClickHouse للقيم null (\N).
تحويل مخصص لقيم القراءة
يتيح لكIReadValueConverter تحويل القيم التي يعيدها قارئ البيانات بعد إلغاء التسلسل، من دون تغيير نوع CLR الخاص بها. ومن الاستخدامات الشائعة: تعيين DateTime.Kind = Utc لعمود DateTime لا يحتوي على timezone، أو إزالة المسافات الزائدة من السلاسل النصية أو تطبيعها، أو إجراء معالجة لاحقة على عمود JSON قبل أن يصل إلى شيفرة التطبيق.
عيّن ReadValueConverter في ClickHouseClientSettings لتطبيق محوّل على جميع عمليات القراءة. ويُستدعى المحوّل مرة واحدة لكل عمود في كل صف عبر كلٍّ من المسار المغلّف (GetValue) والمسار العام (GetFieldValue<T>). وعند عدم تعيين أي محوّل، لا توجد أي كلفة إضافية — إذ يعيد القارئ القيم مباشرةً.
استخدام DictionaryReadValueConverter لإجراء تحويل بسيط لكل نوع CLR:
For<T> كما هي من دون تغيير. ويجري التوجيه وفق نوع CLR المطابق تمامًا، لذا سجّل النوع الفعلي الذي يُنتجه القارئ (على سبيل المثال، For<JsonObject> لعمود JSON في JsonReadMode.Binary).
IReadValueConverter مخصّص للحالات المتقدمة:
إذا كنت بحاجة إلى التوجيه استنادًا إلى سلسلة النوع من جانب ClickHouse (على سبيل المثال، للتمييز بين DateTime وDateTime('UTC') — إذ يظهر كلاهما على أنهما نوع CLR نفسه)، فنفّذ IReadValueConverter مباشرةً:
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.
أجسام رسائل الخطأ
عندما يستجيب الخادم بحالة 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 بالكامل:
ClickHouseClientSettings:
enable_http_compression=1 في الـ URL، وهو ما يشترطه ClickHouse قبل أن يأخذ الـ header بعين الاعتبار أصلًا — بما في ذلك عندما تكون UseCompression بقيمة false، إذ إنّ تسمية codec صراحةً تُعدّ طلبًا له. وفي حال عدم ضبط أي قيمة، فإنّ UseCompression=false لا ترسل أي Accept-Encoding إطلاقًا.
يمكن ضبط Accept-Encoding في أربعة مواضع، ويُعتمد أوّل موضع منها يسمّي codec:
QueryOptions.AcceptEncoding(أوClickHouseCommand.AcceptEncoding)CustomHeaders["Accept-Encoding"]على مستوى الاستعلامCustomHeaders["Accept-Encoding"]على مستوى العميلClickHouseClientSettings.AcceptEncoding، أو الكلمة المفتاحيةAcceptEncodingفي سلسلة الاتصال
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 المستدعي، لذا عندما لا تكون الشبكة هي عنق الزجاجة، قد تصبح سرعة فكّ الترميز في جهة العميل هي العامل المحدِّد.
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 فتُعيد دائمًا البايتات تمامًا كما وصلت.
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 دون ضغط.
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:
راجع توثيق إعدادات الـ formats للاطلاع على خيارات التحكم في سلوك استيعاب البيانات.
المزيد من الأمثلة
للاطلاع على المزيد من أمثلة الاستخدام العملية، راجع دليل الأمثلة في مستودع GitHub.ADO.NET
توفّر المكتبة دعمًا كاملًا لـ ADO.NET من خلالClickHouseConnection وClickHouseCommand وClickHouseDataReader. وتُعد واجهة برمجة التطبيقات هذه ضرورية للتكامل مع ORM (Dapper وLinq2db)، وكذلك عند الحاجة إلى طبقات تجريد قواعد البيانات القياسية في .NET.
إدارة دورة الحياة باستخدام ClickHouseDataSource
أنشئ الاتصالات دائمًا عبرClickHouseDataSource لضمان الإدارة السليمة لدورة الحياة وتجميع الاتصالات. يدير DataSource مثيل ClickHouseClient واحدًا داخليًا، وتشترك جميع الاتصالات في تجمّع الاتصالات HTTP الخاص به.
استخدام ClickHouseCommand
أنشئ أوامر باستخدام اتصال لتنفيذ SQL:ExecuteNonQueryAsync()- لعبارات INSERT وUPDATE وDELETE وDDLExecuteScalarAsync()- يعيد أول عمود من أول صف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.
التعامل مع DateTime
-
استخدم UTC كلما أمكن. خزّن الطوابع الزمنية في أعمدة
DateTime('UTC')واستخدمDateTimeKind.Utcفي الشيفرة الخاصة بك. هذا يزيل أي التباس متعلق بالمنطقة الزمنية. -
استخدم
DateTimeOffsetللتعامل الصريح مع المنطقة الزمنية. فهو يمثّل دائمًا لحظة زمنية محددة ويتضمن معلومات الإزاحة. -
حدّد المنطقة الزمنية في تلميحات النوع في SQL. عند استخدام المعلمات مع قيم DateTime من النوع
Unspecifiedوالموجّهة إلى أعمدة غير UTC، ضمّن المنطقة الزمنية في SQL:
عمليات الإدراج غير المتزامنة
تنقل عمليات الإدراج غير المتزامنة مسؤولية التجميع من العميل إلى الخادم. فبدلًا من اشتراط التجميع من جهة العميل، يخزّن الخادم البيانات الواردة مؤقتًا ثم يفرّغها إلى التخزين وفقًا لعتبات قابلة للتهيئة. ويكون هذا مفيدًا في السيناريوهات عالية التزامن، مثل أحمال عمل observability، حيث يرسل العديد من الوكلاء حمولات صغيرة. فعِّل عمليات الإدراج غير المتزامنة عبرCustomSettings أو سلسلة الاتصال:
wait_for_async_insert):
الإعدادات الأساسية:
الجلسات
لا تُفعِّل الجلسات إلا عند الحاجة إلى ميزات من جهة الخادم تحتفظ بالحالة، مثل:- الجداول المؤقتة (
CREATE TEMPORARY TABLE) - الحفاظ على سياق الاستعلام عبر عدة تعليمات
- إعدادات على مستوى الجلسة (
SET max_threads = 4)
أنواع البيانات المدعومة
يدعم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. وهذا يحافظ على الوقت المحلي كما تظهره الساعة تمامًا كما هو مخزّن، من دون افتراض أي منطقة زمنية.
إذا كنت بحاجة إلى سلوكٍ مدركٍ للمنطقة الزمنية للأعمدة التي لا تتضمن مناطق زمنية صريحة، فإما أن:
- تستخدم مناطق زمنية صريحة في تعريفات الأعمدة:
DateTime('UTC')أوDateTime('Europe/Amsterdam') - تطبّق المنطقة الزمنية بنفسك بعد القراءة.
نوع 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 أو لم يتضمّن المستند هذا المسار أصلًا — ولا يمكن التمييز بين
الحالتين:
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.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 (تُحفَظ اللحظة الزمنية كما هي)
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(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 عبر واجهة برمجة تطبيقات .NETSystem.Diagnostics.Activity. وعند تمكينه، يُنشئ برنامج التشغيل spans لعمليات قاعدة البيانات يمكن تصديرها إلى أنظمة observability الخلفية مثل Jaeger أو ClickHouse نفسه (عبر OpenTelemetry Collector).
تمكين التتبّع
في تطبيقات ASP.NET Core، أضِفActivitySource الخاص ببرنامج تشغيل ClickHouse إلى تهيئة OpenTelemetry:
سمات span
يتضمن كل span سمات قاعدة البيانات القياسية في OpenTelemetry، بالإضافة إلى إحصاءات query الخاصة بـ ClickHouse التي يمكن استخدامها في debugging.خيارات التكوين
تحكَّم في سلوك التتبّع باستخدامClickHouseDiagnosticsOptions:
إعدادات 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).
لإيقاف ضغط عمليات الإدراج:
المخازن المؤقتة
يحدّدReadBufferSize حجم المخزن المؤقت الذي يقرأ استجابات HTTP، وقيمته الافتراضية 64 كيبيبايت.
يستعير المشغّل (driver) هذا المخزن المؤقت من تجمّع مشترك ويعيده إليه عند التخلّص من القارئ، لذا لا يجري تخصيص ذاكرة جديد مع كل استعلام. زِد هذه القيمة لتقليل عدد مرات إعادة ملء المخزن المؤقت مع النتائج الكبيرة. ويحتفظ المشغّل بمخزن مؤقت واحد لكل قارئ مفتوح في الوقت نفسه، لذا يرتفع استهلاك الذاكرة كلما زاد حجم المخزن المؤقت وزاد عدد القرّاء المتزامنين.
بيئة التشغيل وجامع المهملات (GC)
فعّل Server GC في التطبيقات كثيفة الإدراج. فمع الشيفرة نفسها، والعدد نفسه من البايتات المخصصة، كان Workstation GC أبطأ بنسبة تصل إلى 97% في عمليات الإدراج مقارنةً بـ Server 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 القياسية مدعومة: الكائنات المجهولة:DynamicParameters (من قاموس أو كائن مجهول الاسم):
الاستعلام عن POCOs
يربط Dapper الأعمدة بالخصائص حسب الاسم (من دون حساسية لحالة الأحرف):صياغة المعلمات الأصلية في ClickHouse
عندما تحتاج إلى تحكم صريح في النوع، استخدم مباشرةً في SQL صياغة{param:Type} الخاصة بـ ClickHouse مع Dictionary<string, object> لقيم المعلمات. لا تجمع بين صياغة @param وصياغة {param:Type} للمعلمة نفسها.
WHERE IN
تعمل ميزة توسيع IN المدمجة في Dapper:WHERE id IN (@Ids1, @Ids2, @Ids3)، ويحوّل برنامج التشغيل كل معلمة موسَّعة.
كما تعمل الدالة has() في ClickHouse أيضًا مع معلمة من النوع Array:
معالِجات الأنواع المخصّصة
تتطلّب بعض أنواع ClickHouse، مثلITuple وBigInteger وClickHouseDecimal، تسجيل معالِجات عند بدء التشغيل:
Dapper.Contrib
يعمل كلٌّ منGetAll<T>() وGet<T>(id). أما Insert<T>() فلا يعمل، إذ يولّد صياغة SQL Server (SCOPE_IDENTITY, []). ويُوصى باستخدام الطريقة الأصلية InsertBinaryAsync في ClickHouseClient بدلًا من ذلك.
القيود
Linq2db
يتوافق برنامج التشغيل هذا مع linq2db، وهو ORM خفيف الوزن وموفّر LINQ لـ .NET. راجع موقع المشروع للاطلاع على وثائق تفصيلية. مثال على الاستخدام: أنشئDataConnection باستخدام موفّر ClickHouse:
BulkCopyAsync لإجراء عمليات إدراج مجمّعة بكفاءة.
Entity Framework Core
موفّر Entity Framework Core الرسمي لـ ClickHouse. اربط فئات C# بجداول ClickHouse، ونفّذ الاستعلامات باستخدام LINQ، وأدرِج البيانات عبرSaveChanges — وكل ذلك باستخدام أنماط EF Core المألوفة.
- NuGet:
ClickHouse.EntityFrameworkCore - المصدر: GitHub
هذا الموفّر قيد التطوير النشط. يدعم الإصدار الحالي استعلامات LINQ (بما في ذلك عمليات JOIN، والاستعلامات الفرعية، وعمليات المجموعات)، و
INSERT عبر SaveChanges / BulkInsertAsync، وعمليات الترحيل مع دعم DDL الكامل (CREATE / ALTER / DROP)، وتهيئة محرك الجدول الخاصة بـ ClickHouse. لا يدعم UPDATE / DELETE.التثبيت
البدء السريع
عرّف الكيان و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)، فأوقِف هذا السلوك باستخدام:
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 صراحةً.
استخدام التعليقات التوضيحية للبيانات (السمات):
OnModelCreating:
Array(Nullable(Int32)) وLowCardinality(Nullable(String)) مدعومة — إذ يفكّ الموفّر تغليف Nullable وLowCardinality تلقائيًا عند كل مستوى من مستويات التداخل.
أعمدة Variant وDynamic
تُقابِل أعمدة ClickHouseVariant(T1, T2, ...) وDynamic النوع object في .NET. وبما أن object عام جدًا بحيث لا يتيح استنتاج النوع تلقائيًا، يجب التصريح بنوع التخزين صراحةً باستخدام .HasColumnType():
string وulong وulong[]).
أعمدة JSON
يدعم المزوّد نوع العمودJson في ClickHouse، ويعيّنه إلى System.Text.Json.Nodes.JsonNode (بشكل أساسي) أو string (عبر ValueConverter تلقائي):
SaveChanges وBulkInsertAsync:
string مع نوع عمود Json — يطبّق الموفّر ValueConverter تلقائيًا:
- عدم ترجمة JSON path — لا يُترجَم
entity.Data["name"]في LINQ إلى صياغة SQLdata.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(...):
عمليات الترحيل
سير العمل القياسي لعمليات الترحيل في 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)متداخلة
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(...) أو الإدراج فيها مباشرةً.
للإدراج: