Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

عميل C# الرسمي لـ ClickHouse

عميل C# الرسمي للاتصال بـ ClickHouse. تتوفّر الشيفرة المصدرية للعميل في مستودع GitHub. طوّره في الأصل Oleg V. Kozlyuk.

توفّر المكتبة واجهتَي برمجة تطبيقات رئيسيتين:

  • ClickHouseClient (موصى به): عميل عالي المستوى وآمن للاستخدام من عدة خيوط، ومصمَّم للاستخدام بنمط singleton. يوفّر واجهة برمجة تطبيقات غير متزامنة وبسيطة للاستعلامات وعمليات الإدراج المجمّع. وهو الأنسب لمعظم التطبيقات.

  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): تجريدات قياسية لقواعد البيانات في .NET. وهي مطلوبة لتكامل ORM ‏(Dapper وLinq2db) وعندما تحتاج إلى التوافق مع ADO.NET. تُعد ClickHouseBulkCopy فئة مساعدة لإدراج البيانات بكفاءة باستخدام اتصال ADO.NET. الفئة ClickHouseBulkCopy مُهمَلة وستُزال في إصدار مستقبلي؛ استخدم ClickHouseClient.InsertBinaryAsync بدلاً منها.

تشترك كلتا الواجهتين في مجمّع اتصالات HTTP الأساسي نفسه، ويمكن استخدامهما معًا داخل التطبيق نفسه.

دليل الترحيل

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

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

يدعم ClickHouse.Driver إصدارات ‎.NET‎ التالية:

  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

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

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

التثبيت

ثبّت الحزمة من NuGet:

dotnet add package ClickHouse.Driver

أو باستخدام مدير حزم NuGet:

Install-Package ClickHouse.Driver

البدء السريع

using ClickHouse.Driver;

// Create a client (typically as a singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Execute a query
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);

التهيئة

هناك طريقتان لتهيئة اتصالك بـ ClickHouse:

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

فيما يلي قائمة كاملة بجميع الإعدادات وقيمها الافتراضية وتأثيراتها.

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

الخاصية النوع الافتراضي مفتاح سلسلة الاتصال الوصف
المضيف string "localhost" Host اسم المضيف أو عنوان IP لخادم ClickHouse
المنفذ ushort 8123 (HTTP) / 8443 (HTTPS) Port رقم المنفذ؛ تُحدَّد القيمة الافتراضية حسب البروتوكول
اسم المستخدم string "default" Username اسم المستخدم للمصادقة
كلمة المرور string "" Password كلمة المرور للمصادقة
قاعدة البيانات string "" Database قاعدة البيانات الافتراضية؛ عند تركها فارغة، تُستخدَم القيمة الافتراضية للخادم أو المستخدم
البروتوكول string "http" Protocol بروتوكول الاتصال: "http" أو "https"
المسار string null Path مسار URL في سيناريوهات الوكيل العكسي (مثل /clickhouse)
المهلة TimeSpan دقيقتان Timeout مهلة العملية (تُخزَّن بالثواني في سلسلة الاتصال)

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

الخاصية النوع الافتراضي مفتاح سلسلة الاتصال الوصف
UseCompression bool true Compression تفعيل ضغط gzip لنقل البيانات
UseCustomDecimals bool true UseCustomDecimals استخدم ClickHouseDecimal للدقة الاعتباطية؛ وإذا كانت القيمة false، فسيُستخدم decimal في .NET (بحد 128 بت)
ReadStringsAsByteArrays bool false ReadStringsAsByteArrays اقرأ أعمدة String وFixedString كمصفوفات byte[] بدلًا من string؛ وهذا مفيد للبيانات الثنائية
UseFormDataParameters bool false UseFormDataParameters أرسل المعلمات كبيانات نموذج بدلًا من سلسلة استعلام URL
ReadBufferSize int 8192 (8 KiB) ReadBufferSize الحجم بالبايت للمخزن المؤقت المستخدم لقراءة استجابات استعلامات HTTP. زِدْه لتقليل إعادة تعبئة المخزن المؤقت لمجموعات النتائج الكبيرة. تُخصَّص القيم التي تبلغ 85,000 بايت أو أكثر على كومة الكائنات الكبيرة، مما قد يسبب تجزئة LOH وفترات توقف أطول لـ GC.
ParameterTypeResolver IParameterTypeResolver null مُحلِّل مخصص لتعيين نوع المعلمات بأسلوب @؛ راجع تعيين نوع المعلمات المخصص
ParameterFormatter IParameterFormatter null مُنسِّق مخصص لتسلسل قيم المعلمات؛ راجع تنسيق قيمة المعلمات المخصص
ReadValueConverter IReadValueConverter null مُحوِّل مخصص يُطبَّق على القيم التي يُرجعها قارئ البيانات؛ راجع تحويل قيمة القراءة المخصص
JsonReadMode JsonReadMode Binary JsonReadMode كيفية إرجاع بيانات JSON: Binary (يعيد JsonObject) أو String (يعيد سلسلة JSON الخام)
JsonWriteMode JsonWriteMode String JsonWriteMode كيفية إرسال بيانات JSON: String (يُسلسِل عبر JsonSerializer ويقبل جميع المدخلات) أو Binary (كائنات POCO المسجَّلة فقط مع تلميحات النوع)

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

Property Type Default Connection String Key Description
UseSession bool false UseSession تمكين الجلسات ذات الحالة؛ يجعل الطلبات تُنفَّذ تسلسليًا
SessionId string null SessionId معرّف الجلسة؛ يُنشئ GUID تلقائيًا إذا كانت القيمة null وكان UseSession يساوي true

الأمان

الخاصية النوع الافتراضي مفتاح سلسلة الاتصال الوصف
SkipServerCertificateValidation bool false تجاوز التحقق من شهادة HTTPS؛ غير مخصص للاستخدام في بيئة الإنتاج

تهيئة عميل HTTP

الخاصية النوع الافتراضي مفتاح سلسلة الاتصال الوصف
HttpClient HttpClient null مثيل HttpClient مخصص ومُهيأ مسبقًا
HttpClientFactory IHttpClientFactory null مصنع مخصص لإنشاء مثيلات HttpClient
HttpClientName string null اسم يستخدِمه HttpClientFactory لإنشاء عميل معيّن

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

Property Type Default Connection String Key Description
LoggerFactory ILoggerFactory null مصنع logger المستخدم للتسجيل التشخيصي
EnableDebugMode bool false تمكين تتبّع الشبكة في .NET (يتطلب LoggerFactory مع ضبط المستوى على Trace)؛ تأثير ملحوظ على الأداء

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

الخاصية النوع القيمة الافتراضية مفتاح سلسلة الاتصال الوصف
CustomSettings IDictionary<string, object> فارغ البادئة set_* إعدادات خادم ClickHouse، راجع الملاحظة أدناه
Roles IReadOnlyList<string> فارغ Roles أدوار ClickHouse مفصولة بفواصل (مثل Roles=admin,reader)
ApplicationInfo IReadOnlyDictionary<string, string> فارغ وسوم حرة تُضاف إلى ترويسة HTTP ‏User-Agent لإسناد الاستعلامات إلى كل تطبيق على حدة.

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

اتصال بسيط

Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb

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

Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000

QueryOptions

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

الخاصية النوع الوصف
QueryId string معرّف استعلام مخصّص للتتبّع في system.query_log أو للإلغاء
Database string تجاوز قاعدة البيانات الافتراضية لهذا الاستعلام
Roles IReadOnlyList<string> تجاوز أدوار العميل لهذا الاستعلام
CustomSettings IDictionary<string, object> إعدادات خادم ClickHouse لهذا الاستعلام (مثل max_threads)
CustomHeaders IDictionary<string, string> ترويسات HTTP إضافية لهذا الاستعلام
UseSession bool? تجاوز سلوك الجلسة لهذا الاستعلام
SessionId string معرّف الجلسة لهذا الاستعلام (يتطلب UseSession = true)
BearerToken string تجاوز رمز المصادقة لهذا الاستعلام
ParameterTypeResolver IParameterTypeResolver تجاوز المُحلِّل على مستوى العميل لتعيين نوع المعلَمات بنمط @؛ راجع تعيين نوع المعلَمات المخصّص
ParameterFormatter IParameterFormatter تجاوز المُنسِّق على مستوى العميل لتسلسل قيم المعلَمات بنمط @؛ راجع تنسيق قيمة المعلمات المخصص
ReadValueConverter IReadValueConverter تجاوز المُحوِّل على مستوى العميل المُطبَّق على القيم المُعادة بواسطة قارئ البيانات؛ راجع تحويل قيمة القراءة المخصص
MaxExecutionTime TimeSpan? مهلة الاستعلام من جانب الخادم (تُمرَّر كإعداد max_execution_time)؛ يُلغي الخادم الاستعلام إذا تم تجاوزها
AcceptEncoding string تجاوز ترويسة Accept-Encoding لكل استعلام على حدة (مثل "zstd" و"zstd, gzip;q=0.5")؛ كما يفرض أيضًا enable_http_compression=1 على URL. راجع ضغط النقل لكل استعلام.

مثال:

var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

InsertOptions

يوسّع InsertOptionsQueryOptions بإعدادات خاصة بعمليات الإدراج المجمّع عبر InsertBinaryAsync.

الخاصية النوع القيمة الافتراضية الوصف
BatchSize int 100,000 عدد الصفوف في كل دفعة
MaxDegreeOfParallelism int 1 عدد عمليات رفع الدُفعات المتوازية
Format RowBinaryFormat RowBinary التنسيق الثنائي: RowBinary أو RowBinaryWithDefaults
ColumnTypes IReadOnlyDictionary<string, string> null اسم العمود ← سلسلة النوع في ClickHouse. يتجاوز استعلام فحص المخطط عند تعيينه.
UseSchemaCache bool false يخزّن مخطط الجدول الكامل لكل زوج من (database, table) طوال مدة حياة العميل.

تتوفّر أيضًا جميع خصائص QueryOptions في InsertOptions.

مثال:

var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);

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

بشكل افتراضي، يرسل InsertBinaryAsync استعلام SELECT ... WHERE 1=0 قبل كل عملية إدراج لاكتشاف أنواع الأعمدة. في السيناريوهات ذات معدل النقل المرتفع، يمكنك التخلص من هذا العبء الإضافي باستخدام خيارين:

الخيار 1: تحديد أنواع الأعمدة صراحةً

عندما تكون على دراية بمخطط الجدول وقت التجميع، مرّره مباشرةً عبر ColumnTypes. عندها لن يُرسل أي استعلام للمخطط على الإطلاق:

var options = new InsertOptions
{
    ColumnTypes = new Dictionary<string, string>
    {
        ["id"] = "UInt64",
        ["name"] = "Nullable(String)",
        ["score"] = "Float32",
    },
};

await client.InsertBinaryAsync("my_table", ["id", "name", "score"], rows, options);

الخيار 2: تخزين المخطط مؤقتًا

عند الإدراج في الجدول نفسه بشكل متكرر، اضبط UseSchemaCache = true للاستعلام عن المخطط مرة واحدة ثم إعادة استخدامه في عمليات الإدراج اللاحقة ضمن مثيل ClickHouseClient نفسه:

var options = new InsertOptions { UseSchemaCache = true };

// First call fetches schema from the server
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Second call reuses cached schema — no extra round-trip
await client.InsertBinaryAsync("my_table", columns, batch2, options);

ClickHouseClient

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

إنشاء عميل

أنشئ ClickHouseClient باستخدام سلسلة اتصال أو كائن ClickHouseClientSettings. راجع قسم التهيئة للاطّلاع على الخيارات المتاحة.

تتوفّر تفاصيل خدمة ClickHouse Cloud الخاصة بك في وحدة تحكم ClickHouse Cloud.

حدّد خدمة وانقر على Connect:

زر الاتصال بخدمة ClickHouse Cloud

اختر C#. ستُعرض تفاصيل الاتصال أدناه.

تفاصيل اتصال ClickHouse Cloud لـ C#

إذا كنت تستخدم ClickHouse مُدارًا ذاتيًا، فسيُحدِّد مسؤول ClickHouse لديك تفاصيل الاتصال.

باستخدام سلسلة اتصال:

using ClickHouse.Driver;

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");

أو باستخدام ClickHouseClientSettings:

using ClickHouse.Driver;

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);

في حالات استخدام حقن التبعية، استخدم IHttpClientFactory:

// In your DI configuration
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");

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

استخدم ExecuteNonQueryAsync مع التعليمات التي لا تُرجع نتائج:

// Create a table
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Drop a table
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");

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

var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Row count: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Server version: {version}");

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

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

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

using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("id", 1L);
parameters.AddParameter("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);

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

استخدم InsertBinaryAsync لإدراج أعداد كبيرة من الصفوف بكفاءة. فهو يمرّر البيانات بتنسيق الصفوف الثنائي الأصلي في ClickHouse، ويدعم تحميل الدُفعات بالتوازي، ويتجنب أخطاء "URL طويل جدًا" التي قد تحدث مع الاستعلامات ذات المعلمات.

// Prepare data as IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// Basic insert
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");

لمجموعات البيانات الكبيرة، اضبط التجميع على دفعات ومستوى التوازي باستخدام InsertOptions:

var options = new InsertOptions
{
    BatchSize = 100_000,           // Rows per batch (default: 100,000)
    MaxDegreeOfParallelism = 4     // Parallel batch uploads (default: 1)
};

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

بدلاً من إنشاء مصفوفات object[]، يمكنك إدراج كائنات POCO محددة النوع مباشرةً. سجّل النوع مرة واحدة، ثم مرّر IEnumerable<T>:

// Define a POCO matching your table columns
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Register the type (once per client lifetime)
client.RegisterBinaryInsertType<SensorReading>();

// Insert directly — column names are derived from property names
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

long rowsInserted = await client.InsertBinaryAsync("sensors", readings);

افتراضيًا، تُعيَّن جميع الخصائص العامة القابلة للقراءة إلى أعمدة باستخدام مطابقة صارمة للأسماء تراعي حالة الأحرف. يمكنك تخصيص هذا التعيين باستخدام السمات:

public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Map to a differently-named column
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Explicit ClickHouse type
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Exclude from insert
    public string InternalTag { get; set; }
}
السمة الغرض
[ClickHouseColumn(Name = "...")] تجاوز اسم العمود الهدف
[ClickHouseColumn(Type = "...")] التصريح بنوع ClickHouse صراحةً
[ClickHouseNotMapped] استبعاد الخاصية من عملية الإدراج

عندما تحدد جميع الخصائص المعينة Type صريحًا، يُتخطّى استعلام فحص المخطط بالكامل. وعندما تكون بعض الخصائص فقط ذات أنواع صريحة، يعود برنامج التشغيل إلى فحص المخطط لمجموعة الأعمدة الكاملة.

يدعم InsertBinaryAsync<T> خيارات InsertOptions نفسها (التجميع على دفعات، التوازي، التخزين المؤقت للمخطط) مثل التحميل الزائد object[].

تطور المخطط

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


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

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

استدعِ Read() للانتقال إلى الصف التالي. وتُرجع false عند عدم وجود المزيد من الصفوف. ويمكنك الوصول إلى الأعمدة حسب الفهرس (ابتداءً من 0) أو حسب اسم العمود.

using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}

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

بدلًا من قراءة الأعمدة حسب الفهرس أو الاسم، يمكنك تمرير نتائج الاستعلام مباشرةً إلى فئاتك الخاصة. سجّل النوع مرة واحدة في العميل، ثم استخدم QueryAsync<T>:

// Define a POCO matching your result columns
public class SensorReading
{
    public ulong Id { get; set; }
    public DateTime Timestamp { get; set; }

    [ClickHouseColumn(Name = "sensor_name")]
    public string SensorName { get; set; }
    public double Value { get; set; }

}

// Register the type (once per client lifetime)
client.RegisterPocoType<SensorReading>();

// Stream results as typed objects
await foreach (var reading in client.QueryAsync<SensorReading>(
    "SELECT Id, sensor_name, Value, Timestamp FROM sensors"))
{
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
التسجيل

يُعِدّ RegisterPocoType<T>() كلاً من عمليات ربط الإدراج والقراءة، ويتحقق من كليهما مسبقًا. أما RegisterBinaryInsertType<T>() فلم يتغير، ويظل مخصصًا للإدراج فقط حفاظًا على التوافق مع الإصدارات السابقة.

يجب أن يتضمن النوع المسجَّل ما يلي:

  • مُنشئًا عامًا بدون معاملات.
  • خاصية عامة واحدة على الأقل لها مُعدِّل set عام وليس init. الخصائص required مدعومة.
مطابقة الأعمدة

مطابقة الأعمدة حسّاسة لحالة الأحرف. تترك أعمدة النتائج المفقودة الخصائص على قيمتها الافتراضية، بينما يتم تجاهل أعمدة النتائج الإضافية.

لا توجد تحويلات تلقائية، ويؤدي عدم تطابق النوع إلى ظهور InvalidOperationException.

تحويل صف واحد إلى كائن

عند التكرار على القارئ يدويًا، استخدم ClickHouseDataReader.MapTo<T>() لتحويل الصف الحالي إلى POCO مسجَّل دون تحريك القارئ إلى الصف التالي:

var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
تشخيصات التسجيل

عند تهيئة LoggerFactory، يُصدر كلٌّ من RegisterPocoType<T>() وRegisterBinaryInsertType<T>() سجلًا على مستوى Debug (ضمن الفئة ClickHouse.Driver.Client) يبيّن الخصائص التي طابقت الأعمدة، وتلك التي جرى تخطيها وسبب ذلك. راجع التسجيل والتشخيصات.


معلمات SQL

في ClickHouse، الصيغة القياسية لمعلمات الاستعلام في استعلامات SQL هي {parameter_name:DataType}.

أمثلة:

SELECT {value:Array(UInt16)} as a
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})

معلمات Identifier

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

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

var reader = await client.ExecuteReaderAsync("SELECT {col:Identifier} FROM t", parameters);

تُرسَل القيمة كما هي حرفيًا، ويستبدلها الخادم كمُعرّف SQL خام، مع تطبيق الإحاطة بعلامات backtick وإفلات الأحرف الخاصة وفق آليته الخاصة. ويمكن نقل المعرّفات التي تحتوي على أحرف خاصة (بما في ذلك علامات backtick) ذهابًا وإيابًا بأمان.


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

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

var options = new QueryOptions
{
    QueryId = $"report-{Guid.NewGuid()}"
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

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

عند استخدام المعلمات بأسلوب @ (مثل WHERE id = @id)، يستنتج برنامج التشغيل تلقائيًا نوع ClickHouse من نوع القيمة في ‎.NET. على سبيل المثال، يُعيَّن int إلى Int32.

لتجاوز هذه الإعدادات الافتراضية، عيّن ParameterTypeResolver في ClickHouseClientSettings. ويكون ذلك مفيدًا عندما تريد استخدام DateTime64(3) لجميع معلمات DateTime بدقة الملّي ثانية، أو استخدام قيمة scale محددة لجميع قيم Decimal، من دون تعيين ClickHouseType لكل معلمة على حدة.

استخدام DictionaryParameterTypeResolver لتعيينات الأنواع البسيطة:

using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterTypeResolver = new DictionaryParameterTypeResolver(new Dictionary<Type, string>
    {
        [typeof(DateTime)] = "DateTime64(3)",
        [typeof(decimal)] = "Decimal64(4)",
    }),
};
using var client = new ClickHouseClient(settings);

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("dt", DateTime.UtcNow);     // Mapped to DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Mapped to Decimal64(4)

await client.ExecuteReaderAsync("SELECT @dt, @amount", parameters);

IParameterTypeResolver مخصّص للحالات المتقدمة:

للاستدلال المعتمد على القيمة أو المستند إلى الاسم، نفِّذ الواجهة IParameterTypeResolver مباشرةً. أرجِع null للانتقال إلى الاستدلال الافتراضي:

public class SmartDecimalResolver : IParameterTypeResolver
{
    public string ResolveType(Type clrType, object value, string parameterName)
    {
        if (clrType != typeof(decimal))
            return null; // Fall through to default

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}

يمكنك أيضًا تعيين محلِّل لاستعلام واحد عبر QueryOptions.ParameterTypeResolver. وعند تعيينه، تكون له أولوية أعلى من المحلِّل على مستوى العميل.

ترتيب أولوية تحديد النوع:

المحلِّل ليس سوى خطوة ضمن سلسلة ترتيب الأولوية. من الأعلى إلى الأدنى أولوية:

  1. تعيين ClickHouseType صراحةً على المعلَمة
  2. تلميح نوع SQL من الصيغة {name:Type} في الاستعلام
  3. IParameterTypeResolver (من QueryOptions.ParameterTypeResolver، مع الرجوع إلى ClickHouseClientSettings.ParameterTypeResolver)
  4. استنتاج النوع المضمَّن (TypeConverter.ToClickHouseType)

يعمل المحلِّل أيضًا مع مسار ADO.NET ClickHouseConnection — إذ ترث الاتصالات المُنشأة من العميل هذه الإعدادات.


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

IParameterFormatter هو خطاف يحدّد كيفية تسلسل قيم المعلمات. استخدمه عندما لا يتوافق التنسيق المضمّن (مثل دقة DateTime، والإعدادات المحلية لـ Decimal، وإفلات السلاسل النصية، وتمثيل الأرقام) مع ما يتوقعه المخطط أو الأدوات اللاحقة.

عيّن ParameterFormatter في ClickHouseClientSettings لتثبيت منسّق لجميع الاستعلامات المعلَّمة بمعلمات. يتلقى المنسّق القيمة، واسم نوع ClickHouse الذي جرى تحديده، واسم المعلمة، ويُرجع التمثيل النصي الذي يُرسَل إلى الخادم. أعد null للرجوع إلى المنسّق الافتراضي.

استخدام DictionaryParameterFormatter للتنسيق البسيط لكل نوع CLR على حدة:

using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterFormatter = new DictionaryParameterFormatter(new Dictionary<Type, Func<object, string>>
    {
        [typeof(DateTime)] = v => ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.ffffff",
            System.Globalization.CultureInfo.InvariantCulture),
        [typeof(decimal)] = v => ((decimal)v).ToString("F4",
            System.Globalization.CultureInfo.InvariantCulture),
    }),
};
using var client = new ClickHouseClient(settings);

IParameterFormatter مخصّص للحالات المتقدمة:

public class FixedDecimalFormatter : IParameterFormatter
{
    public string Format(object value, string typeName, string parameterName)
    {
        if (value is decimal d)
            return d.ToString("F4", System.Globalization.CultureInfo.InvariantCulture);
        return null; // Fall through for anything else
    }
}

يمكنك أيضًا تعيين منسّق لكل استعلام عبر QueryOptions.ParameterFormatter. وعند تعيينه، تكون له الأولوية على المنسّق على مستوى العميل.

القيم المركّبة:

يُطبَّق المنسّق على معلمات المجموعات ذات المستوى الأعلى، وكذلك على كل عنصر داخل القيم المركّبة (Array, Tuple, Map, Nullable, LowCardinality, Variant). على سبيل المثال، تؤدي مطابقة typeof(int) إلى تنسيق كل عنصر Int32 داخل Array(Int32) على حدة.

الإحاطة بعلامات الاقتباس المفردة في السياقات المركّبة:

بالنسبة إلى أنواع ClickHouse الشبيهة بالسلاسل النصية (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) المضمّنة داخل قيمة حرفية مركّبة، يحيط برنامج التشغيل مخرجات المنسّق بعلامات اقتباس مفردة، لكنه لا يجري إفلاتًا لمحتواها. إذا كانت السلسلة التي يعيدها المنسّق تحتوي على علامة اقتباس مفردة أو شرطة مائلة عكسية غير مُفلَتة، فستصبح القيمة الحرفية المركّبة غير صحيحة، وسيرفض الخادم الاستعلام.

تُستخدم معلمات السلاسل النصية ذات المستوى الأعلى (غير المضمّنة داخل قيمة مركّبة) كما هي، من دون إحاطة، لذا لا يلزم الإفلات هنا.

أولوية المنسّق:

  1. IParameterFormatter (من QueryOptions.ParameterFormatter، مع الرجوع إلى ClickHouseClientSettings.ParameterFormatter). إذا أعاد قيمة غير null، فستُستخدم هذه القيمة.
  2. التنسيق المضمّن الخاص بالنوع في HttpParameterFormatter.

لا يُستشار المنسّق لقيم null أو DBNull؛ إذ تُسلسَل هذه القيم دائمًا باعتبارها مؤشر ClickHouse للقيم null (\N).


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

يتيح لك IReadValueConverter تحويل القيم التي يعيدها قارئ البيانات بعد إلغاء التسلسل، من دون تغيير نوع CLR الخاص بها. ومن الاستخدامات الشائعة: تعيين DateTime.Kind = Utc لعمود DateTime لا يحتوي على timezone، أو إزالة المسافات الزائدة من السلاسل النصية أو تطبيعها، أو إجراء معالجة لاحقة على عمود JSON قبل أن يصل إلى شيفرة التطبيق.

عيّن ReadValueConverter في ClickHouseClientSettings لتطبيق محوّل على جميع عمليات القراءة. ويُستدعى المحوّل مرة واحدة لكل عمود في كل صف عبر كلٍّ من المسار المغلّف (GetValue) والمسار العام (GetFieldValue<T>). وعند عدم تعيين أي محوّل، لا توجد أي كلفة إضافية — إذ يعيد القارئ القيم مباشرةً.

استخدام DictionaryReadValueConverter لإجراء تحويل بسيط لكل نوع CLR:

using ClickHouse.Driver.ADO.Readers;

var converter = new DictionaryReadValueConverter()
    .For<DateTime>(dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))
    .For<string>(s => s.Trim());

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ReadValueConverter = converter,
};
using var client = new ClickHouseClient(settings);

تمرّ القيم التي لم يُسجَّل نوع CLR الخاص بها وقت التشغيل باستخدام For<T> كما هي من دون تغيير. ويجري التوجيه وفق نوع CLR المطابق تمامًا، لذا سجّل النوع الفعلي الذي يُنتجه القارئ (على سبيل المثال، For<JsonObject> لعمود JSON في JsonReadMode.Binary).

IReadValueConverter مخصّص للحالات المتقدمة:

إذا كنت بحاجة إلى التوجيه استنادًا إلى سلسلة النوع من جانب ClickHouse (على سبيل المثال، للتمييز بين DateTime وDateTime('UTC') — إذ يظهر كلاهما على أنهما نوع CLR نفسه)، فنفّذ IReadValueConverter مباشرةً:

public class UtcKindForNoTzDateTimeConverter : IReadValueConverter
{
    public object ConvertValue(object value, string columnName, string clickhouseType)
    {
        if (value is DateTime dt && clickhouseType == "DateTime")
            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }

    public T ConvertValue<T>(T value, string columnName, string clickhouseType)
    {
        if (typeof(T) == typeof(DateTime) && value is DateTime dt && clickhouseType == "DateTime")
            return (T)(object)DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }
}

يجب أن يحافظ المحوّل على نوع CLR وقت التشغيل؛ إذ لا يتم تمرير البيانات الوصفية للأعمدة (GetFieldType, GetSchemaTable) عبره، ويجب أن تظل متسقة مع ما يتم إرجاعه.

يمكنك أيضًا تعيين محوّل لكل استعلام عبر QueryOptions.ReadValueConverter؛ وعند تعيينه، تكون له الأولوية على المحوّل على مستوى العميل.

حدود الإرسال:

يُستدعى المحوّل مرة واحدة لكل عمود مع قيمة الخلية كاملة بعد فك التسلسل، وهو لا يتعمق تكراريًا داخل الحاويات المركبة. بالنسبة إلى عمود Array(Int32)، تكون القيمة المُمرَّرة هي int[]؛ وبالنسبة إلى Tuple(Int32, String)، تكون ITuple.

يعمل المحوّل مع مسار ClickHouseConnection في ADO.NET — وترث الاتصالات المُنشأة من العميل هذه الإعدادات.


البث الخام

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

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();

التنسيقات الشائعة: JSONEachRow، CSV، TSV، Parquet، Native. راجع توثيق التنسيقات للتعرّف على جميع الخيارات.


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

افتراضيًا، يتفاوض العميل على gzip, deflate عندما تكون Compression=true (وهو الإعداد الافتراضي في سلسلة الاتصال)، ويتولى عميل HTTP فك ضغط الدفق تلقائيًا وبشفافية.

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

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT Parquet",
    options: new QueryOptions { AcceptEncoding = "zstd" });

// Decode yourself or write to a file
await using var body = await result.ReadAsStreamAsync();

إعداد HttpClient

يأتي HttpClient الافتراضي الذي ينشئه برنامج التشغيل مع AutomaticDecompression = GZip | Deflate، ما يعني أنه يفك ضغط هاتين الخوارزميتين تلقائيًا ويزيل Content-Encoding من الاستجابة. وهذا هو السلوك المطلوب للاستعلامات العادية، لكن ليس إذا كنت تريد التعامل بنفسك مع البيانات الخام المضغوطة.

في هذه الحالة، مرّر HttpClient أو HttpClientFactory إلى ClickHouseClient مع AutomaticDecompression = DecompressionMethods.None

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

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


إدراج تدفق خام

استخدم InsertRawStreamAsync لإدراج البيانات مباشرةً من تدفقات الملفات أو الذاكرة بتنسيقات مثل CSV وJSON وParquet، أو بأي تنسيق ClickHouse مدعوم.

إدراج من ملف CSV:

await using var fileStream = File.OpenRead("data.csv");

using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: fileStream,
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);

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

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

ADO.NET

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

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

أنشئ الاتصالات دائمًا عبر ClickHouseDataSource لضمان الإدارة السليمة لدورة الحياة وتجميع الاتصالات. يدير DataSource مثيل ClickHouseClient واحدًا داخليًا، وتشترك جميع الاتصالات في تجمّع الاتصالات HTTP الخاص به.

using ClickHouse.Driver.ADO;

// Create DataSource once (register as singleton in DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Create lightweight connections as needed
await using var connection = await dataSource.OpenConnectionAsync();

// Use the connection
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();

في حال استخدام حقن التبعيات:

// In Startup.cs or Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// In your service
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Use connection...
    }
}

استخدام ClickHouseCommand

أنشئ أوامر باستخدام اتصال لتنفيذ SQL:

await using var connection = await dataSource.OpenConnectionAsync();

// Create command with SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Execute and read results
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}

طرق الأوامر:

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

استخدام ClickHouseDataReader

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

await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // Access by column index
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Access by column name
    var email = reader.GetString("email");

    // Generic access
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Check for null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}

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

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

يستخدم ClickHouse.Driver المكوّن System.Net.Http.HttpClient في الخلفية. ويحتوي HttpClient على تجمّع اتصالات لكل endpoint. ونتيجة لذلك:

  • تُمرَّر جلسات قاعدة البيانات عبر اتصالات HTTP التي يديرها تجمّع الاتصالات.
  • يُعيد التجمّع استخدام اتصالات HTTP تلقائيًا.
  • قد تظل الاتصالات مفتوحة حتى بعد التخلّص من الكائنات ClickHouseClient أو ClickHouseConnection.

الأنماط الموصى بها:

السيناريو النهج الموصى به
الاستخدام العام استخدم ClickHouseClient بنمط singleton
ADO.NET / ORMs استخدم ClickHouseDataSource (ينشئ اتصالات تشترك في تجمّع الاتصالات نفسه)
بيئات DI سجّل ClickHouseClient أو ClickHouseDataSource بنمط singleton مع IHttpClientFactory

التعامل مع DateTime

  1. استخدم UTC كلما أمكن. خزّن الطوابع الزمنية في أعمدة DateTime('UTC') واستخدم DateTimeKind.Utc في الشيفرة الخاصة بك. هذا يزيل أي التباس متعلق بالمنطقة الزمنية.

  2. استخدم DateTimeOffset للتعامل الصريح مع المنطقة الزمنية. فهو يمثّل دائمًا لحظة زمنية محددة ويتضمن معلومات الإزاحة.

  3. حدّد المنطقة الزمنية في تلميحات النوع في SQL. عند استخدام المعلمات مع قيم DateTime من النوع Unspecified والموجّهة إلى أعمدة غير UTC، ضمّن المنطقة الزمنية في SQL:

    var parameters = new ClickHouseParameterCollection();
    parameters.AddParameter("dt", myDateTime);
    
    await client.ExecuteNonQueryAsync(
        "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
        parameters
    );

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

تنقل عمليات الإدراج غير المتزامنة مسؤولية التجميع من العميل إلى الخادم. فبدلًا من اشتراط التجميع من جهة العميل، يخزّن الخادم البيانات الواردة مؤقتًا ثم يفرّغها إلى التخزين وفقًا لعتبات قابلة للتهيئة. ويكون هذا مفيدًا في السيناريوهات عالية التزامن، مثل أحمال عمل observability، حيث يرسل العديد من الوكلاء حمولات صغيرة.

فعِّل عمليات الإدراج غير المتزامنة عبر CustomSettings أو سلسلة الاتصال:

// Using CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recommended: wait for flush acknowledgment

// Or via connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"

وضعان (يحددهما wait_for_async_insert):

الوضع السلوك حالة الاستخدام
wait_for_async_insert=1 تعود عملية الإدراج بعد كتابة البيانات إلى القرص. وتُعاد الأخطاء إلى العميل. موصى به لمعظم أحمال العمل
wait_for_async_insert=0 تعود عملية الإدراج فورًا عند تخزين البيانات مؤقتًا. لا يوجد ما يضمن حفظ البيانات. فقط عندما يكون فقدان البيانات مقبولًا

الإعدادات الأساسية:

الإعداد الوصف
async_insert_max_data_size فرّغ عند وصول المخزن المؤقت إلى هذا الحجم (بايت)
async_insert_busy_timeout_ms فرّغ بعد انقضاء هذه المهلة (مللي ثانية)
async_insert_max_query_number فرّغ بعد تراكم هذا العدد من الاستعلامات

الجلسات

لا تُفعِّل الجلسات إلا عند الحاجة إلى ميزات من جهة الخادم تحتفظ بالحالة، مثل:

  • الجداول المؤقتة (CREATE TEMPORARY TABLE)
  • الحفاظ على سياق الاستعلام عبر عدة تعليمات
  • إعدادات على مستوى الجلسة (SET max_threads = 4)

عند تفعيل الجلسات، تُعالَج الطلبات تسلسليًا لمنع الاستخدام المتزامن للجلسة نفسها. ويضيف ذلك حملًا إضافيًا إلى أحمال العمل التي لا تتطلب حالة الجلسة.

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Optional -- will be auto-generated if not provided
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);

استخدام ADO.NET (للتوافق مع أطر ORM):

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();

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

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

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

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

نوع ClickHouse نوع .NET
Int8 sbyte
UInt8 byte
Int16 short
UInt16 ushort
Int32 int
UInt32 uint
Int64 long
UInt64 ulong
Int128 BigInteger
UInt128 BigInteger
Int256 BigInteger
UInt256 BigInteger

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

نوع ClickHouse نوع .NET
Float32 float
Float64 double
BFloat16 float

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

نوع ClickHouse نوع .NET
Decimal(P, S) decimal / ClickHouseDecimal
Decimal32(S) decimal / ClickHouseDecimal
Decimal64(S) decimal / ClickHouseDecimal
Decimal128(S) decimal / ClickHouseDecimal
Decimal256(S) decimal / ClickHouseDecimal

نوع Boolean

نوع ClickHouse نوع .NET
Bool bool

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

نوع ClickHouse نوع .NET
String string
FixedString(N) string

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

نوع ClickHouse نوع .NET
Date DateTime
Date32 DateTime
DateTime DateTime
DateTime32 DateTime
DateTime64 DateTime
Time TimeSpan
Time64 TimeSpan

يخزّن ClickHouse قيم DateTime وDateTime64 داخليًا على هيئة طوابع زمنية Unix (بالثواني أو بوحدات أصغر من الثانية منذ حقبة Unix). وعلى الرغم من أن التخزين يكون دائمًا بتوقيت UTC، فقد تكون للأعمدة منطقة زمنية مرتبطة بها تؤثر في كيفية عرض القيم وتفسيرها.

عند قراءة قيم DateTime، تُضبط الخاصية DateTime.Kind بناءً على المنطقة الزمنية للعمود:

تعريف العمود قيمة DateTime.Kind المُعادة ملاحظات
DateTime('UTC') Utc منطقة UTC زمنية محددة صراحةً
DateTime('Europe/Amsterdam') Unspecified تُطبَّق الإزاحة
DateTime Unspecified يُحفَظ الوقت المحلي كما هو

بالنسبة إلى الأعمدة التي لا تستخدم UTC، تمثل قيمة DateTime المُعادة الوقت المحلي في تلك المنطقة الزمنية. استخدم ClickHouseDataReader.GetDateTimeOffset() للحصول على DateTimeOffset مع الإزاحة الصحيحة لتلك المنطقة الزمنية:

var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)

بالنسبة إلى الأعمدة التي لا تتضمن منطقة زمنية صريحة (أي DateTime بدلًا من DateTime('Europe/Amsterdam'))، يعيد برنامج التشغيل قيمة DateTime مع Kind=Unspecified. وهذا يحافظ على الوقت المحلي كما تظهره الساعة تمامًا كما هو مخزّن، من دون افتراض أي منطقة زمنية.

إذا كنت بحاجة إلى سلوكٍ مدركٍ للمنطقة الزمنية للأعمدة التي لا تتضمن مناطق زمنية صريحة، فإما أن:

  1. تستخدم مناطق زمنية صريحة في تعريفات الأعمدة: DateTime('UTC') أو DateTime('Europe/Amsterdam')
  2. تطبّق المنطقة الزمنية بنفسك بعد القراءة.

نوع JSON

نوع ClickHouse نوع .NET ملاحظات
Json JsonObject الافتراضي (JsonReadMode=Binary)
Json string عند استخدام JsonReadMode=String

يُحدَّد نوع الإرجاع لأعمدة JSON من خلال الإعداد JsonReadMode:

  • Binary (الافتراضي): يعيد System.Text.Json.Nodes.JsonObject. يوفّر وصولًا منظّمًا إلى بيانات JSON، لكن أنواع ClickHouse المتخصصة (مثل عناوين IP وUUIDs والقيم العشرية الكبيرة) تُحوَّل إلى تمثيلاتها النصية داخل بنية JSON.

  • String: يعيد JSON الخام كسلسلة string. ويحافظ على تمثيل JSON كما هو تمامًا من ClickHouse، وهو ما يكون مفيدًا عندما تحتاج إلى تمرير JSON كما هو دون تحليله، أو عندما تريد التعامل مع فك التسلسل بنفسك.

// Configure string mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Or via connection string
// "Host=localhost;JsonReadMode=String"

أنواع أخرى

نوع ClickHouse نوع .NET
UUID Guid
IPv4 IPAddress
IPv6 IPAddress
Nothing DBNull
Dynamic انظر الملاحظة
Array(T) T[] (تُقرأ Array(Array(T)) المتداخلة على أنها T[][] غير منتظمة؛ استخدم reader.GetFieldValue<T[,]>(ordinal) لمادية البيانات المستطيلة كمصفوفة CLR متعددة الأبعاد)
Tuple(T1, T2, …) Tuple<T1, T2, ...> / LargeTuple
Map(K, V) Dictionary<K, V>
Nullable(T) T?
Enum8 string
Enum16 string
LowCardinality(T) نفس T
SimpleAggregateFunction نفس النوع الأساسي
Nested(…) Tuple[]
Variant(T1, T2, …) انظر الملاحظة
QBit(T, dimension) T[]

أنواع Geometry

نوع ClickHouse نوع .NET
Point Tuple<double, double>
Ring Tuple<double, double>[]
LineString Tuple<double, double>[]
Polygon Ring[]
MultiLineString LineString[]
MultiPolygon Polygon[]
Geometry راجع الملاحظة

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

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

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

نوع ClickHouse أنواع .NET المقبولة ملاحظات
Int8 sbyte، وأي نوع متوافق مع Convert.ToSByte()
UInt8 byte، وأي نوع متوافق مع Convert.ToByte()
Int16 short، وأي نوع متوافق مع Convert.ToInt16()
UInt16 ushort، وأي نوع متوافق مع Convert.ToUInt16()
Int32 int، وأي نوع متوافق مع Convert.ToInt32()
UInt32 uint، وأي نوع متوافق مع Convert.ToUInt32()
Int64 long، وأي نوع متوافق مع Convert.ToInt64()
UInt64 ulong، وأي نوع متوافق مع Convert.ToUInt64()
Int128 BigInteger، decimal، double، float، int، uint، long، ulong، وأي نوع متوافق مع Convert.ToInt64()
UInt128 BigInteger، decimal، double، float، int، uint، long، ulong، وأي نوع متوافق مع Convert.ToInt64()
Int256 BigInteger، decimal، double، float، int، uint، long، ulong، وأي نوع متوافق مع Convert.ToInt64()
UInt256 BigInteger، decimal، double، float، int، uint، long، ulong، وأي نوع متوافق مع Convert.ToInt64()

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

نوع ClickHouse أنواع .NET المقبولة ملاحظات
Float32 float، وأي نوع متوافق مع Convert.ToSingle()
Float64 double، وأي نوع متوافق مع Convert.ToDouble()
BFloat16 float، وأي نوع متوافق مع Convert.ToSingle() يُقتطع إلى تنسيق bfloat16 ‏(brain float)‏ ذي 16 بت

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

نوع ClickHouse أنواع .NET المقبولة ملاحظات
Bool bool

أنواع String

نوع ClickHouse أنواع .NET المقبولة ملاحظات
String string, byte[], ReadOnlyMemory<byte>, Stream تُكتب الأنواع الثنائية مباشرةً؛ ويمكن أن تكون التدفقات قابلةً لـ seek أو غير قابلة له
FixedString(N) string, byte[], ReadOnlyMemory<byte>, Stream يُرمَّز String بترميز UTF-8 ويُحشّى؛ ويجب أن تكون الأنواع الثنائية بطول N بايتات بالضبط

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

نوع ClickHouse أنواع .NET المقبولة ملاحظات
Date DateTime, DateTimeOffset, DateOnly, أنواع NodaTime تُحوَّل إلى أيام Unix بصيغة UInt16؛ والنطاق المدعوم هو [1970-01-01, 2149-06-06]
Date32 DateTime, DateTimeOffset, DateOnly, أنواع NodaTime تُحوَّل إلى أيام Unix بصيغة Int32؛ والنطاق المدعوم هو [1900-01-01, 2299-12-31]
DateTime DateTime, DateTimeOffset, DateOnly, أنواع NodaTime انظر أدناه للاطّلاع على التفاصيل؛ والنطاق المدعوم هو [1970-01-01, 2106-02-07 06:28:15] UTC
DateTime32 DateTime, DateTimeOffset, DateOnly, أنواع NodaTime مثل DateTime
DateTime64 DateTime, DateTimeOffset, DateOnly, أنواع NodaTime تعتمد الدقة على معلمة Scale
Time TimeSpan, int تُقيَّد إلى ±999:59:59؛ ويُتعامل مع int على أنه ثوانٍ
Time64 TimeSpan, decimal, double, float, int, long, string تُحلَّل السلسلة النصية بالصيغة [-]HHH:MM:SS[.fraction]؛ وتُقيَّد إلى ±999:59:59.999999999

يراعي برنامج التشغيل DateTime.Kind عند كتابة القيم:

DateTime.Kind معلمات HTTP Bulk
Utc تُحفَظ اللحظة الزمنية كما هي تُحفَظ اللحظة الزمنية كما هي
Local تُحفَظ اللحظة الزمنية كما هي تُحفَظ اللحظة الزمنية كما هي
Unspecified يُتعامل معه كوقت محلي في المنطقة الزمنية لنوع المعلمة (UTC افتراضيًا) يُتعامل معه كوقت محلي في المنطقة الزمنية للعمود

تحافظ قيم DateTimeOffset دائمًا على اللحظة الزمنية الدقيقة.

مثال: DateTime بتوقيت UTC (تُحفَظ اللحظة الزمنية كما هي)

var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC

مثال: DateTime غير محدد (التوقيت المحلي)

var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30

التوصية: للحصول على أبسط سلوك وأكثره قابلية للتنبؤ، استخدم DateTimeKind.Utc أو DateTimeOffset في جميع عمليات DateTime. يضمن ذلك أن تعمل شيفرتك بشكل متسق بغض النظر عن المنطقة الزمنية للخادم أو العميل أو العمود.

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

يوجد فرق مهم بين ربط معلمات HTTP وBulk Copy عند كتابة قيم DateTime من النوع Unspecified:

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

HTTP Parameters لا تعرف تلقائيًا المنطقة الزمنية للعمود. يجب تحديدها في تلميح نوع SQL:

// CORRECT: Timezone in SQL type hint - type is extracted automatically
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECT: Without timezone hint, interpreted as UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// String value "2024-01-15 14:30:00" interpreted as UTC, not Amsterdam time!
DateTime.Kind العمود المستهدف معلمة HTTP (مع تلميح المنطقة الزمنية) معلمة HTTP (من دون تلميح المنطقة الزمنية) النسخ المجمّع
Utc UTC تبقى اللحظة الزمنية كما هي تبقى اللحظة الزمنية كما هي تبقى اللحظة الزمنية كما هي
Utc Europe/Amsterdam تبقى اللحظة الزمنية كما هي تبقى اللحظة الزمنية كما هي تبقى اللحظة الزمنية كما هي
Local أيّ قيمة تبقى اللحظة الزمنية كما هي تبقى اللحظة الزمنية كما هي تبقى اللحظة الزمنية كما هي
Unspecified UTC يُعامَل على أنه UTC يُعامَل على أنه UTC يُعامَل على أنه UTC
Unspecified Europe/Amsterdam يُعامَل على أنه بتوقيت Amsterdam يُعامَل على أنه UTC يُعامَل على أنه بتوقيت Amsterdam

أنواع Decimal

نوع ClickHouse أنواع .NET المقبولة ملاحظات
Decimal(P,S) decimal، ClickHouseDecimal، وأي نوع متوافق مع Convert.ToDecimal() يتم طرح OverflowException إذا تجاوزت القيمة الدقة
Decimal32 decimal، ClickHouseDecimal، وأي نوع متوافق مع Convert.ToDecimal() الحد الأقصى للدقة هو 9
Decimal64 decimal، ClickHouseDecimal، وأي نوع متوافق مع Convert.ToDecimal() الحد الأقصى للدقة هو 18
Decimal128 decimal، ClickHouseDecimal، وأي نوع متوافق مع Convert.ToDecimal() الحد الأقصى للدقة هو 38
Decimal256 decimal، ClickHouseDecimal، وأي نوع متوافق مع Convert.ToDecimal() الحد الأقصى للدقة هو 76

نوع JSON

ClickHouse Type أنواع .NET المقبولة ملاحظات
Json string, JsonObject, JsonNode, أي كائن يعتمد السلوك على إعداد JsonWriteMode

يتحكم إعداد JsonWriteMode في السلوك عند كتابة JSON:

نوع الإدخال JsonWriteMode.String (الافتراضي) JsonWriteMode.Binary
string يُمرَّر كما هو يطرح ArgumentException
JsonObject يُسلسَل عبر ToJsonString() يطرح ArgumentException
JsonNode يُسلسَل عبر ToJsonString() يطرح ArgumentException
POCO مسجّل يُسلسَل عبر JsonSerializer.Serialize() ترميز ثنائي مع دعم تلميحات النوع وسمات المسار المخصصة
POCO غير مسجّل / كائن مجهول يُسلسَل عبر JsonSerializer.Serialize() يطرح ClickHouseJsonSerializationException
  • String (الافتراضي): يقبل string وJsonObject وJsonNode أو أي كائن. تُسلسَل جميع المدخلات عبر System.Text.Json.JsonSerializer وتُرسل كسلاسل JSON لتحليلها على جهة الخادم. هذا هو الوضع الأكثر مرونة ويعمل من دون تسجيل النوع.

  • Binary: لا يقبل إلا أنواع POCO المسجّلة. تُحوَّل البيانات إلى تنسيق JSON الثنائي الخاص بـ ClickHouse على جهة العميل مع دعم كامل لتلميحات النوع. ويتطلب استدعاء connection.RegisterJsonSerializationType<T>() قبل الاستخدام. وتؤدي كتابة قيم string أو JsonNode في هذا الوضع إلى طرح ArgumentException.

// Default String mode works with any input
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary mode requires explicit opt-in and type registration
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
أعمدة JSON محددة الأنواع

عندما يحتوي عمود JSON على تلميحات للأنواع (مثل JSON(id UInt64, price Decimal128(2)))، يستخدم برنامج التشغيل هذه التلميحات لتسلسل القيم مع الحفاظ الكامل على سلامة الأنواع. وهذا يحافظ على الدقة في أنواع مثل UInt64 وDecimal وUUID وDateTime64، والتي قد تفقد دقتها لولا ذلك عند تسلسلها بصيغة JSON عامة.

تسلسل كائنات POCO

يمكن كتابة كائنات 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.

CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
using ClickHouse.Driver.Json;

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // Not serialized
}

// For Binary mode: Register the type and enable Binary mode
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insert POCO - serialized to JSON with nested structure via custom path attributes
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Resulting JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}

مطابقة أسماء الخصائص مع تلميحات نوع العمود حسّاسة لحالة الأحرف. فالخاصية 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).

أنواع أخرى

نوع ClickHouse أنواع .NET المقبولة ملاحظات
UUID Guid, string تُحلَّل السلسلة النصية باعتبارها Guid
IPv4 IPAddress, string يجب أن يكون IPv4؛ وتُحلَّل السلسلة النصية باستخدام IPAddress.Parse()
IPv6 IPAddress, string يجب أن يكون IPv6؛ وتُحلَّل السلسلة النصية باستخدام IPAddress.Parse()
Nothing أيّ نوع لا يكتب شيئًا (no-op)
Dynamic غير مدعوم (throws NotImplementedException)
Array(T) IList, null تؤدي القيمة null إلى كتابة مصفوفة فارغة. بالنسبة إلى الأنواع المتداخلة (Array(Array(T)) وما هو أعمق)، يُقبل كلٌّ من الأشكال المتعرجة (T[][], List<List<T>>) ومصفوفات CLR المستطيلة متعددة الأبعاد (T[,], T[,,], …)؛ ويجب أن تتطابق رتبة CLR مع عمق التداخل في ClickHouse.
Tuple(T1, T2, …) ITuple, IList يجب أن يتطابق عدد العناصر مع رتبة Tuple. راجع تحذير ValueTuple للعناصر التي يزيد عددها على 7.
Map(K, V) IDictionary
Nullable(T) null, DBNull, أو الأنواع المقبولة لـ T يكتب بايت علامة null قبل القيمة
Enum8 string, sbyte, أنواع رقمية يُبحث عن السلسلة النصية في قاموس enum
Enum16 string, short, أنواع رقمية يُبحث عن السلسلة النصية في قاموس enum
LowCardinality(T) الأنواع المقبولة لـ T يُفوَّض إلى النوع الأساسي
SimpleAggregateFunction الأنواع المقبولة للنوع الأساسي يُفوَّض إلى النوع الأساسي
Nested(…) IList من tuples يجب أن يتطابق عدد العناصر مع عدد الحقول
Variant(T1, T2, …) قيمة تطابق أحد T1 أو T2 أو … Throws ArgumentException إذا لم يطابق أي نوع
QBit(T, dim) IList يُفوَّض إلى Array؛ والبُعد بيانات وصفية فقط

أنواع Geometry

نوع ClickHouse أنواع .NET المقبولة ملاحظات
Point System.Drawing.Point, ITuple, IList (عنصران)
Ring IList من عناصر Point
LineString IList من عناصر Point
Polygon IList من عناصر Ring
MultiLineString IList من عناصر LineString
MultiPolygon IList من عناصر Polygon
Geometry أي نوع من أنواع Geometry أعلاه Variant لجميع أنواع Geometry

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

نوع ClickHouse ملاحظات
Dynamic يؤدي إلى إطلاق NotImplementedException
AggregateFunction يؤدي إلى إطلاق AggregateFunctionException

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

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

CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

await client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);

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

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

البدء السريع

using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Information);
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

استخدام appsettings.json

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

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

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

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

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

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

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

الفئة المصدر أبرز الجوانب
ClickHouse.Driver.Connection ClickHouseConnection دورة حياة الاتصال، واختيار HTTP client factory، وفتح الاتصال وإغلاقه، وإدارة الجلسات.
ClickHouse.Driver.Command ClickHouseCommand بدء تنفيذ الاستعلام واكتماله، والتوقيت، ومعرّفات الاستعلام، وإحصاءات الخادم، وتفاصيل الخطأ.
ClickHouse.Driver.Transport ClickHouseConnection طلبات HTTP streaming منخفضة المستوى، وعلامات الضغط، ورموز حالة الاستجابة، وإخفاقات النقل.
ClickHouse.Driver.Client ClickHouseClient عمليات insert الثنائية، والاستعلامات، والعمليات الأخرى
ClickHouse.Driver.NetTrace TraceHelper تتبّع الشبكة، فقط عند تمكين وضع التصحيح

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

{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}

سيُسجِّل هذا ما يلي:

  • اختيار 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 في بيئة الإنتاج.

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Must be Trace level to see network events
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Enable low-level network tracing
};

OpenTelemetry

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

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

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

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Subscribe to ClickHouse driver spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Or AddJaegerExporter(), etc.

لتطبيقات سطر الأوامر، أو للاختبار، أو للإعداد اليدوي:

using OpenTelemetry;
using OpenTelemetry.Trace;

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)
    .AddConsoleExporter()
    .Build();

سمات span

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

السمة الوصف
db.system تكون دائمًا "clickhouse"
db.name اسم قاعدة البيانات
db.user اسم المستخدم
db.statement SQL query (إذا كان enabled)
db.clickhouse.read_rows الصفوف التي قرأها الـ query
db.clickhouse.read_bytes البايتات التي قرأها الـ query
db.clickhouse.written_rows الصفوف التي كتبها الـ query
db.clickhouse.written_bytes البايتات التي كتبها الـ query
db.clickhouse.elapsed_ns execution time على جهة الخادم بالنانوثانية

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

تحكَّم في سلوك التتبّع باستخدام ClickHouseDiagnosticsOptions:

using ClickHouse.Driver.Diagnostic;

// Include SQL statements in spans (default: false for security)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncate long SQL statements (default: 1000 characters)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;

إعدادات TLS

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

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

بالنسبة إلى بيئات الإنتاج التي تتطلب منطقًا مخصصًا للتحقق من الشهادات، وفّر HttpClient خاصًا بك مع معالج ServerCertificateCustomValidationCallback مُعدّ:

using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // Required when compression is enabled (default)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Example: Accept a specific certificate thumbprint
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Example: Accept certificates from a specific issuer
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Default: Use standard validation
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);

دعم ORM

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

// Register DataSource as singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Create connections for ORM use
await using var connection = await dataSource.OpenConnectionAsync();
// Pass connection to your ORM...

Dapper

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

استخدم ClickHouseDataSource لإدارة مدة بقاء الاتصال على نحو صحيح:

var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Register as singleton in DI

using var connection = dataSource.CreateConnection();

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

جميع أنماط مَعلمات Dapper القياسية مدعومة:

الكائنات المجهولة:

await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)",
    new { Id = 1, Name = "alice", Balance = 3.14 });

فئات POCO:

class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

var param = new InsertParams { Id = 42, Name = "bob", Balance = 99.9 };
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)", param);

القاموس:

var parameters = new Dictionary<string, object> { { "Id", 2 } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", parameters);

DynamicParameters (من قاموس أو كائن مجهول الاسم):

var dynParams = new DynamicParameters(new { Id = 1 });
// or: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", dynParams);

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

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

class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// From a table
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// From a literal
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();

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

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

var parameters = new Dictionary<string, object> { { "value", 42 } };
var result = await connection.QueryAsync<int>("SELECT {value:Int32}", parameters);

WHERE IN

تعمل ميزة توسيع IN المدمجة في Dapper:

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id IN @Ids ORDER BY id",
    new { Ids = new[] { 1, 3, 5 } });

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

كما تعمل الدالة has() في ClickHouse أيضًا مع معلمة من النوع Array:

var parameters = new Dictionary<string, object> { { "ids", new[] { 1, 3, 5 } } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE has({ids:Array(Int32)}, id) ORDER BY id",
    parameters);

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

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

// ClickHouseDecimal (for Decimal64/128/256 columns)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (for Int128/Int256/UInt128/UInt256 columns)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (for IPv4/IPv6 columns)
SqlMapper.AddTypeHandler(new IpAddressHandler());

راجع مثال Dapper للاطلاع على مثال لتنفيذ معالج أنواع.

Dapper.Contrib

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

[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

var all = await connection.GetAllAsync<UserRecord>();
var one = await connection.GetAsync<UserRecord>(1);

يجب أن تتطابق أسماء الخصائص تمامًا مع أسماء أعمدة ClickHouse (تراعي حالة الأحرف).

القيود

العنصر الحالة التفاصيل
Tuple كـ نتيجة يعمل يتطلب تسجيل SqlMapper.TypeHandler<ITuple>
Tuple كـ معلمة غير مدعوم لا يستطيع Dapper إجراء تسلسل لـ ITuple/Tuple<> كقيمة DbParameter
أنواع Nested كمعلمة غير مدعوم للسبب نفسه — يرفض Dapper الأنواع المعقدة كقيم للمعلمات
أنواع Geo كمعلمة غير مدعوم Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon
Dapper.Contrib.Insert<T>() غير مدعوم يُنشئ صياغة خاصة بـ SQL Server
النوع Nothing غير مدعوم لا يوجد له تمثيل ذو معنى في .NET

Linq2db

يتوافق برنامج التشغيل هذا مع linq2db، وهو ORM خفيف الوزن وموفّر LINQ لـ .NET. راجع موقع المشروع للاطلاع على وثائق تفصيلية.

مثال على الاستخدام:

أنشئ DataConnection باستخدام موفّر ClickHouse:

using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);

يمكن تحديد تعيينات الجداول باستخدام السمات أو التهيئة بأسلوب Fluent. إذا كانت أسماء الفئة والخاصية لديك تطابق تمامًا أسماء الجدول والعمود، فلا حاجة إلى أي تهيئة:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

الاستعلام عن:

await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();

النسخ المجمّع:

استخدم BulkCopyAsync لإجراء عمليات إدراج مجمّعة بكفاءة.

await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);

Entity Framework Core

موفّر Entity Framework Core الرسمي لـ ClickHouse. اربط فئات C# بجداول ClickHouse، ونفّذ الاستعلامات باستخدام LINQ، وأدرِج البيانات عبر SaveChanges — وكل ذلك باستخدام أنماط EF Core المألوفة.

التثبيت

dotnet add package ClickHouse.EntityFrameworkCore

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

البدء السريع

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

using Microsoft.EntityFrameworkCore;

public class PageView
{
    public long Id { get; set; }
    public string Path { get; set; }
    public DateOnly Date { get; set; }
    public string UserAgent { get; set; }
}

public class AnalyticsContext : DbContext
{
    public DbSet<PageView> PageViews { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseClickHouse("Host=localhost;Database=analytics");
}

// Query
await using var ctx = new AnalyticsContext();

var topPages = await ctx.PageViews
    .Where(v => v.Date >= new DateOnly(2024, 1, 1))
    .GroupBy(v => v.Path)
    .Select(g => new { Path = g.Key, Views = g.Count() })
    .OrderByDescending(x => x.Views)
    .Take(10)
    .ToListAsync();

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

الفئة أنواع ClickHouse أنواع CLR
الأعداد الصحيحة Int8Int64, UInt8UInt64 sbyte, short, int, long, byte, ushort, uint, ulong
الأعداد الصحيحة الكبيرة Int128, Int256, UInt128, UInt256 BigInteger
الأعداد ذات الفاصلة العائمة Float32, Float64, BFloat16 float, double
الأعداد العشرية Decimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S) decimal أو ClickHouseDecimal
Bool Bool bool
السلاسل النصية String, FixedString(N) string
التعدادات Enum8(...), Enum16(...) string أو enum في C#
التاريخ/الوقت Date, Date32, DateTime, DateTime64(P, 'TZ') DateOnly, DateTime
الوقت Time, Time64(N) TimeSpan
UUID UUID Guid
الشبكة IPv4, IPv6 IPAddress
المصفوفات Array(T) T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
الخرائط Map(K, V) Dictionary<K,V>
Tuples Tuple(T1, ...) Tuple<...> أو ValueTuple<...>
Variant Variant(T1, T2, ...) object
Dynamic Dynamic object
JSON Json JsonNode أو string
الأنواع الجغرافية Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon, Geometry Tuple<double,double> ومصفوفات منها؛ وobject لـ Geometry
المغلِّفات Nullable(T), LowCardinality(T) تُفك تلقائيًا

استخدم 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 — بما يشمل الدوال الحسابية واللوغاريتمية والمثلثية ودوال الأدوات المساعدة.

دلالات NULL في LEFT JOIN

يحقن الموفّر set_join_use_nulls=1 تلقائيًا في كل مسار اتصال لمواءمة توقّعات Entity Framework بشأن سلوك JOIN.

إذا كان خادم ClickHouse أو ملف التعريف لديك يمنع تغيير هذا الإعداد (على سبيل المثال، ملف تعريف بقيمة readonly=1)، فأوقِف هذا السلوك باستخدام:

optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());

عند تفعيل خيار إلغاء الاشتراك، يعيد LEFT JOIN القيم الافتراضية لأعمدة ClickHouse، ولا يعود اكتشاف التنقل في EF المعتمد على null يعمل كما هو متوقع. استخدم مقارنات صريحة مع 0 / "" بدلًا من == null.

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

يستخدم SaveChanges واجهة برمجة تطبيقات InsertBinaryAsync الأصلية لبرنامج التشغيل — بترميز RowBinary مع ضغط GZip، وهو أكثر كفاءة بكثير من عبارات SQL ذات المعلمات:

await using var ctx = new AnalyticsContext();

ctx.PageViews.Add(new PageView
{
    Id = 1,
    Path = "/home",
    Date = new DateOnly(2024, 6, 15),
    UserAgent = "Mozilla/5.0"
});

await ctx.SaveChangesAsync();

تنتقل الكيانات من Added إلى Unchanged بعد الحفظ، تمامًا كما يحدث مع أي موفّر آخر لـ EF Core.

يمكن ضبط حجم الدفعة (الافتراضي 1000):

optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));

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

لعمليات التحميل عالية الإنتاجية، استخدم BulkInsertAsync بدلًا من SaveChanges. هذه طريقة امتداد لـ DbContext تتجاوز بالكامل متتبّع التغييرات في EF Core، وآلية حلّ الهوية، وإدارة الحالة — إذ تستدعي مباشرةً InsertBinaryAsync الخاصة ببرنامج التشغيل باستخدام ترميز RowBinary وضغط GZip.

وهذا يجعلها مناسبة لتحميل مجموعات بيانات كبيرة عندما لا تحتاج إلى تتبّع الكيانات بعد الإدراج:

var events = Enumerable.Range(0, 100_000)
    .Select(i => new PageView
    {
        Id = i,
        Path = $"/page/{i}",
        Date = DateOnly.FromDateTime(DateTime.Today)
    });

long rowsInserted = await ctx.BulkInsertAsync(events);

يمكن أن يكون الإدخال أي IEnumerable<T> — إذ يمر عبر الكيانات دون تحميلها جميعًا إلى الذاكرة. قيمة الإرجاع هي عدد الصفوف المُدرجة. لا تُرفَق الكيانات بـ DbContext بعد الإدراج، لذلك لا يحدث انتقال في الحالة من Added إلى Unchanged.

التعدادات

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

public enum Status { Active, Inactive, Pending }

public class User
{
    public long Id { get; set; }
    public Status Status { get; set; }
}

// Query with enum values
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();

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

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

التحويل على مستوى كل خاصية:

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public long Id { get; set; }
    public Money Price { get; set; }
}

// In OnModelCreating:
modelBuilder.Entity<Order>()
    .Property(o => o.Price)
    .HasConversion(
        m => $"{m.Amount}|{m.Currency}",
        s => new Money
        {
            Amount = decimal.Parse(s.Split('|')[0]),
            Currency = s.Split('|')[1]
        })
    .HasColumnType("String");

فئة مُحوِّل قابلة لإعادة الاستخدام:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter() : base(
        m => $"{m.Amount}|{m.Currency}",
        s => Parse(s)) { }

    private static Money Parse(string s)
    {
        var parts = s.Split('|');
        return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
    }
}

// Apply to a single property:
.HasConversion<MoneyConverter>()

// Or apply to all properties of a type via conventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}

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

بالنسبة إلى الأنواع القياسية مثل string وint وDateTime وغيرها، يستنتج الموفّر نوع ClickHouse تلقائيًا. أمّا الأنواع ذات المعلمات والأغلفة، فيلزم تحديد نوع ClickHouse صراحةً.

استخدام التعليقات التوضيحية للبيانات (السمات):

using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("sensor_readings")]
public class SensorReading
{
    public long Id { get; set; }

    [Column(TypeName = "Array(String)")]
    public string[] Tags { get; set; }

    [Column(TypeName = "Map(String, String)")]
    public Dictionary<string, string> Metadata { get; set; }

    [Column(TypeName = "Nullable(Float64)")]
    public double? Value { get; set; }

    [Column(TypeName = "Decimal128(18)")]
    public decimal HighPrecision { get; set; }
}

استخدام أسلوب fluent API في OnModelCreating:

modelBuilder.Entity<SensorReading>(e =>
{
    e.ToTable("sensor_readings");
    e.Property(x => x.Tags).HasColumnType("Array(String)");
    e.Property(x => x.Metadata).HasColumnType("Map(String, String)");
    e.Property(x => x.Value).HasColumnType("Nullable(Float64)");
    e.Property(x => x.Category).HasColumnType("LowCardinality(String)");
    e.Property(x => x.HighPrecision).HasColumnType("Decimal128(18)");
});

الأغلفة المتداخلة مثل Array(Nullable(Int32)) وLowCardinality(Nullable(String)) مدعومة — إذ يفكّ الموفّر تغليف Nullable وLowCardinality تلقائيًا عند كل مستوى من مستويات التداخل.

أعمدة Variant وDynamic

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

public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// or:
entity.Property(e => e.Payload).HasColumnType("Dynamic");

عند القراءة، يُفك تسلسل القيمة تلقائيًا إلى نوع .NET الموافق للمميِّز المخزَّن (مثل string وulong وulong[]).

أعمدة JSON

يدعم المزوّد نوع العمود Json في ClickHouse، ويعيّنه إلى System.Text.Json.Nodes.JsonNode (بشكل أساسي) أو string (عبر ValueConverter تلقائي):

using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Data).HasColumnType("Json");

تتم قراءة JSON وكتابته من خلال كلٍّ من SaveChanges وBulkInsertAsync:

ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"

إذا كنت تفضّل سلاسل JSON الخام، فاضبط الخاصية لتكون string مع نوع عمود Json — يطبّق الموفّر ValueConverter تلقائيًا:

public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // raw JSON string
}

entity.Property(e => e.Data).HasColumnType("Json");

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

اضبط محركات جداول ClickHouse والعبارات الخاصة بكل محرك عبر واجهة ToTable(name, t => ...) بأسلوب الاستدعاءات المتسلسلة. عند عدم ضبط أي محرك، يستخدم الموفّر MergeTree افتراضيًا، مع استمداد ORDER BY من المفتاح الأساسي للكيان.

modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});

عائلات المحركات المدعومة:

المحرّك الأسلوب المتسلسل ملاحظات
MergeTree HasMergeTreeEngine() الافتراضي عند عدم تكوين أي شيء
ReplacingMergeTree HasReplacingMergeTreeEngine("Version", "IsDeleted") or HasReplacingMergeTreeEngine<T>(e => e.Version) عمودا Version وIsDeleted اختياريان
SummingMergeTree HasSummingMergeTreeEngine(…) or HasSummingMergeTreeEngine<T>(e => new { … }) أعمدة الجمع اختيارية
AggregatingMergeTree HasAggregatingMergeTreeEngine()
CollapsingMergeTree HasCollapsingMergeTreeEngine("Sign") or HasCollapsingMergeTreeEngine<T>(e => e.Sign) يجب أن يكون العمود Sign من النوع Int8
VersionedCollapsingMergeTree HasVersionedCollapsingMergeTreeEngine("Sign", "Version") or <T>(e => e.Sign, e => e.Version)
GraphiteMergeTree HasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, Memory HasLogEngine(), HasTinyLogEngine(), HasStripeLogEngine(), HasMemoryEngine() بدون ORDER BY / PARTITION BY

بنود المحرّك: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. ترتبط جميعها بمنشئ المحرّك المُعاد من HasXxxEngine().

ميزات على مستوى العمود: HasCodec, HasTtl, HasComment, HasDefault — جميعها تدخل في عمليات الترحيل.

فهارس تخطي البيانات — عبر HasIndex(...).HasSkippingIndexType(...):

modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// Index with parameters (e.g. bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);

يُتجاهَل الفهرس القياسي (غير القائم على تخطي البيانات) بصمت، إذ لا يوجد في ClickHouse ما يقابله. أما الفهارس الفريدة فتؤدي إلى ظهور استثناء، لأن ClickHouse لا يفرض التفرد.

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

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

dotnet ef migrations add InitialCreate
dotnet ef database update

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

العملية الناتج
CREATE TABLE يتضمن عبارة engine، وORDER BY، وPARTITION BY، وSETTINGS، ومرمّزات الأعمدة/TTL/التعليقات/القيم الافتراضية
ALTER TABLE ADD COLUMN
ALTER TABLE DROP COLUMN
ALTER TABLE MODIFY COLUMN يعالج تغيير النوع، بالإضافة إلى إضافة/إزالة السمات (CODEC, TTL, COMMENT, DEFAULT)
ALTER TABLE RENAME COLUMN
RENAME TABLE
ALTER TABLE ADD INDEX / DROP INDEX فهارس تخطي البيانات فقط
CREATE DATABASE / DROP DATABASE عبر EnsureCreated / EnsureDeleted وعمليات الترحيل

قيود الترحيل

الميزة السبب
المفاتيح الخارجية لا يفرض ClickHouse المفاتيح الخارجية. ترفض عمليات الترحيل AddForeignKey، ويُصدر مدقق النموذج تحذيرًا عند بناء النموذج.
القيود الفريدة / الفهارس الفريدة لا يفرض ClickHouse التفرد. وتؤدي الفهارس الفريدة إلى إطلاق استثناء أثناء الترحيل.
القيم التي ينشئها الخادم (الزيادة التلقائية / IDENTITY) لا يوجد ما يقابلها في ClickHouse.
أعمدة Nested(…) غير مدعومة بعد كنوع CLR مُعيَّن.
الكيانات المملوكة بصيغة JSON (.ToJson()) لم يُنفَّذ بعد التعيين البنيوي لـ JSON للكيانات المملوكة. استخدم JsonNode / string في عمود Json بدلًا من ذلك (راجع أعمدة JSON).

وبالإضافة إلى عمليات الترحيل، لا يزال الموفّر لا يدعم أيضًا ما يلي:

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

القيود

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

تستخدم أنواع C# ValueTuple التي تضم أكثر من 7 عناصر نمط تداخل يُنشئه المصرّف: إذ تكون الوسيطة العامة الثامنة (TRest) هي نفسها ValueTuple تحتوي على العناصر المتبقية. على سبيل المثال، يُصرَّف (int, int, int, int, int, int, int, string, string) إلى ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.

يؤدي ذلك إلى التباس عندما يكون عمود ClickHouse عبارة عن Tuple من 8 عناصر ويكون العنصر الأخير فيه هو نفسه Tuple — مثل Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). لا يستطيع برنامج التشغيل التمييز بين:

  • Tuple مسطحة من 9 عناصر (تداخل TRest الذي يُنشئه المصرّف)
  • Tuple من 8 عناصر يكون عنصرها الأخير Tuple(String, String) متداخلة

كلتاهما تنتجان نوع .NET نفسه: ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.

يتعامل برنامج التشغيل مع الوسيطة الثامنة على أنها TRest (أي يُسطّحها)، ما يعني أن حالة Tuple ذات 8 عناصر مع Tuple متداخلة ستُسلسَل بصورة غير صحيحة.

ينطبق ذلك على كلٍّ من System.Tuple وValueTuple لأن كليهما يستخدم تداخل TRest عند وجود أكثر من 7 عناصر. أما Tuples التي تضم 7 عناصر أو أقل، أو التي لا يكون عنصرها الأخير نفسه Tuple، فلا تتأثر.

الحل البديل: لفّ Tuple الداخلية بطبقة إضافية حتى يتمكن برنامج التشغيل من تمييزها عن تداخل TRest:

// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))

أعمدة AggregateFunction

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

للإدراج:

INSERT INTO t VALUES (uniqState(1));

للاختيار:

SELECT uniqMerge(c) FROM t;

Navigation