-
ClickHouseClient(موصى به): عميل عالي المستوى وآمن للاستخدام عبر خيوط تنفيذ متعددة، ومصمّم للاستخدام بوصفه نسخة واحدة مشتركة. يوفّر واجهة برمجة تطبيقات بسيطة وغير متزامنة لتنفيذ الاستعلامات والإدراج المجمّع. وهو الأنسب لمعظم التطبيقات. -
ADO.NET (
ClickHouseDataSource,ClickHouseConnection,ClickHouseCommand): تجريدات قياسية لقواعد البيانات في .NET. وهي ضرورية للتكامل مع أدوات ربط الكائنات بقواعد البيانات (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: كائن تهيئة ذو أنواع محددة بدقة، يمكن تحميله من ملفات التهيئة أو تعيينه في الشيفرة.
إعدادات الاتصال
صيغة البيانات وتسلسلها
إدارة الـsession
تتيح العلامة
UseSession الاحتفاظ بـsession الخادم، مما يسمح باستخدام عبارات SET والجداول المؤقتة. تُعاد الـsession إلى حالتها الأولية بعد 60 ثانية من عدم النشاط (مهلة انتهاء الاتصال الافتراضية). ويمكن إطالة عمر الـsession بضبط إعداداتها عبر عبارات 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لذلك الجدول. - تقتصر ذاكرة التخزين المؤقت على مثيل
ClickHouseClient، وتُفهرس بحسب (database, table). وتشترك المجموعات الفرعية المختلفة من أعمدة الجدول نفسه في مخطط واحد مخزّن مؤقتًا.
ClickHouseClient
تُعدّClickHouseClient واجهة برمجة التطبيقات الموصى بها للتفاعل مع ClickHouse. وهي آمنة للاستخدام المتزامن عبر خيوط تنفيذ متعددة، ومصممة لاستخدامها كمثيل واحد مشترك، وتتولى تجميع اتصالات HTTP داخليًا.
إنشاء عميل
أنشئClickHouseClient باستخدام سلسلة اتصال أو كائن ClickHouseClientSettings. راجع قسم التهيئة للاطلاع على الخيارات المتاحة.
تتوفر بيانات الاتصال بخدمة ClickHouse Cloud الخاصة بك في وحدة تحكم ClickHouse Cloud.
حدّد خدمة، ثم انقر على Connect:
اختر C#. ستظهر بيانات الاتصال أدناه.
إذا كنت تستخدم ClickHouse مُدارًا ذاتيًا، فيحدّد مسؤول ClickHouse بيانات الاتصال.
لاستخدام سلسلة اتصال:
ClickHouseClientSettings:
IHttpClientFactory:
صُمّم
ClickHouseClient ليُستخدم طويلًا ويُشارك بين أجزاء تطبيقك. أنشئ مثيلًا واحدًا منه (عادةً بوصفه مثيلًا واحدًا مشتركًا)، وأعِد استخدامه في جميع عمليات قاعدة البيانات. يتولى العميل تجميع اتصالات HTTP داخليًا.تنفيذ الاستعلامات
استخدمExecuteNonQueryAsync لتنفيذ العبارات التي لا تُرجع نتائج:
ExecuteScalarAsync للحصول على قيمة واحدة:
إدراج البيانات
عمليات الإدراج باستخدام المعلمات
أدرج البيانات باستخدام استعلامات مُعلَّمة عبرExecuteNonQueryAsync. يجب تحديد أنواع المعلمات في SQL بالصيغة {name:Type}:
الإدراجات المجمّعة
استخدمInsertBinaryAsync لإدراج أعداد كبيرة من الصفوف بكفاءة. تُرسل هذه الطريقة البيانات في دفق بصيغة الصفوف الثنائية الأصلية في ClickHouse، وتدعم رفع الدفعات المتوازي، وتتجنب حالات error من نوع “URL too long” التي قد تحدث مع الاستعلامات ذات المعلمات.
InsertOptions:
- يجلب العميل بنية الجدول تلقائيًا عبر
SELECT * FROM <table> WHERE 1=0قبل إدراج البيانات. يجب أن تتوافق القيم المقدَّمة مع أنواع أعمدة الجدول الهدف. لتجاوز هذا الاستعلام، استخدمInsertOptions.ColumnTypesأوInsertOptions.UseSchemaCache. - إذا كانت قيمة
MaxDegreeOfParallelism > 1، تُحمَّل الدفعات بالتوازي. لا يتوافق استخدام الـsession مع الإدراج المتوازي؛ لذا عطّل الـsession أو اضبط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 لتوجيه الطلبات أو فحصها، أو عندما تريد ظهور العبارة في سجلات الوصول وأدوات إمكانية المراقبة. هذا الخيار غير مفعّل افتراضيًا، لأن طول العبارة يُحتسب عند تفعيله ضمن طول URL. والحد الفعلي هو أدنى حد تفرضه بيئة تشغيل .NET أو أحد المكونات الوسيطة أو الخادم. في الإصدارات من .NET 6 إلى .NET 9، يحدّ System.Uri طول عنوان URI الكامل والمُرمَّز للطلب بـ65,519 محرفًا؛ ويرفع المشغّل استثناء InvalidOperationException يوجّهك إلى استخدام InsertQueryPlacement.Body عند تجاوز هذا الحد. يبلغ الحد الافتراضي لإعداد http_max_uri_size في ClickHouse 1 MiB، لكن قد يفرض أحد المكونات الوسيطة حدًا أقل. في وضع المتن، لا تخضع العبارة والصفوف لهذا الحد على طول URL؛ ومع ذلك، يمكن أن تظهر خيارات أخرى للطلب في URL.
هذا الإعداد مستقل عن Compressor: يُرمَّز المتن بالطريقة نفسها في كلا الوضعين.
قراءة البيانات
استخدمExecuteReaderAsync لتنفيذ استعلامات SELECT. يتيح الكائن ClickHouseDataReader المُعاد الوصول إلى أعمدة النتائج بحسب أنواع بياناتها عبر طرق مثل GetInt64() وGetString() وGetFieldValue<T>().
استدعِ Read() للانتقال إلى الصف التالي. وتُرجع هذه الطريقة false عند عدم وجود صفوف أخرى. يمكنك الوصول إلى الأعمدة باستخدام الفهرس (بدءًا من 0) أو اسم العمود.
قراءة البيانات باستخدام POCO
بدلاً من قراءة الأعمدة حسب الفهرس أو الاسم، يمكنك تمرير نتائج الاستعلام مباشرةً إلى أصنافك. سجّل النوع لدى العميل مرة واحدة، ثم استخدمQueryAsync<T>:
RegisterPocoType<T>() تعيينات الإدراج والقراءة معًا، وتتحقق من صلاحيتها مسبقًا. أما RegisterBinaryInsertType<T>() فلم تتغير، ولا تزال مخصصة للإدراج فقط حفاظًا على التوافق مع الإصدارات السابقة.
يجب أن يتوفر في النوع المسجّل ما يلي:
- مُنشئ عام بلا معلمات.
- خاصية عامة واحدة على الأقل لها أداة تعيين عامة غير مقيّدة بـ
init. وتُدعم الخصائص المعرّفة بالكلمةrequired.
InvalidOperationException. لذا تقبل الخاصية من النوع object أي عمود.
تقرأ QueryAsync<T> كل عمود من الأعمدة التالية مباشرةً في خاصية من النوع المناسب:
يقبل كل صف أيضًا صيغة نوع الخاصية التي تسمح بالقيم الفارغة (
long? وDateOnly? وما إلى ذلك)،
سواء كان العمود من النوع Nullable(...) أم لا. ويُقبل تسجيل خاصية ذات نوع قيمة لا يسمح بالقيم الفارغة لعمود 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>[]
عبر مسار لا يتطلب تغليف القيم، مع الحفاظ على ترتيب العناصر في التمثيل المنقول وأي مفاتيح مكررة، في كلا وضعَي
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> إذا كنت لا تحتاج إلا إلى الصفوف؛ وراجع
اختيار مسار التجسيد للاطلاع على الأرقام.
ينطبق محوّل قيم القراءة، سواء ضُبط على مستوى العميل أو لكل استعلام، على كلا المسارين، ولا
يعطّل القراءة من دون تغليف. يحوّل المشغّل كل عمود باستخدام نسخة الدالة المناسبة لطريقة
قراءة العمود: النسخة محدّدة النوع ConvertValue<T> للعمود المقروء من دون تغليف،
والنسخة التي تتعامل مع القيم المغلّفة ConvertValue للعمود المركّب. نفّذ النسختين
على نحو متسق، وإلا أعطى العمود نفسه نتائج مختلفة بحسب مسار القراءة.
عند تهيئة LoggerFactory، تُصدر RegisterPocoType<T>() وRegisterBinaryInsertType<T>() رسالة سجلّ بمستوى Debug (الفئة ClickHouse.Driver.Client) توضّح الخصائص التي رُبطت بكل عمود، والخصائص التي جرى تجاهلها وأسباب ذلك. راجع تسجيل الأحداث والتشخيص.
معلمات SQL
في ClickHouse، تُكتب معلمات استعلامات SQL بالصيغة القياسية{parameter_name:DataType}.
أمثلة:
تُمرَّر معلمات الربط في SQL ضمن معلمات استعلام عنوان URI لطلب HTTP، لذا قد يؤدي الإكثار منها إلى حدوث خطأ “عنوان URL طويل جدًا”. استخدم
InsertBinaryAsync لإدراج كميات كبيرة من البيانات وتجنّب هذا القيد.العناصر النائبة @name بأسلوب ADO
يقبل المشغّل أيضًا العناصر النائبة @name التي تُنتجها أدوات تعيين الكائنات بالعلاقات (ORM) مثل Dapper. وهي وسيلة
لتسهيل العمل من جهة العميل: قبل إرسال الطلب، يُستبدل كل منها بالصيغة
{name:ResolvedType}، فلا يرى الخادم الرمز @ مطلقًا. راجع
تحديد النوع لمعرفة كيفية اختيار النوع. استخدم الصيغة الصريحة
{name:Type} متى أمكن.
يُترك @name دون تغيير إذا لم تكن له معلمة مطابقة، ليرفضه الخادم. وتُراعي المطابقة
حالة الأحرف، لذا لا يرتبط @ID بمعلمة اسمها id.
لإيقاف إعادة الكتابة، اضبط مفتاح AppContext المسمى
ClickHouse.Driver.DisableReplacingParameters
قبل استخدام المشغّل لأول مرة. تتوقف إعادة كتابة النص وحدها؛ إذ تظل المعلمات تُرسَل، لذا
تستمر الاستعلامات المكتوبة بصيغة ClickHouse الأصلية {name:Type} في العمل.معلمات المعرّفات
يتيح لك نوع المعلمةIdentifier تمرير اسم قاعدة بيانات أو جدول أو عمود بأمان، بدلًا من تمريره كقيمة نصية حرفية بين علامتَي اقتباس. استخدم الصيغة {name:Identifier} في SQL، أو عيّن ClickHouseDbParameter.ClickHouseType = "Identifier":
معرّف الاستعلام
يُسنَد إلى كل استعلام معرّف فريدquery_id، يمكن استخدامه لاسترجاع البيانات من جدول system.query_log أو لإلغاء الاستعلامات التي يستغرق تنفيذها وقتًا طويلًا. ويمكنك تحديد معرّف مخصّص للاستعلام باستخدام QueryOptions:
تخصيص ربط أنواع المعلمات
عند استخدام المعلمات بصيغة@ (مثل WHERE id = @id)، يستنتج المشغّل نوع ClickHouse تلقائيًا من نوع القيمة في .NET. فعلى سبيل المثال، يقابل النوع int النوع Int32.
لتجاوز هذا السلوك الافتراضي، اضبط ParameterTypeResolver في ClickHouseClientSettings. يفيد ذلك إذا أردت استخدام النوع DateTime64(3) لجميع معلمات DateTime للحصول على دقة بالمللي ثانية، أو تحديد عدد المنازل العشرية لجميع القيم العشرية، دون الحاجة إلى ضبط ClickHouseType لكل معلمة على حدة.
استخدام DictionaryParameterTypeResolver لربط الأنواع البسيطة:
IParameterTypeResolver للحالات المتقدمة:
لتحديد نوع المعلمة بناءً على قيمتها أو اسمها، نفّذ الواجهة IParameterTypeResolver مباشرةً. أعِد null لاستخدام الاستنتاج الافتراضي عند تعذّر تحديد النوع:
QueryOptions.ParameterTypeResolver. وإذا عيّنته، فستكون له الأسبقية على المحلّل المعيّن على مستوى العميل.
ترتيب أسبقية تحديد النوع:
يُعدّ المحلّل إحدى حلقات سلسلة الأسبقية. والترتيب من الأعلى إلى الأدنى هو:
ClickHouseTypeالمعيّن صراحةً للمعلمة- تلميح نوع SQL الوارد في الاستعلام بالصيغة
{name:Type} IParameterTypeResolver(منQueryOptions.ParameterTypeResolver، أو منClickHouseClientSettings.ParameterTypeResolverفي حال عدم تعيينه)- استنتاج النوع المضمّن (
TypeConverter.ToClickHouseType)
ClickHouseConnection في ADO.NET — إذ ترث الاتصالات التي ينشئها العميل إعداداته.
تنسيق قيمة المعلمات المخصص
IParameterFormatter خطاف يحدد كيفية تسلسل قيم المعلمات. استخدمه إذا لم يتوافق التنسيق المضمّن (مثل دقة DateTime، أو التنسيق الإقليمي للأعداد العشرية، أو إفلات السلاسل النصية، أو تمثيل الأرقام) مع ما يتوقعه المخطط أو الأدوات التي تستخدم هذه القيم لاحقًا.
عيّن 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 (\N).
تحويل قيمة القراءة المخصص
تتيح لكIReadValueConverter تحويل القيم التي يعيدها قارئ البيانات بعد فك التسلسل، دون تغيير نوع CLR الخاص بها. ومن الاستخدامات الشائعة: تعيين DateTime.Kind = Utc لعمود من نوع DateTime لا تتوفر له منطقة زمنية، أو إزالة الأحرف الزائدة من أطراف السلاسل النصية وتوحيد تنسيقها، أو معالجة عمود 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.
أيّ الصيغتين تُستدعى:
يجب أن تتفق الصيغتان في النتيجة، لأن الصيغة التي يستدعيها المشغّل تعتمد على طريقة قراءة المستدعي
للعمود:
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)، قد تحتاج إلى التفاوض على مرمِّز مختلف (مثل zstd أو lz4) للموازنة بين استهلاك CPU وعرض النطاق الترددي دون تغيير الإعداد على مستوى الاتصال. يضبط كلٌّ من QueryOptions.AcceptEncoding وClickHouseCommand.AcceptEncoding ترويسة HTTP Accept-Encoding لطلب واحد، لتحل محل أي قيمة افتراضية سابقة، ويفعّلان enable_http_compression=1 في URL (وهو ما يشترطه ClickHouse للاستجابة لترويسة Accept-Encoding).
تهيئة HttpClient
لا يلزم تهيئة أي شيء: يُبقي المشغّلAutomaticDecompression في كائن HttpClient الذي ينشئه على القيمة DecompressionMethods.None، ويتولى فك ترميز الاستجابات بنفسه. لذلك لا تُزال ترويسة Content-Encoding دون علمك، ويصلك المتن الخام تمامًا كما أرسله الخادم.
متون الأخطاء
إذا استجاب الخادم برمز 4xx/5xx مع ضبطenable_http_compression=1، فإنه يضغط متن الخطأ بالمرمِّز نفسه الذي كان سيستخدمه في استجابة ناجحة. ويفكّ المشغّل ضغط المتن إذا استُخدم أحد المرمِّزات التي يدعمها (lz4 وzstd وgzip وdeflate وbr/brotli)، لتظهر الرسالة في ClickHouseServerException بصورة مقروءة. أما إذا استُخدم مرمِّز آخر (snappy، …)، فيعيد عنصرًا نائبًا يذكر اسم المرمِّز ويشير إلى system.query_log للاطلاع على نص الخطأ الأصلي.
فك ضغط الاستجابة
لا يفعلAccept-Encoding سوى طلب ضغط الاستجابة من الخادم — ولا بد بعد ذلك من فك ضغطها. يتولى المشغّل هذه المهمة بنفسه بناءً على قيمة Content-Encoding في الاستجابة، لذا تعمل جميع واجهات برمجة تطبيقات القراءة المعتادة (ExecuteReaderAsync وExecuteScalarAsync وExecuteNonQueryAsync وQueryAsync<T> وDapper وEF Core وlinq2db) مع الاستجابات المضغوطة دون الحاجة إلى أي تهيئة. ويمكنه فك ضغط lz4 وzstd وgzip وdeflate وbr؛ أما snappy فغير مدعوم.
افتراضيًا، يعلن المشغّل عن zstd, lz4, gzip, deflate، ويستجيب ClickHouse باستخدام zstd. لاختيار صيغة أخرى، اضبط Accept-Encoding بنفسك — على مستوى العميل بالكامل:
ClickHouseClientSettings:
enable_http_compression=1 إلى عنوان URL، وهو شرط يفرضه ClickHouse قبل أن يعتدّ بالترويسة أصلًا — حتى عندما تكون قيمة UseCompression هي false، لأن تحديد مرمِّز صراحةً يُعدّ طلبًا لاستخدامه. وإذا لم تُحدَّد أي قيمة، فلن يرسل UseCompression=false الترويسة Accept-Encoding إطلاقًا.
يمكن تعيين Accept-Encoding في أربعة مواضع. ويُعتمد أول موضع يحدّد مرمِّزًا:
QueryOptions.AcceptEncoding(أوClickHouseCommand.AcceptEncoding)CustomHeaders["Accept-Encoding"]على مستوى الاستعلامCustomHeaders["Accept-Encoding"]على مستوى العميلClickHouseClientSettings.AcceptEncoding، أو الكلمة المفتاحيةAcceptEncodingفي سلسلة الاتصال
identity.
الخادم، لا العميل، هو من يختار المرمِّز. يفحص ClickHouse الترويسة Accept-Encoding بحثًا عن الرموز وفق ترتيب تفضيل ثابت لديه — zstd > br > lz4 > snappy > gzip > deflate — ويتجاهل ترتيب إدراجها وأي قيم q. لذا فالترويسة إعلان عن المرمِّزات المدعومة وليست طلبًا ملزمًا، ولا يمكن التأثير في الاختيار إلا باستبعاد بعض الرموز. تتضمن القائمة الافتراضية zstd، لذا تُضغط الاستجابة للاستعلام الافتراضي باستخدامه؛ وتُستخدم الرموز المتبقية بدائل احتياطية. يمكن فك ترميز br، لكنه غير مُدرج في القائمة الافتراضية المعلَن عنها.
تختلف المرمِّزات من حيث حجم الحمولة واستهلاك CPU على الخادم والعميل بحسب بياناتك واتصالك وقيمة http_zlib_compression_level على الخادم (القيمة الافتراضية المضمّنة: 3) — راجع ضبط الضغط.
http_zlib_compression_level. ينطبق هذا الإعداد على جميع مرمِّزات HTTP، وقيمته الافتراضية 3. اضبطه وفق بياناتك وسرعة اتصالك واستهلاك CPU.- عميل يحدّ أداء CPU من سرعته على اتصال سريع. يفكّ المشغّل ترميز جسم الاستجابة على خيط التنفيذ الذي استدعاه، لذا قد تصبح سرعة فك الترميز لدى العميل العاملَ المقيِّد عندما لا تكون الشبكة عنق الزجاجة.
Content-Encoding إلى ذلك، بغضّ النظر عما طُلب: إذا غابت القيمة أو كانت identity، يُترك المتن كما هو؛ وإذا أشارت إلى مرمِّز مدعوم، يُفك ترميزه؛ أما أي قيمة أخرى فتُظهر error مع ذكرها. ولا خطر من فك الترميز مرتين: فإذا كان المعالج الذي وفّره المستدعي قد فك ترميز المتن باستخدام AutomaticDecompression، فإنه يزيل أيضًا Content-Encoding، وبذلك يرى المشغّل نصًا صريحًا ويتركه كما هو.
النتائج الخام لا تشير إلى أي مرمِّز. يسلّم إليك ExecuteRawResultAsync (وكذلك الدالتان العامتان PostStreamAsync / InsertRawStreamAsync) المتن كما هو، لذا لا تطلب هذه الدوال أي مرمِّز ما لم تحدّد واحدًا بنفسك — فالمشغّل لا يفكّ ترميز هذا المتن، ولو أشارت هذه الدوال إلى مرمِّز لتحوّل التصدير دون تنبيه إلى ملف مضغوط. لذلك فالقاعدة بسيطة، ولا تتأثر بكيفية تهيئة أي HttpClient: يصل المتن كما أرسله الخادم تمامًا، ويرسل الخادم نصًا صريحًا ما لم تطلب مرمِّزًا. وإذا أردت تصدير بايتات مضغوطة، فاطلب مرمِّزًا على مستوى العميل كله أو لكل استعلام.
يظل تحديد AcceptEncoding صراحةً (على أيٍّ من المستويين) ساريًا على الطلبات الخام، ويفكّ ClickHouseRawResult.ReadDecompressedStreamAsync() ترميز النتيجة عند الحاجة؛ أما ReadAsStreamAsync وReadAsByteArrayAsync وReadAsStringAsync وCopyToAsync فتُرجع دائمًا البايتات كما وصلت تمامًا.
leaveOpen، لذا فإن التخلص منها يُبقي الاستجابة سليمة؛ أما إذا كانت غير مضغوطة، فستحصل على دفق محتوى HTTP نفسه، ويؤدي التخلص منه إلى إنهاء المتن. في الحالتين، يتولى ClickHouseRawResult إدارة الاستجابة — فلا تستدعِ أعضاء القراءة الآخرين فيه بعد التخلص من الدفق. يجب دائمًا التخلص من ClickHouseRawResult، وهذا وحده كافٍ: فهو يحرر الاستجابة وأي أداة فك ترميز أُنشئت هنا (تحتفظ أدوات فك الترميز بمخازن مؤقتة من مجمّع). لذلك، يُعدّ await using أعلاه اختياريًا، ولا ضرر من الإبقاء عليه. تُعيد الاستدعاءات المتسلسلة المتكررة الدفق نفسه؛ وهذا النوع غير آمن للاستخدام المتزامن.
راجع Select_007_ResponseCompression.cs للاطلاع على مثال قابل للتشغيل.
ضغط طلبات الإدراج
المرمِّز الافتراضي لعمليات الإدراج هو Zstd: تكون القيمة الأولية لـInsertOptions.Compressor هي ZstdCompressor.Default،
أي zstd بمستوى ضغط 3. لتغيير المرمِّز، عيّن ضاغطًا آخر لهذه القيمة، أو عيّنها إلى null لإرسال
المتن دون ضغط.
Default ومُنشئ يقبل مستوى الضغط
وحجم مخزن الكتابة المؤقت:
أعِد استخدام مثيلات الضواغط. يمثّل كل
Default مثيلًا مشتركًا واحدًا، ويمكن استخدام الضواغط
الأربعة جميعها بأمان من عدة خيوط تنفيذ في الوقت نفسه — وهذا ما يحدث عندما تكون قيمة
InsertOptions.MaxDegreeOfParallelism أكبر من 1، إذ تستخدم عملية الإدراج الواحدة ضاغطًا واحدًا لكل
دفعة. لا ينفّذ أيٌّ منها الواجهة IDisposable. أنشئ مثيلًا خاصًا بك مرة واحدة وأعِد استخدامه،
كما هو الحال مع Default.IClickHouseCompressor عامة، ولا يتعيّن على أي تنفيذ لها سوى توفير عضوين:
Content-Encoding التي تحددها. أما الأعضاء المتبقون —
Decompress وMethodByte وMaxEncodedLength وEncode وDecode — فلها تنفيذات
افتراضية ترفع NotSupportedException، لذا لا تتجاوز منها إلا ما يحتاج إليه مرمِّزك.
نفّذ Decompress لفك ضغط متون الاستجابات، إلى جانب ضغط الطلبات، واجعل الدفق الذي تعيده هذه الطريقة يرفع
InvalidDataException إذا كان المتن تالفًا أو بصيغة غير صحيحة.
يتحكم InsertOptions.Compressor في الإدراج الثنائي فقط. أما متون الطلبات الأخرى التي يرسلها المشغّل فتُضغط وفق قواعد مختلفة، ولا يمر أيٌّ منها عبره:
- كل طلب يتضمن نص SQL (
ExecuteReaderAsyncوExecuteScalarAsyncوExecuteNonQueryAsyncوQueryAsync<T>وExecuteRawResultAsyncوطبقة ADO.NET) يرسل عبارة SQL معContent-Encoding: gzipمتى كانت قيمةUseCompressionهيtrue— وهي القيمة الافتراضية. لا يمكن تغيير المرمِّز هنا: إذ يتحكمAcceptEncodingفي الاستجابة وحدها، فلا خيار للطلب سوى gzip أو عدم الضغط. وعند ضبطCompression=false، تُرسل العبارة من دون ضغط. عبارات SQL صغيرة، لذا نادرًا ما يستدعي الأمر اهتمامًا — لكن من المفيد معرفة ذلك عند مراقبة الطلبات عبر خادم وكيل أو التقاط حزم الشبكة. - المتن متعدد الأجزاء — في استعلام تُرسل معلماته بوصفها بيانات نموذج (
UseFormDataParameters=true) — يُرسل دائمًا من دون ضغط، مهما كانت قيمةUseCompression. - تحميل بيانات أولية (
InsertRawStreamAsyncوPostStreamAsync) له علامة خاصة بكل استدعاء، ولا يعتمد علىUseCompressionأوInsertOptions.Compressor: يُضغط باستخدام gzip عند تفعيل العلامة، ويُرسل من دون ضغط بخلاف ذلك. لاحظ أن القيمة الافتراضية للمعلمةuseCompressionفيInsertRawStreamAsyncهيtrue، لذا يُضغط تحميل البيانات الأولية باستخدام gzip ما لم تمرّرfalse— حتى إذا ضُبطت قيمةCompression=falseعلى العميل.
ضبط الضغط
يخفّض الضغط حجم البيانات مقابل زيادة استهلاك CPU. وتعتمد جدواه، إلى حدّ كبير، على سرعة اتصالك مقارنةً بسرعة عمل المرمِّز. ولا يوجد إعداد واحد يناسب الجميع.الرقم الذي يحسم القرار
يكون الضغط مجديًا ما دام المرمِّز أسرع من الشبكة. وهذه العتبة على مسار القراءة أقل مما يتوقعه معظم الناس، لأن ClickHouse يضغط استجابات HTTP باستخدام خيط تنفيذ واحد في مخزن الإخراج المؤقت. وفي قياس أُجري على خدمة ClickHouse Cloud تضم 16 وحدة vCPU (hits، RowBinary، المستوى 3)، أنتج الخادم بيانات مضغوطة بمعدل يتراوح بين 100 و200 ميغابايت/ثانية تقريبًا.
لذا، مع النتائج الكبيرة، وبافتراض معالجة استعلام واحد في كل مرة، لا يعود الضغط مجديًا عند معدل يقارب 100 ميغابايت/ثانية. فغالبًا ما يتجاوز دفق HTTPS واحد
داخل المنطقة السحابية نفسها هذا المعدل، بينما يقلّ عنه عادةً عند عبور الإنترنت العام أو شبكة VPN أو حدود منطقة جغرافية.
أما على مسار الإدراج، فيظل الضغط مجديًا حتى عند سرعات اتصال أعلى، لأن العميل يضغط البيانات باستخدام نواة مستقلة، ويكون عادةً أسرع من الخادم في ضغط الاستجابات.
دليل تقريبي بحسب بيئة النشر
هناك ثلاثة أمور لا يبيّنها هذا الجدول:
- تكلفة نقل البيانات الصادرة: إذا كنت تدفع رسومًا مقابل نقل البيانات، فلكل بايت تكلفة إلى جانب أثره في زمن الاستجابة، مما يرجّح زيادة الضغط بغض النظر عن سرعة الاتصال.
- النتائج الصغيرة: تنطبق جميع الحالات أعلاه على الحمولات الكبيرة. أما في الاستجابات الصغيرة، فنادرًا ما يؤثر نوع المُرمِّز، وتصبح الكلفة الإضافية لكل طلب هي العامل الغالب.
- الإدراج المتوازي يرفع عتبات الإدراج. تنطبق جميع أرقام معدل النقل أعلاه على خيط تنفيذ واحد. القيمة الافتراضية لـ
InsertOptions.MaxDegreeOfParallelismهي1، لكن زيادتها تتيح ضغط الدفعات بالتزامن، فيزداد معدل الترميز الإجمالي لدى العميل بما يتناسب تقريبًا مع عدد الأنوية المخصصة له. لذا، قد يبقى ضغط الإدراج المتوازي مجديًا على اتصال سريع حتى بعد بلوغ سرعة لا يعود عندها ضغط الإدراج بخيط تنفيذ واحد مجديًا. اعتبر أرقام الإدراج في الجدول حدًا أدنى، وإذا كنت تعالج الدفعات بالتوازي بالفعل، فأعِد الاختبار قبل أن تستنتج أن اتصالك أسرع من أن يكون الضغط مجديًا عليه.
اختيار مرمِّز
مستويات الضغط
يتحكم إعداد واحد على مستوى الخادم، هوhttp_zlib_compression_level، في ضغط الاستجابة، وينطبق على جميع مرمِّزات HTTP، لا على zlib وحده. وقيمته الافتراضية 3.
لا تغيّره إلا إذا أثبتت القياسات الحاجة إلى ذلك. فرفعه فوق القيمة الافتراضية لا يقلّل حجم البيانات إلا قليلًا، لكنه يرفع استهلاك CPU كثيرًا (مع zstd، تؤدي زيادة المستوى من 3 إلى 6 إلى مضاعفة استهلاك الخادم لوحدة CPU تقريبًا مقابل تقليل حجم البيانات بنحو 14%)، ويصبح أداء br سيئًا للغاية. أما خفضه إلى المستوى 1 فيغيّر المعادلة فعلًا: تصبح تكلفة lz4 أقل بكثير، ويفقد zstd ميزته عليه في استهلاك CPU. اضبطه لكل استعلام على حدة إذا احتجت إلى ذلك:
تحديد نقطة التعادل في بيئتك
أسرع طريقة لاختيار المرمِّز ومستوى الضغط الأنسبين هي قياس زمن تنفيذ الاستعلام نفسه باستخدام عدة مرمِّزات، ثم مقارنة النتائج.ProfileEvents من system.query_log — وحدّد
QueryOptions.QueryId لتتمكن من العثور على الصف:
LIMIT n وحدها، من دون ORDER BY، يُرجع صفوفًا مختلفة
في كل تشغيل، لذا تُضغط بيانات مختلفة في كل مرة، وتصبح نسب الضغط غير ذات دلالة. قارِن النتائج
بمجموعة نتائج ثابتة.
الإدراج من دفق بيانات خام
استخدمInsertRawStreamAsync لإدراج البيانات مباشرةً من تدفقات الملفات أو الذاكرة بصيغ مثل CSV أو JSON أو Parquet أو أي صيغة أخرى يدعمها ClickHouse.
الإدراج من ملف CSV:
راجع توثيق إعدادات الصيغ للاطلاع على الخيارات المتاحة للتحكّم في سلوك إدخال البيانات.
مزيد من الأمثلة
للاطلاع على مزيد من الأمثلة العملية، راجع دليل الأمثلة في مستودع GitHub.ADO.NET
توفّر المكتبة دعمًا كاملًا لـ ADO.NET من خلالClickHouseConnection وClickHouseCommand وClickHouseDataReader. وتلزم واجهة برمجة التطبيقات هذه للتكامل مع أدوات ربط الكائنات بقواعد البيانات (Dapper وLinq2db)، وكذلك عند الحاجة إلى واجهات التجريد القياسية لقواعد البيانات في .NET.
إدارة دورة الحياة باستخدام ClickHouseDataSource
أنشئ الاتصالات دائمًا عبرClickHouseDataSource لضمان إدارة دورة حياتها وتجميع الاتصالات على نحو سليم. يدير مصدر البيانات داخليًا مثيلًا واحدًا من ClickHouseClient، وتتشارك جميع الاتصالات مجمّع اتصالات HTTP الخاص به.
استخدام ClickHouseCommand
أنشئ أوامر باستخدام اتصال لتنفيذ عبارات SQL:ExecuteNonQueryAsync()- لتنفيذ عبارات INSERT وUPDATE وDELETE وDDLExecuteScalarAsync()- تُرجع قيمة العمود الأول من الصف الأولExecuteReaderAsync()- تُرجع كائنClickHouseDataReaderلاستعراض النتائج
استخدام ClickHouseDataReader
يتيحClickHouseDataReader الوصول إلى نتائج الاستعلامات مع الحفاظ على أنواع بياناتها:
قراءة القيمة الترتيبية للتعداد
تظهر قيمة عمودEnum8 أو Enum16 باسمها: تُرجع GetFieldType النوع string، وتُرجع
كلٌّ من GetString وGetValue وGetFieldValue<string> اسم القيمة. أما دوال الوصول إلى القيم الرقمية
فترفع الاستثناء InvalidCastException عند استخدامها مع عمود تعداد، لأن القيمة المُحتفَظ بها سلسلة نصية.
استخدم TryGetEnumOrdinal للحصول على الرقم المقابل لاسم القيمة:
true وتُعيّن value لعمود من النوع Enum8/Enum16، وكذلك لعمود من النوع Nullable(Enum...)
إذا لم تكن قيمة خليته NULL. وتُرجع false مع تعيين value إلى 0 إذا كانت قيمة الخلية NULL أو كان
العمود من نوع غير تعدادي. والقيمة الترتيبية هي
قيمة موقّعة مستخرجة من التمثيل الثنائي المنقول، لذا قد تكون سالبة، وقد تتجاوز القيمة الترتيبية للنوع Enum16 سعة
بايت واحد.
أفضل الممارسات
عمر الاتصال وتجميع الاتصالات
يستخدمClickHouse.Driver المكوّن System.Net.Http.HttpClient داخليًا. ويحتفظ HttpClient بمجمّع اتصالات لكل نقطة نهاية. وبناءً على ذلك:
- تتشارك جلسات قاعدة البيانات اتصالات HTTP التي يديرها مجمّع الاتصالات.
- يُعيد المجمّع تدوير اتصالات HTTP تلقائيًا.
- قد تظل الاتصالات نشطة حتى بعد التخلص من كائنات
ClickHouseClientأوClickHouseConnection.
التعامل مع DateTime
-
استخدم UTC كلما أمكن. خزّن الطوابع الزمنية في أعمدة من النوع
DateTime('UTC')واستخدمDateTimeKind.Utcفي شيفرتك. وبذلك تتجنب الالتباس بشأن المنطقة الزمنية. -
استخدم
DateTimeOffsetلتحديد المنطقة الزمنية صراحةً. فهو يمثّل دائمًا لحظة زمنية بعينها ويتضمن معلومات الإزاحة الزمنية. -
حدّد المنطقة الزمنية في تلميحات أنواع SQL. عند استخدام معلمات تحمل قيم DateTime من النوع
Unspecifiedمع أعمدة لا تستخدم UTC، أدرج المنطقة الزمنية في SQL:
عمليات الإدراج غير المتزامنة
تنقل عمليات الإدراج غير المتزامنة مسؤولية تجميع البيانات في دفعات من جهة العميل إلى الخادم. فبدلًا من أن يتولى العميل تجميع البيانات في دفعات، يخزّن الخادم البيانات الواردة مؤقتًا ويفرّغها إلى التخزين عند بلوغ حدود قابلة للضبط. ويفيد ذلك في الحالات التي تتزامن فيها عمليات كثيرة، مثل أعباء عمل إمكانية المراقبة التي يرسل فيها عدد كبير من الوكلاء حمولات صغيرة. فعّل عمليات الإدراج غير المتزامنة عبرCustomSettings أو سلسلة الاتصال:
wait_for_async_insert):
الإعدادات الرئيسية:
الجلسات
لا تفعّل الجلسات إلا إذا كنت بحاجة إلى ميزات على جانب الخادم تحتفظ بالحالة، مثل:- الجداول المؤقتة (
CREATE TEMPORARY TABLE) - الحفاظ على سياق الاستعلام عبر عبارات متعددة
- الإعدادات على مستوى الجلسة (
SET max_threads = 4)
أنواع البيانات المدعومة
يدعمClickHouse.Driver جميع أنواع البيانات في ClickHouse. توضّح الجداول أدناه الأنواع المقابلة لكل نوع من أنواع ClickHouse في .NET عند قراءة البيانات من قاعدة البيانات.
مطابقة أنواع البيانات: القراءة من ClickHouse
أنواع الأعداد الصحيحة
أنواع الأعداد ذات الفاصلة العائمة
الأنواع العشرية
يُحدّد إعداد UseCustomDecimals كيفية تحويل الأنواع العشرية.
النوع المنطقي
String
تُعاد أعمدة
String وFixedString(N) افتراضيًا على هيئة string. لقراءتها على هيئة byte[] بدلًا من ذلك، اضبط ReadStringsAsByteArrays=true في سلسلة الاتصال. يفيد هذا عند تخزين بيانات ثنائية قد لا تكون بترميز 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
يحدّد إعداد
JsonReadMode نوع الإرجاع لأعمدة JSON:
-
Binary(الافتراضي): يعيدSystem.Text.Json.Nodes.JsonObject. يتيح الوصول إلى بيانات JSON المهيكلة، لكن أنواع ClickHouse المتخصصة (مثل عناوين IP ومعرّفات UUID والقيم العشرية الكبيرة) تُحوَّل إلى تمثيلات نصية داخل بنية JSON. -
String: يعيد بيانات JSON الخام على هيئةstring. يحافظ على تمثيل JSON كما يرد من ClickHouse تمامًا، وهو مفيد إذا أردت تمرير البيانات دون تحليلها أو تولّي فكّ تسلسلها بنفسك.
None وضع ثالث يقرأ البيانات تمامًا كما يفعل Binary، لكنه لا يرسل أي إعداد على مستوى الخادم مع
الاستعلام — استخدمه عبر اتصال لا يُسمح له بضبط مثل هذا الإعداد.
المسار المُعلَن في نوع العمود هو مسار محدد النوع؛ أما أي مسار آخر في المستند فهو
مسار ديناميكي. ويظهر الفرق بينهما عندما تكون القيمة خالية.
يظهر المسار محدد النوع دائمًا في JsonObject. وإذا أُعلن عن نوعه بوصفه Nullable(T) أو Dynamic، فإنه يُعاد
بقيمة JSON خالية، سواء كانت القيمة المخزّنة خالية أم كان المسار غير موجود في المستند — ولا يمكن التمييز بين
الحالتين:
JSON(x String)
القيمة {"x":""}، وتُنتج JSON(x Int64) القيمة {"x":0}.
إذا كانت قيمة مسار ديناميكي null، يُحذف المسار من الكائن تمامًا، ولذلك تُرجع ContainsKey القيمة
false له. وعند قراءة {"x":null} من عمود JSON عادي، تكون النتيجة {}.
تُنشئ المسارات المتداخلة محددة النوع المسارات الأصلية لها، لذا تُنتج JSON(a.b Nullable(Int64)) القيمة {"a":{"b":null}}
حتى إذا كان المستند فارغًا.
هذا ما يعرضه الخادم نفسه، لذا أصبحت نتائج وضعي
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 بدلًا من رفع استثناء. عندئذٍ
يحتفظ المشغّل بآخر القيمتين ورودًا في الصف ويتجاهل الأخرى، مما يؤدي إلى
فقدان بعض البيانات: تُقرأ القيمة {"a.b":7} المخزّنة في JSON(a Int64, a.b Int64) بوصفها {"a":0}. أما إذا
حمل مسار قيمة وكان مساره الأب يحمل قيمة قياسية أو مصفوفة، فسيظل يُرفع استثناء، إذ لا يمكن
وضع شجرة فرعية ضمن أيٍّ منهما.
نوع Map
يُخزَّن
Map(K, V) في ClickHouse فعليًا على هيئة Array(Tuple(K, V))، ويمكن أن يضم عدة أزواج لها المفتاح نفسه. أما Dictionary فلا يدعم ذلك؛ لذا لا يحتفظ الوضع الافتراضي إلا بآخر قيمة للمفتاح المتكرر، وتُحذف الأزواج السابقة. يحدد إعداد MapReadMode طريقة التمثيل:
-
Dictionary(الوضع الافتراضي): يُرجعDictionary<K, V>. -
KeyValuePairs: يُرجعList<KeyValuePair<K, V>>وفق ترتيب إرسال الخادم للأزواج، فتُحفَظ جميعها، بما فيها الأزواج التي يتكرر فيها المفتاح.
Map، وينطبق ذلك أيضًا على GetFieldValue<T> وأنواع المخطط التي يعرضها المشغّل وربط خصائص POCO. وينطبق أينما ظهرت خريطة في شجرة نوع العمود — بما في ذلك Array(Map(...)) وMap(K, Map(...)) وTuple(..., Map(...)) وDynamic.
يُقبل كلا التمثيلين عند الكتابة، بغضّ النظر عن الوضع — راجع كتابة الخرائط.
أنواع أخرى
يُحوَّل كلٌّ من Dynamic وVariant إلى النوع الموافق للنوع الفعلي للقيمة الأساسية في كل صف.
الأنواع الهندسية
النوع Geometry هو نوع Variant يمكنه تخزين أيٍّ من الأنواع الهندسية. وتُحوَّل قيمته إلى النوع المقابل.
مطابقة الأنواع: الكتابة إلى ClickHouse
عند إدراج البيانات، يحوّل المشغّل أنواع .NET إلى أنواع ClickHouse المقابلة لها. تبيّن الجداول أدناه أنواع .NET المقبولة لكل نوع من أنواع أعمدة ClickHouse.أنواع الأعداد الصحيحة
أنواع الفاصلة العائمة
النوع المنطقي
String
أنواع التاريخ والوقت
القيم الخارجة عن النطاقعند الكتابة بالصيغة الثنائية، تُطلق قيم
Date وDate32 وDateTime وDateTime32 الخارجة عن نطاقاتها المدعومة استثناء ArgumentOutOfRangeException وقت تنفيذ Write، مع تحديد نوع العمود والنطاق المدعوم. سابقًا، كان من الممكن اقتطاع القيم الخارجة عن النطاق دون تنبيه عند تمريرها عبر عدد صحيح من 32 بت، ثم يعيد الخادم تفسيرها، فتنتج طوابع زمنية صالحة لكنها خاطئة.DateTime.Kind عند كتابة القيم:
تحافظ قيم
DateTimeOffset دائمًا على اللحظة الزمنية نفسها بدقة.
مثال: DateTime بتوقيت UTC (مع الحفاظ على اللحظة الزمنية نفسها)
DateTimeKind.Utc أو DateTimeOffset في جميع عمليات DateTime. يضمن ذلك عمل شيفرتك على نحو متسق، بغض النظر عن المنطقة الزمنية للخادم أو العميل أو العمود.
معلمات HTTP مقارنةً بالنسخ بالجملة
ثمة فرق مهم بين ربط معلمات HTTP والنسخ بالجملة عند كتابة قيم DateTime من النوعUnspecified:
النسخ بالجملة يأخذ المنطقة الزمنية للعمود الهدف في الحسبان، ويفسّر قيم Unspecified وفقًا لها تفسيرًا صحيحًا.
معلمات HTTP لا تتعرّف تلقائيًا على المنطقة الزمنية للعمود. لذا يجب تحديدها في تلميح نوع البيانات في SQL:
الأنواع العشرية
نوع 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 (الافتراضي): تُسلسَل كائنات POCO باستخدام System.Text.Json.JsonSerializer. ولا يلزم تسجيل الأنواع. وهي أبسط طريقة، وتعمل مع الكائنات ذات الأنواع مجهولة الاسم.
وضع Binary: تُسلسَل كائنات POCO باستخدام صيغة JSON الثنائية الخاصة بالمشغّل، مع دعم كامل لتلميحات الأنواع. ويجب تسجيل الأنواع باستخدام connection.RegisterJsonSerializationType<T>() قبل استخدامها. يدعم هذا الوضع ربط الخصائص بمسارات مخصّصة عبر السمات:
-
[ClickHouseJsonPath("path")]: يربط خاصيةً بمسار JSON مخصّص. ويُفيد ذلك في البُنى المتداخلة أو عندما يختلف اسم الخاصية عن مفتاح JSON المطلوب. يعمل فقط في وضع Binary. -
[ClickHouseJsonIgnore]: يستثني خاصيةً من التسلسل. يعمل فقط في وضع Binary.
UserId لا تطابق إلا تلميحًا معرّفًا بالاسم UserId، ولا تطابق userid. ويتسق ذلك مع سلوك ClickHouse، الذي يتيح وجود مسارين مثل userName وUserName بوصفهما حقلين منفصلين.
القيود (في وضع Binary فقط):
- يجب تسجيل أنواع POCO على الاتصال باستخدام
connection.RegisterJsonSerializationType<T>()قبل إجراء التسلسل. وتؤدي محاولة تسلسل نوع غير مسجّل إلى رفع الاستثناءClickHouseJsonSerializationException. - تحتاج خصائص القواميس والمصفوفات/القوائم إلى تلميحات نوع في تعريف العمود لتسلسلها على نحو صحيح. وإذا لم تتوفر هذه التلميحات، فاستخدم وضع String بدلًا من ذلك.
- لا تُكتب القيم الخالية في خصائص POCO إلا إذا كان للمسار تلميح نوع
Nullable(T)في تعريف العمود. ولا يسمح ClickHouse بأنواعNullableداخل مسارات JSON الديناميكية، لذا يُتخطّى تسلسل الخصائص ذات القيم الخالية التي لا تملك تلميحات نوع. - يُتجاهل تأثير السمتين
ClickHouseJsonPathوClickHouseJsonIgnoreفي وضع String (إذ لا تعملان إلا في وضع Binary).
أنواع أخرى
الأنواع الهندسية
غير مدعوم عند الكتابة
التعامل مع الأنواع المتداخلة
يمكن قراءة الأنواع المتداخلة في ClickHouse (Nested(...)) والكتابة فيها كما لو كانت مصفوفات.
التسجيل والتشخيص
يتكامل عميل ClickHouse لـ .NET مع تجريداتMicrosoft.Extensions.Logging لتوفير تسجيل خفيف يُفعَّل عند الطلب. وعند تفعيله، يُصدر المشغّل رسائل مهيكلة تتعلق بأحداث دورة حياة الاتصال، وتنفيذ الأوامر، وعمليات النقل، وعمليات الإدراج المجمّع. والتسجيل اختياري تمامًا؛ إذ تواصل التطبيقات التي لا تهيّئ مكوّنًا للتسجيل عملها دون أي عبء إضافي.
البدء السريع
استخدام appsettings.json
يمكنك ضبط مستويات تسجيل السجلات من خلال تهيئة .NET القياسية:استخدام التهيئة في الذاكرة
يمكنك أيضًا ضبط مستوى تفصيل التسجيل لكل فئة في الشيفرة:الفئات ومُصدِرات السجلات
يستخدم المشغّل فئات مخصّصة تتيح لك ضبط مستوى تسجيل السجلات لكل مكوّن بدقة:مثال: تشخيص مشكلات الاتصال
- اختيار مُنشئ عملاء HTTP (مجمّع الاتصالات الافتراضي أو اتصال واحد)
- تهيئة معالج HTTP (SocketsHttpHandler أو HttpClientHandler)
- إعدادات مجمّع الاتصالات (MaxConnectionsPerServer وPooledConnectionLifetime وغيرها)
- إعدادات المهلة الزمنية (ConnectTimeout وExpect100ContinueTimeout وغيرها)
- تهيئة SSL/TLS
- أحداث فتح الاتصالات وإغلاقها
- تتبّع معرّف session
وضع التصحيح: تتبّع عمليات الشبكة والتشخيص
للمساعدة في تشخيص مشكلات الشبكة، تتضمن مكتبة المشغّل عمليةً مساعدة تتيح تتبّع المكوّنات الداخلية لشبكات .NET على مستوى منخفض. لتفعيل هذه العملية، يجب تمرير LoggerFactory مع ضبط المستوى على Trace، وضبط EnableDebugMode على true (أو تفعيل التتبّع يدويًا عبر الفئةClickHouse.Driver.Diagnostic.TraceHelper). ستُسجَّل الأحداث ضمن الفئة ClickHouse.Driver.NetTrace. تحذير: سيؤدي ذلك إلى إنشاء سجلات بالغة التفصيل والتأثير سلبًا في الأداء. لا يُنصح بتفعيل وضع التصحيح في بيئة الإنتاج.
OpenTelemetry
يوفّر المشغّل دعمًا مدمجًا للتتبّع الموزّع باستخدام OpenTelemetry من خلال واجهة برمجة تطبيقات .NETSystem.Diagnostics.Activity. وعند تفعيل هذا الدعم، يُنشئ المشغّل وحدات span لعمليات قاعدة البيانات، يمكن تصديرها إلى أنظمة إمكانية المراقبة مثل Jaeger أو ClickHouse نفسه (عبر OpenTelemetry Collector).
تفعيل تتبّع التنفيذ
في تطبيقات ASP.NET Core، أضفActivitySource الخاص بمشغّل ClickHouse إلى تهيئة OpenTelemetry:
سمات span
يتضمن كل span سمات قاعدة البيانات القياسية في OpenTelemetry، إلى جانب إحصاءات استعلام خاصة بـ ClickHouse تفيد في تصحيح الأخطاء.خيارات التهيئة
تحكّم في سلوك التتبّع باستخدامClickHouseDiagnosticsOptions:
تهيئة TLS
عند الاتصال بـ ClickHouse عبر HTTPS، يمكنك ضبط إعدادات TLS/SSL بعدة طرق.التحقق المخصص من الشهادات
في بيئات الإنتاج التي تتطلب آلية مخصصة للتحقق من الشهادات، قدّمHttpClient خاصًا بك مع معالج ServerCertificateCustomValidationCallback بعد تهيئته:
اعتبارات مهمة عند توفير HttpClient مخصّص
- فك الضغط تلقائيًا: اترك
AutomaticDecompressionمعطّلًا. يفكّ المشغّل ضغط الاستجابات بنفسه، فلا حاجة إلى تفعيله — بل إن تفعيله قد يسبب مشكلة عند إرسال الطلبات: إذ يضيف المعالج أيضًا، وقت الإرسال، جميع الخوارزميات المحددة في قناعه إلى ترويسةAccept-Encodingالصادرة. وهذا يوسّع نطاق الخوارزميات التي أعلنها المشغّل، وقد يدفع ClickHouse إلى الرد باستخدام مرمِّز لم تطلبه. راجع فك ضغط الاستجابات. - مهلة خمول الاتصال: اضبط
PooledConnectionIdleTimeoutعلى قيمة أقل منkeep_alive_timeoutللخادم (10 ثوانٍ في ClickHouse Cloud) لتجنّب أخطاء الاتصال الناتجة عن الاتصالات نصف المفتوحة.
ضبط الأداء
يوضح هذا القسم كيفية استخدام العميل لتحقيق الأداء الأمثل، والخيارات التي يمكنك تعديلها لتحسين أدائه بما يناسب حالة الاستخدام لديك.نظرة سريعة
| إذا كنت | افعل الآتي | |---|---|---| | تقرأ صفوفًا في كائنات POCO | استخدمQueryAsync<T>، وليس MapTo<T> |
| تُجري عمليات إدراج كبيرة | قم بزيادة InsertOptions.BatchSize |
| تُشغّل تطبيق وحدة تحكم أو تطبيق عامل كثيف الإدراج | قم بتشغيل Server GC |
| تقرأ نتائج كبيرة عبر الشبكة | أبقِ ضغط الاستجابة قيد التشغيل (الإعداد الافتراضي) |
| تُدرج بيانات عبر اتصال سريع | جرّب InsertOptions.Compressor = null |
| تُدرج بيانات في نفس الجدول عدة مرات | استخدم UseSchemaCache أو ColumnTypes |
| تقرأ نتائج كبيرة جدًا | قم بزيادة ReadBufferSize |
القراءة: اختيار مسار التجسيد
توجد ثلاث طرق لاستخراج صف من النتائج، ولكل منها تكلفة مختلفة. تتطلّب بعض هذه المسارات تغليف القيم، مما يزيد تخصيصات الذاكرة ويضعف الأداء.
عند قراءة 1,000,000 صف يحتوي كل منها على 105 أعمدة من مجموعة بيانات hits:
تستفيد أدوات ربط الكائنات بقواعد البيانات (ORM) من مسار التنفيذ السريع عند استخدام وسائل وصول محددة النوع. يسجّل linq2db الدوال
GetInt64 و
GetDouble وGetDateTime لكل عمود، فيقرأ القيم دون تغليفها. أما الشيفرة التي تقرأ القيم عبر
GetValue (بما في ذلك النتائج من النوع dynamic في Dapper) فتغلّف كل قيمة. إذا كان استعلام ORM يُنفَّذ بكثرة
ويقرأ القيم عبر GetValue، فاستخدم QueryAsync<T> لذلك الاستعلام تحديدًا.الإدراج: حجم الدفعة والتوازي
حجم الدفعة هو العامل الأهم في تحديد معدل نقل عمليات الإدراج. تبلغ القيمة الافتراضية لـInsertOptions.BatchSize
100,000 صف.
استخدم دفعات كبيرة. عند إدراج 1,000,000 صف، أدّت زيادة حجم الدفعة من 10,000 إلى 100,000 صف
إلى النتائج التالية:
إذا تعذّر عليك التحكم في حجم الدفعة (مثلًا، عندما يرسل عدد كبير من الجهات المنتِجة الصغيرة الصفوف بصورة مستقلة)، فاستخدم عملية الإدراج غير المتزامنة واترك للخادم مهمة تجميع الصفوف في دفعات.
التحميل المتوازي. القيمة الافتراضية لـ
InsertOptions.MaxDegreeOfParallelism هي 1. ارفعها لإرسال
عدة دفعات في الوقت نفسه. وتظهر الفائدة الأكبر عند تفعيل الضغط، إذ تُضغط كل دفعة
على خيط تنفيذ مستقل. لا تتوافق الجلسات مع عمليات الإدراج المتوازية: إما أن تعطّل الجلسات، أو تُبقي
MaxDegreeOfParallelism = 1.
استغنِ عن استعلام فحص المخطط. يرسل كل استدعاء للدالة InsertBinaryAsync أولًا الاستعلام SELECT ... WHERE 1=0
لمعرفة أنواع الأعمدة. راجع تخطّي استعلام فحص المخطط لمعرفة كيفية تجنّب
هذا التبادل الإضافي مع الخادم باستخدام ColumnTypes أو UseSchemaCache.
ينطبق مسار الإدراج الذي لا يتطلب تغليف القيم على صيغة
RowBinary الافتراضية. أما RowBinaryWithDefaults فيجب عليه
فحص كل قيمة بحثًا عن وسم DBDefault، لذا يظل يستخدم المسار الأبطأ.الضغط: تختلف جدواه باختلاف اتجاه النقل
يستهلك الضغط مزيدًا من موارد CPU مقابل تقليل حجم البيانات المنقولة. وتتوقف جدوى هذه المقايضة على اتجاه النقل، وعرض نطاق اتصالك بخادم ClickHouse، ومدى استجابة بياناتك لخوارزمية الضغط المختارة، وما إذا كنت تدفع مقابل كل بايت يُنقل. القراءات: أبقِ الضغط مفعّلًا، إلا إذا كان الخادم يعمل على الجهاز نفسه. والضغط مفعّل افتراضيًا. مقارنةً بعدم استخدام الضغط، أعطىzstd عند المستوى 1
النتائج التالية:
عمليات الإدراج: قِس الأثر قبل تفعيل الضغط؛ فقد لا يكون التحسّن كافيًا لتبريره. وتذكّر أيضًا أن فك الضغط يضيف حملًا على الخادم؛ ويكون هذا الحمل محدودًا مع Zstd وLZ4، لكنه قد يرتفع مع خوارزميات أخرى (مثل Brotli).
لإيقاف ضغط عمليات الإدراج:
المخازن المؤقتة
يحدّدReadBufferSize حجم المخزن المؤقت المستخدَم لقراءة استجابات HTTP. وتبلغ قيمته الافتراضية 64 KiB.
يستعير المشغّل هذا المخزن المؤقت من مجمّع اتصالات مشترك ويعيده عند تحرير القارئ، فلا يحتاج إلى
تخصيصه من جديد لكل استعلام. زِد حجمه لتقليل عدد مرات إعادة ملئه عند قراءة النتائج
الكبيرة. يحتفظ المشغّل بمخزن مؤقت لكل قارئ مفتوح في الوقت نفسه، لذا يزداد استهلاك الذاكرة
كلما زاد حجم المخزن المؤقت أو عدد القرّاء المتزامنين.
بيئة التشغيل وجمع البيانات المهملة
فعّل Server GC في التطبيقات التي تُكثِر من عمليات الإدراج. عند استخدام الشيفرة نفسها وتخصيص العدد نفسه من البايتات، كان Workstation GC أبطأ في عمليات الإدراج من Server GC بنسبة وصلت إلى 97%.يُعد Server GC إعدادًا لتحسين معدل النقل، لا زمن الاستجابة. وفي القياسات نفسها، كان إجمالي الوقت الذي قضاه Server GC
في التوقف أقل من النصف، لكن فترات توقفه الفردية كانت أطول
(114.6 مللي ثانية عند المئين الخامس والتسعين مقابل 61.9 مللي ثانية). إذا كانت خدمتك حساسة لزمن استجابة الطلبات الأبطأ، فقِس
أداء كلا الوضعين قبل الاختيار.
زمن الاستجابة: إعادة استخدام الاتصالات
يستغرق إنشاء اتصال TCP جديد وإتمام مصافحة TLS وقتًا طويلًا نسبيًا. وتُخفّض إعادة استخدام الاتصالات زمن استجابة استعلاماتك بدرجة كبيرة.- لا تُنشئ عميلًا جديدًا لكل طلب. فكل عميل جديد يستخدم
HttpClientخاصًا به يُنشئ مجمّع اتصالات جديدًا، ما يستلزم إتمام المصافحة مجددًا. استخدم مثيلًا واحدًا منClickHouseClientطوال فترة تشغيل التطبيق. فهو آمن للاستخدام المتزامن عبر خيوط تنفيذ متعددة، ومصمّم للاستخدام كمثيل مشترك واحد. - عند استخدام ADO.NET أو أدوات ربط الكائنات بقواعد البيانات (ORM)، استخدم
ClickHouseDataSourceلتتشارك جميع الاتصالات مجمّع اتصالات واحدًا.
قِس الأداء بنفسك
في كثير من الحالات، يتوقف الأداء على طبيعة بياناتك، وسرعة اتصالك بالخادم، وما إذا كنت تفضّل تقليل استهلاك CPU لدى الخادم على حساب زيادته لدى العميل (أو العكس)، وقيود أجهزتك، وغير ذلك. لذلك، يُنصح بقياس الأداء بنفسك باستخدام بياناتك وفي بيئتك. لمعرفة مقدار العمل الذي أنجزه الخادم، عيّنQueryOptions.QueryId ثم اقرأ قيم العدّادات:
دعم أداة ربط الكائنات بقواعد البيانات (ORM)
تتطلب هذه الأدوات واجهة برمجة تطبيقات ADO.NET (ClickHouseConnection). ولإدارة عمر الاتصال إدارةً سليمة، أنشئ الاتصالات من ClickHouseDataSource:
Dapper
تعملClickHouse.Driver مع Dapper. يحوّل المشغّل تلقائيًا صيغة @parameter المستخدمة في Dapper إلى الصيغة الأصلية {parameter:Type} في ClickHouse، مع استنتاج الأنواع من قيم .NET.
استخدم ClickHouseDataSource لإدارة عمر الاتصال إدارةً صحيحة:
أساليب تمرير المعلمات
يدعم Dapper جميع أساليب تمرير المعلمات القياسية: الكائنات مجهولة الاسم:DynamicParameters (من قاموس أو كائن مجهول النوع):
الاستعلام للحصول على كائنات POCO
يربط Dapper الأعمدة بالخصائص استنادًا إلى أسمائها (دون حساسية لحالة الأحرف):صيغة المعلمات الأصلية في ClickHouse
إذا احتجت إلى تحديد نوع البيانات صراحةً، فاستخدم صيغة{param:Type} الخاصة بـ ClickHouse مباشرةً في SQL، مع 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 (بما فيها عمليات الربط والاستعلامات الفرعية وعمليات المجموعات)، و
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
التجميع والدوال التجميعية: GroupBy مع Count, LongCount, Sum, Average, Min, Max — بما في ذلك HAVING (باستخدام .Where() بعد .GroupBy())، وعدة دوال تجميعية في إسقاط واحد، وترتيب نتائج التجميع باستخدام OrderBy.
عمليات الربط: Join (INNER)، وأنماط GroupJoin/SelectMany (LEFT وCROSS). تُرجع عملية LEFT JOIN القيمة null الفعلية للصفوف التي لا تجد صفوفًا مطابقة (انظر دلالات القيم الفارغة في LEFT JOIN أدناه).
الاستعلامات الفرعية: Contains / IN المترابطة، وAny / EXISTS، وAll، والاستعلامات الفرعية ذات القيمة المفردة في الإسقاطات.
عمليات المجموعات: Concat (→ UNION ALL)، وUnion (→ UNION DISTINCT)، وIntersect، وExcept.
المجموعات المحلية المضمّنة: تُحوَّل عمليات الربط وContains التي تستخدم مجموعات موجودة في الذاكرة (int[] وList<T> وغيرها) إلى سلسلة من عمليات UNION.
طرائق السلاسل النصية: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat (والمشغّل +).
الدوال الرياضية: تُحوَّل طرائق Math وMathF القياسية إلى ما يقابلها في ClickHouse — بما يشمل الدوال الحسابية واللوغاريتمية والمثلثية والدوال المساعدة.
يُضيف المزوّد set_join_use_nulls=1 تلقائيًا إلى كل مسار اتصال، بحيث يتوافق سلوك JOIN مع توقعات Entity Framework.
إذا كان خادم ClickHouse أو ملف التعريف لديك لا يسمح بتغيير هذا الإعداد (مثل ملف تعريف readonly=1)، فيمكنك تعطيل إضافته باستخدام:
0 / "" بدلًا من == null.
إدراج البيانات
تستخدمSaveChanges واجهة برمجة التطبيقات الأصلية InsertBinaryAsync الخاصة بالمشغّل، مع ترميز RowBinary ومتن طلب مضغوط، ما يجعلها أكفأ بكثير من عبارات SQL ذات المعلمات:
Added إلى الحالة Unchanged بعد الحفظ، كما هو الحال مع أي موفّر آخر لـ EF Core.
حجم الدفعة قابل للضبط (القيمة الافتراضية 1000):
الإدراج بالجملة
لتحميل كميات كبيرة من البيانات بمعدل نقل مرتفع، استخدمBulkInsertAsync بدلًا من SaveChanges. هذه طريقة امتداد لـDbContext تتجاوز تمامًا تتبّع التغييرات في EF Core، وتوحيد هوية الكيانات، وإدارة الحالة — إذ تستدعي InsertBinaryAsync الخاص بالمشغّل مباشرةً باستخدام ترميز RowBinary ومتن طلب مضغوط.
لذلك، فهي مناسبة لتحميل مجموعات بيانات كبيرة عندما لا تحتاج إلى تتبّع الكيانات بعد إدراجها:
IEnumerable<T> — إذ تُعالَج الكيانات تباعًا دون تحميلها جميعًا في الذاكرة. والقيمة المُعادة هي عدد الصفوف المُدرجة. لا تُرفَق الكيانات بـDbContext بعد الإدراج، لذا لا تنتقل حالتها من Added إلى Unchanged.
التعدادات
يمكن تعيين أعمدة ClickHouse من النوعينEnum8/Enum16 إلى خصائص من النوع string أو إلى أنواع enum في C#. وعند استخدام تعدادات C#، يحوّل موفّر الخدمة تلقائيًا بين قيمة التعداد وتمثيلها النصي:
تحويلات الأنواع المخصصة
يتيح لك نظامValueConverter في EF Core تحويل الأنواع المخصصة إلى أنواع يدعمها موفّر قاعدة البيانات. ولا يرى الموفّر نوعك المخصص مطلقًا، لأن EF Core يتولى تحويله عند الحد الفاصل بينهما.
التحويل لكل خاصية:
تعليقات توضيحية لأنواع الأعمدة
يستنتج الموفّر نوع ClickHouse تلقائيًا للأنواع ذات القيمة المفردة مثلstring وint وDateTime وغيرها. أما الأنواع ذات المعلمات والأنواع المغلِّفة، فيجب تحديد نوع ClickHouse لها صراحةً.
باستخدام التعليقات التوضيحية للبيانات (السمات):
OnModelCreating:
Array(Nullable(Int32)) وLowCardinality(Nullable(String)) — إذ يزيل الموفّر غلافَي Nullable وLowCardinality تلقائيًا في كل مستوى من مستويات التداخل.
أعمدة Variant وDynamic
تُمثَّل أعمدةVariant(T1, T2, ...) وDynamic في ClickHouse بالنوع object في .NET. ولأن object نوع عام جدًا لا يسمح باستنتاج النوع تلقائيًا، يجب تحديد نوع التخزين صراحةً باستخدام .HasColumnType():
string وulong وulong[]).
أعمدة JSON
يدعم الموفّر نوع الأعمدةJson في ClickHouse، ويحوّله إلى System.Text.Json.Nodes.JsonNode (النوع الأساسي) أو string (باستخدام ValueConverter تلقائي):
SaveChanges وBulkInsertAsync:
string مع جعل نوع العمود Json — إذ يطبّق موفّر الخدمة ValueConverter تلقائيًا:
- لا يُحوَّل مسار JSON — لا يُحوَّل التعبير
entity.Data["name"]في LINQ إلى صيغةdata.nameفي SQL الخاصة بـ ClickHouse. صفِّ النتائج باستخدام أعمدة غير مخزّنة بنوع JSON، وافحص بيانات JSON في الذاكرة. - دلالات NULL — يعيد نوع JSON في 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أي أثر. لا يدعم ClickHouse معاملات ACID. - ترجمة استعلامات مسارات JSON: لا تُترجم
entity.Data["key"]في LINQ إلى بناء جملة SQL data.keyالخاص بـ ClickHouse. صفِّ النتائج باستخدام أعمدة غير JSON، وافحص JSON في الذاكرة.
القيود
الصفوف المرتبة التي تضم 8 عناصر أو أكثر ويكون عنصرها الأخير صفًا مرتبًا متداخلًا
تستخدم أنواعValueTuple في C# التي تضم أكثر من 7 عناصر طريقة تداخل يُنشئها المصرّف: وسيطة النوع العامة الثامنة (TRest) هي نفسها ValueTuple تضم العناصر المتبقية. على سبيل المثال، يُصرَّف (int, int, int, int, int, int, int, string, string) إلى ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
ينشأ التباس عندما يكون عمود ClickHouse صفًا مرتبًا من 8 عناصر، ويكون عنصره الأخير صفًا مرتبًا أيضًا، مثل Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). فلا يستطيع المشغّل التمييز بين:
- صف مرتب مسطّح من 9 عناصر (ناتج عن تداخل TRest الذي يُنشئه المصرّف)
- صف مرتب من 8 عناصر يكون عنصره الأخير
Tuple(String, String)متداخلًا
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
يتعامل المشغّل مع الوسيطة الثامنة بوصفها TRest (أي يبسط بنيتها المتداخلة)، ولذلك يُسلسِل الصف المرتب المكوّن من 8 عناصر، والذي يكون عنصره الأخير صفًا مرتبًا متداخلًا، على نحو غير صحيح.
ينطبق ذلك على كلٍّ من System.Tuple وValueTuple، إذ يستخدم كلاهما تداخل TRest عند تجاوز عدد العناصر 7. أما الصفوف المرتبة التي تضم 7 عناصر أو أقل، أو التي لا يكون عنصرها الأخير صفًا مرتبًا، فلا تتأثر.
الحل البديل: غلّف الصف المرتب الداخلي بطبقة إضافية حتى يتمكن المشغّل من تمييزه عن تداخل TRest:
أعمدة AggregateFunction
لا يمكن الاستعلام مباشرةً عن الأعمدة من النوعAggregateFunction(...) أو إدراج البيانات فيها مباشرةً.
للإدراج: