Перейти к основному содержанию
Официальный клиент C# для подключения к ClickHouse. Исходный код клиента доступен в репозитории GitHub. Изначально разработан Oleg V. Kozlyuk. Библиотека предоставляет два основных API:
  • ClickHouseClient (рекомендуется): высокоуровневый потокобезопасный клиент, предназначенный для использования в качестве singleton. Предоставляет простой асинхронный API для запросов и массовых вставок. Лучше всего подходит для большинства приложений.
  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): стандартные абстракции базы данных в .NET. Требуются для интеграции с ORM (Dapper, Linq2db) и в случаях, когда нужна совместимость с ADO.NET. ClickHouseBulkCopy — вспомогательный класс для эффективной вставки данных с использованием ADO.NET-соединения. ClickHouseBulkCopy устарел и будет удалён в одном из будущих релизов; вместо него используйте ClickHouseClient.InsertBinaryAsync.
Оба API используют один и тот же базовый пул 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

Установка

Установите пакет из NuGet:
dotnet add package ClickHouse.Driver
Или через диспетчер пакетов NuGet:
Install-Package ClickHouse.Driver

Быстрый старт

using ClickHouse.Driver;

// Создание клиента (обычно как singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Выполнение запроса
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);

Конфигурация

Существует два способа настроить подключение к ClickHouse:
  • Строка подключения: пары ключ/значение, разделённые точкой с запятой, которые задают хост, учётные данные для аутентификации и другие параметры подключения.
  • Объект ClickHouseClientSettings: строго типизированный объект конфигурации, который можно загрузить из файлов конфигурации или задать в коде.
Ниже приведён полный список всех настроек, их значений по умолчанию и того, как они влияют на работу.

Настройки подключения

СвойствоТипПо умолчаниюКлюч строки подключенияОписание
Hoststring"localhost"HostИмя хоста или IP-адрес сервера ClickHouse
Portushort8123 (HTTP) / 8443 (HTTPS)PortНомер порта; значение по умолчанию зависит от протокола
Usernamestring"default"UsernameИмя пользователя для аутентификации
Passwordstring""PasswordПароль для аутентификации
Databasestring""DatabaseБаза данных по умолчанию; если значение пустое, используются настройки сервера или пользователя по умолчанию
Protocolstring"http"ProtocolПротокол подключения: "http" или "https"
PathstringnullPathURL-путь для сценариев с использованием обратного прокси (например, /clickhouse)
TimeoutTimeSpan2 минутыTimeoutТайм-аут операции (в строке подключения хранится в секундах)

Формат данных и сериализация

СвойствоТипПо умолчаниюКлюч строки подключенияОписание
UseCompressionbooltrueCompressionВключает gzip-сжатие при передаче данных
UseCustomDecimalsbooltrueUseCustomDecimalsИспользовать ClickHouseDecimal для чисел произвольной точности; если false, используется .NET decimal (предел — 128 бит)
ReadStringsAsByteArraysboolfalseReadStringsAsByteArraysЧитать столбцы String и FixedString как byte[] вместо string; полезно для бинарных данных
UseFormDataParametersboolfalseUseFormDataParametersОтправлять параметры в виде form data, а не в строке запроса URL
ParameterTypeResolverIParameterTypeResolvernullПользовательский резолвер для сопоставления типов параметров в стиле @; см. Пользовательское сопоставление типов параметров
JsonReadModeJsonReadModeBinaryJsonReadModeКак возвращаются данные JSON: Binary (возвращает JsonObject) или String (возвращает сырую строку JSON)
JsonWriteModeJsonWriteModeStringJsonWriteModeКак отправляются данные JSON: String (сериализует через JsonSerializer, принимает любые входные данные) или Binary (только зарегистрированные POCO с подсказками типов)

Управление сеансами

СвойствоТипПо умолчаниюКлюч строки подключенияОписание
UseSessionboolfalseUseSessionВключает сеансы с сохранением состояния; запросы выполняются последовательно
SessionIdstringnullSessionIdИдентификатор сеанса; GUID генерируется автоматически, если null и UseSession имеет значение true
Флаг UseSession включает сохранение сеанса на сервере, что позволяет использовать операторы SET и временные таблицы. Сеансы сбрасываются после 60 секунд бездействия (тайм-аут по умолчанию). Время жизни сеанса можно увеличить, задав настройку сеанса через команды ClickHouse или конфигурацию сервера.Класс ClickHouseConnection обычно поддерживает параллельную работу (несколько потоков могут выполнять запросы одновременно). Однако при включении флага UseSession для одного подключения в любой момент времени будет доступен только один активный запрос (это ограничение на стороне сервера).

Безопасность

СвойствоТипПо умолчаниюКлюч строки подключенияОписание
SkipServerCertificateValidationboolfalseПропустить проверку HTTPS-сертификата; не использовать в продакшне

Конфигурация HTTP-клиента

СвойствоТипПо умолчаниюКлюч строки подключенияОписание
HttpClientHttpClientnullПользовательский предварительно настроенный экземпляр HttpClient
HttpClientFactoryIHttpClientFactorynullПользовательская фабрика для создания экземпляров HttpClient
HttpClientNamestringnullИмя, которое HttpClientFactory использует для создания конкретного клиента

Логирование и отладка

СвойствоТипПо умолчаниюКлюч строки подключенияОписание
LoggerFactoryILoggerFactorynullФабрика логгеров для диагностического логирования
EnableDebugModeboolfalseВключает сетевую трассировку .NET (требуется LoggerFactory с уровнем Trace); существенно влияет на производительность

Пользовательские настройки и роли

PropertyTypeDefaultConnection String KeyDescription
CustomSettingsIDictionary<string, object>Пустопрефикс set_*настройки сервера ClickHouse, см. примечание ниже
RolesIReadOnlyList<string>ПустоRolesРоли ClickHouse, разделённые запятыми (например, Roles=admin,reader)
Если вы задаёте пользовательские настройки через строку подключения, используйте префикс set_, например: “set_max_threads=4”. Если вы используете объект ClickHouseClientSettings, префикс set_ указывать не нужно.Полный список доступных настроек см. здесь.

Примеры строк подключения

Базовое подключение

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 позволяет переопределять настройки уровня клиента для отдельных запросов. Все свойства необязательны и переопределяют значения клиента по умолчанию только если они указаны.
СвойствоТипОписание
QueryIdstringПользовательский идентификатор запроса для отслеживания в system.query_log или отмены
DatabasestringПереопределяет базу данных по умолчанию для этого запроса
RolesIReadOnlyList<string>Переопределяет роли клиента для этого запроса
CustomSettingsIDictionary<string, object>Настройки сервера ClickHouse для этого запроса (например, max_threads)
CustomHeadersIDictionary<string, string>Дополнительные HTTP-заголовки для этого запроса
UseSessionbool?Переопределяет поведение сеанса для этого запроса
SessionIdstringИдентификатор сеанса для этого запроса (требуется UseSession = true)
BearerTokenstringПереопределяет токен аутентификации для этого запроса
ParameterTypeResolverIParameterTypeResolverПереопределяет резолвер уровня клиента для сопоставления типов параметров в стиле @; см. Пользовательское сопоставление типов параметров
MaxExecutionTimeTimeSpan?Тайм-аут запроса на стороне сервера (передаётся как настройка max_execution_time); сервер отменяет запрос при превышении
Пример:
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

InsertOptions дополняет QueryOptions настройками, специфичными для пакетных операций вставки через InsertBinaryAsync.
СвойствоТипПо умолчаниюОписание
BatchSizeint100,000Количество строк в батче
MaxDegreeOfParallelismint1Количество параллельных загрузок батчей
FormatRowBinaryFormatRowBinaryБинарный формат: RowBinary или RowBinaryWithDefaults
ColumnTypesIReadOnlyDictionary<string, string>nullИмя столбца → строка типа ClickHouse. Если задано, запрос для определения схемы пропускается.
UseSchemaCacheboolfalseКэшировать полную схему таблицы для каждой пары (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 };

// Первый вызов получает схему с сервера
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Второй вызов использует кэшированную схему — без лишних обращений к серверу
await client.InsertBinaryAsync("my_table", columns, batch2, options);
  • ColumnTypes имеет приоритет над UseSchemaCache. Если заданы оба параметра, используются явно указанные типы.
  • Кэш схемы не отслеживает изменения, внесённые командой ALTER TABLE. Если вы изменяете схему таблицы, создайте новый ClickHouseClient или не используйте UseSchemaCache для этой таблицы.
  • Кэш привязан к экземпляру ClickHouseClient, а в качестве ключа используются (database, table). Разные подмножества столбцов одной и той же таблицы используют одну общую кэшированную схему.

ClickHouseClient

ClickHouseClient — рекомендуемый API для работы с ClickHouse. Он потокобезопасен, рассчитан на использование как singleton и самостоятельно управляет пулом HTTP-соединений.

Создание клиента

Создайте ClickHouseClient с помощью строки подключения или объекта ClickHouseClientSettings. Доступные параметры см. в разделе Конфигурация. Сведения о вашем сервисе ClickHouse Cloud доступны в консоли ClickHouse Cloud. Выберите сервис и нажмите Connect: Выберите 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:
// В конфигурации DI
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

// Создание клиента с использованием фабрики
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");
ClickHouseClient рассчитан на длительное использование и совместное использование во всём приложении. Создайте его один раз (обычно как singleton) и затем повторно используйте для всех операций с базой данных. Клиент сам управляет пулом HTTP-соединений.

Выполнение запросов

Используйте ExecuteNonQueryAsync для команд, которые не возвращают результатов:
// Создать таблицу
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Удалить таблицу
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
Используйте ExecuteScalarAsync, чтобы получить единственное значение:
var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Количество строк: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Версия сервера: {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 too long”, которые могут возникать при параметризованных запросах.
// Подготовка данных как IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

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

// Базовая вставка
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");
Для больших объёмов данных настройте пакетную обработку и степень параллелизма с помощью InsertOptions:
var options = new InsertOptions
{
    BatchSize = 100_000,           // Строк в батче (по умолчанию: 100 000)
    MaxDegreeOfParallelism = 4     // Параллельная загрузка батчей (по умолчанию: 1)
};
  • Перед вставкой клиент автоматически получает структуру таблицы с помощью SELECT * FROM <table> WHERE 1=0. Передаваемые значения должны соответствовать типам целевых столбцов. Чтобы пропустить этот запрос, используйте InsertOptions.ColumnTypes или InsertOptions.UseSchemaCache.
  • Если MaxDegreeOfParallelism > 1, батчи загружаются параллельно. Сеансы несовместимы с параллельной вставкой; либо отключите сеансы, либо задайте MaxDegreeOfParallelism = 1.
  • Используйте RowBinaryFormat.RowBinaryWithDefaults в InsertOptions.Format, если хотите, чтобы сервер применял значения DEFAULT для столбцов, которые не были переданы.

Вставка POCO

Вместо создания массивов object[] можно напрямую вставлять строго типизированные объекты POCO. Зарегистрируйте тип один раз, а затем передайте IEnumerable<T>:
// Определите POCO, соответствующий столбцам вашей таблицы
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Зарегистрируйте тип (один раз за время жизни клиента)
client.RegisterBinaryInsertType<SensorReading>();

// Вставка напрямую — имена столбцов выводятся из имён свойств
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")]     // Сопоставить со столбцом с другим именем
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Явный тип ClickHouse
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Исключить из вставки
    public string InternalTag { get; set; }
}
АтрибутНазначение
[ClickHouseColumn(Name = "...")]Переопределяет имя целевого столбца
[ClickHouseColumn(Type = "...")]Явно задаёт тип ClickHouse
[ClickHouseNotMapped]Исключает свойство из вставки
Когда все сопоставленные свойства явно задают Type, запрос для определения схемы полностью пропускается. Если явные типы указаны только у части свойств, драйвер возвращается к запросу для определения схемы для полного набора столбцов. InsertBinaryAsync<T> поддерживает те же InsertOptions (батчинг, параллелизм, кэширование схемы), что и перегрузка object[].
В отличие от перегрузки object[], InsertBinaryAsync<T> не принимает явный список столбцов. Столбцы определяются сопоставленными свойствами зарегистрированного типа. Чтобы управлять тем, какие столбцы вставляются, используйте [ClickHouseNotMapped], чтобы исключить свойства, или [ClickHouseColumn(Name = "...")], чтобы переименовать их.Если в InsertOptions задан ColumnTypes, он имеет приоритет над атрибутами POCO.

Эволюция схемы

Вставка POCO работает без проблем, если после регистрации типа в целевую таблицу добавляются новые столбцы. Поскольку драйвер вставляет только те столбцы, которые сопоставлены с POCO, все новые столбцы с DEFAULT (или другими выражениями по умолчанию) сервер заполняет автоматически. Никаких изменений в коде или повторной регистрации не требуется.

Чтение данных

Используйте ExecuteReaderAsync для выполнения SELECT-запросов. Возвращаемый ClickHouseDataReader предоставляет типизированный доступ к столбцам результата с помощью таких методов, как GetInt64(), GetString() и GetFieldValue<T>(). Вызовите Read(), чтобы перейти к следующей строке. Метод возвращает false, когда строк больше не осталось. К столбцам можно обращаться по индексу (с нуля) или по имени столбца.
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)}");
}

Параметры 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)})
SQL-параметры ‘bind’ передаются как параметры HTTP-запроса в URI, поэтому их слишком большое количество может привести к исключению “URL too long”. Чтобы избежать этого ограничения при массовой вставке данных, используйте InsertBinaryAsync.

Query ID

Каждому запросу назначается уникальный 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
);
Если вы задаёте собственный QueryId, убедитесь, что он уникален для каждого вызова. Хорошим выбором будет случайный GUID.

Пользовательское сопоставление типов параметров

При использовании параметров в стиле @ (например, WHERE id = @id) драйвер автоматически определяет тип ClickHouse по типу значения .NET. Например, int сопоставляется с Int32, а DateTime — с DateTime. Чтобы переопределить эти значения по умолчанию, задайте ParameterTypeResolver в ClickHouseClientSettings. Это полезно, если вы хотите, чтобы все параметры DateTime использовали DateTime64(3) с точностью до миллисекунд, или чтобы для всех десятичных значений использовался определённый масштаб, без необходимости задавать 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);     // Преобразуется в DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Преобразуется в 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; // Передать управление стандартному обработчику

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}
Вы также можете задать resolver для отдельного запроса через QueryOptions.ParameterTypeResolver. Если он задан, он имеет приоритет над resolver на уровне клиента. Приоритет разрешения типов: Resolver — это один из шагов в цепочке приоритетов. От наивысшего приоритета к наименьшему:
  1. Явно заданный ClickHouseType у параметра
  2. Подсказка типа SQL из синтаксиса {name:Type} в запросе
  3. IParameterTypeResolver (из QueryOptions.ParameterTypeResolver с откатом к ClickHouseClientSettings.ParameterTypeResolver)
  4. Встроенный вывод типов (TypeConverter.ToClickHouseType)
Resolver также работает с путём ADO.NET ClickHouseConnection — настройки наследуются соединениями, созданными клиентом.

Прямая потоковая передача

Используйте 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. Все доступные варианты см. в документации по форматам.

Вставка из необработанного потока

Используйте 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"] // Необязательно: укажите столбцы
);
Параметры, управляющие поведением ингестии данных, см. в документации по настройкам форматов.

Дополнительные примеры

Дополнительные практические примеры использования см. в каталоге examples репозитория GitHub.

ADO.NET

Библиотека предоставляет полную поддержку ADO.NET через ClickHouseConnection, ClickHouseCommand и ClickHouseDataReader. Этот API необходим для интеграции с ORM (Dapper, Linq2db), а также в случаях, когда нужны стандартные абстракции базы данных .NET.

Управление жизненным циклом с ClickHouseDataSource

Всегда создавайте соединения через ClickHouseDataSource, чтобы обеспечить корректное управление жизненным циклом и использование пула соединений. DataSource внутренне использует один ClickHouseClient, и все соединения совместно используют его пул HTTP-соединений.
using ClickHouse.Driver.ADO;

// Создать DataSource один раз (зарегистрировать как singleton в DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Создавать лёгкие соединения по мере необходимости
await using var connection = await dataSource.OpenConnectionAsync();

// Использовать соединение
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
При использовании внедрения зависимостей:
// В Startup.cs или Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// В вашем сервисе
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

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

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Используйте соединение...
    }
}
Не создавайте ClickHouseConnection напрямую в коде для продакшн. При каждом таком создании экземпляра создаются новый HTTP-клиент и новый пул соединений, что под нагрузкой может привести к исчерпанию сокетов:
// НЕ ДЕЛАЙТЕ ТАК - каждый раз создается новый пул соединений
using var conn = new ClickHouseConnection("Host=localhost");
await conn.OpenAsync();
Вместо этого всегда используйте ClickHouseDataSource или переиспользуйте один экземпляр ClickHouseClient.

Использование ClickHouseCommand

Создавайте команды на основе соединения для выполнения SQL:
await using var connection = await dataSource.OpenConnectionAsync();

// Создание команды с SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Выполнение и чтение результатов
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())
{
    // Доступ по индексу столбца
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Доступ по имени столбца
    var email = reader.GetString("email");

    // Универсальный доступ
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Проверка на null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}

Рекомендации

Время жизни соединений и пул соединений

ClickHouse.Driver использует System.Net.Http.HttpClient внутри. У HttpClient есть отдельный пул соединений для каждой конечной точки. В результате:
  • Сеансы базы данных мультиплексируются через HTTP-соединения, которыми управляет пул соединений.
  • HTTP-соединения автоматически переиспользуются пулом.
  • Соединения могут оставаться активными даже после освобождения объектов ClickHouseClient или ClickHouseConnection.
Рекомендуемые подходы:
СценарийРекомендуемый подход
Общий случайИспользуйте singleton ClickHouseClient
ADO.NET / ORMИспользуйте ClickHouseDataSource (он создает соединения, использующие один и тот же пул)
Окружения с DIРегистрируйте ClickHouseClient или ClickHouseDataSource как singleton через IHttpClientFactory
При использовании пользовательского HttpClient или HttpClientFactory убедитесь, что для PooledConnectionIdleTimeout задано значение меньше, чем keep_alive_timeout сервера, чтобы избежать ошибок из-за полузакрытых соединений. Значение keep_alive_timeout по умолчанию для развертываний в Cloud составляет 10 секунд.
Не создавайте несколько экземпляров ClickHouseClient или автономных экземпляров ClickHouseConnection без общего HttpClient. Каждый экземпляр создает собственный пул соединений.

Обработка 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
    );
    

Асинхронные вставки

Асинхронные вставки переносят ответственность за батчинг с клиента на сервер. Вместо батчинга на стороне клиента сервер буферизует входящие данные и сбрасывает их в хранилище при достижении настраиваемых пороговых значений. Это особенно полезно в сценариях с высоким параллелизмом, например для рабочих нагрузок обсервабилити, где множество агентов отправляют небольшие полезные нагрузки. Включите асинхронные вставки через CustomSettings или строку подключения:
// Использование CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Рекомендуется: ожидать подтверждения сброса буфера

// Или через строку подключения
// "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Вставка завершается сразу после буферизации данных. Нет гарантии, что данные будут сохранены.Только если допустима потеря данных
При 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", // Необязательно — будет сгенерирован автоматически, если не указан
};

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
Int8sbyte
UInt8byte
Int16short
UInt16ushort
Int32int
UInt32uint
Int64long
UInt64ulong
Int128BigInteger
UInt128BigInteger
Int256BigInteger
UInt256BigInteger

Типы чисел с плавающей точкой

Тип ClickHouseТип .NET
Float32float
Float64double
BFloat16float

Десятичные типы

Тип ClickHouseТип .NET
Decimal(P, S)decimal / ClickHouseDecimal
Decimal32(S)decimal / ClickHouseDecimal
Decimal64(S)decimal / ClickHouseDecimal
Decimal128(S)decimal / ClickHouseDecimal
Decimal256(S)decimal / ClickHouseDecimal
Преобразование типа Decimal регулируется настройкой UseCustomDecimals.

Логический тип

Тип ClickHouseТип .NET
Boolbool

Строковые типы

Тип ClickHouseТип .NET
Stringstring
FixedString(N)string
По умолчанию столбцы String и FixedString(N) возвращаются как string. Установите ReadStringsAsByteArrays=true в строке подключения, чтобы вместо этого считывать их как byte[]. Это полезно при хранении бинарных данных, которые могут быть не в корректной кодировке UTF-8.

Типы даты и времени

Тип ClickHouseТип .NET
DateDateTime
Date32DateTime
DateTimeDateTime
DateTime32DateTime
DateTime64DateTime
TimeTimeSpan
Time64TimeSpan
ClickHouse хранит значения DateTime и DateTime64 внутри как Unix-временные метки (секунды или доли секунды с начала эпохи Unix). Хотя хранение всегда выполняется в UTC, со столбцами может быть связан часовой пояс, который влияет на то, как значения отображаются и интерпретируются. При чтении значений DateTime свойство DateTime.Kind устанавливается на основе часового пояса столбца:
Определение столбцаВозвращаемый DateTime.KindПримечания
DateTime('UTC')UtcЯвно указан часовой пояс UTC
DateTime('Europe/Amsterdam')UnspecifiedПрименяется смещение
DateTimeUnspecifiedЛокальное время сохраняется как есть
Для столбцов не в 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Примечания
JsonJsonObjectПо умолчанию (JsonReadMode=Binary)
JsonstringПри JsonReadMode=String
Возвращаемый тип для JSON-столбцов задаётся параметром JsonReadMode:
  • Binary (по умолчанию): Возвращает System.Text.Json.Nodes.JsonObject. Обеспечивает структурированный доступ к JSON-данным, но специализированные типы ClickHouse (например, IP-адреса, UUID и большие decimal-значения) внутри структуры JSON преобразуются в строковое представление.
  • String: Возвращает исходный JSON в виде string. Сохраняет точное представление JSON из ClickHouse, что полезно, когда JSON нужно передать дальше без парсинга или если вы хотите самостоятельно выполнять десериализацию.
// Настройка строкового режима через параметры
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Или через строку подключения
// "Host=localhost;JsonReadMode=String"

Другие типы

Тип ClickHouseТип .NET
UUIDGuid
IPv4IPAddress
IPv6IPAddress
NothingDBNull
DynamicСм. примечание
Array(T)T[]
Tuple(T1, T2, …)Tuple<T1, T2, ...> / LargeTuple
Map(K, V)Dictionary<K, V>
Nullable(T)T?
Enum8string
Enum16string
LowCardinality(T)То же, что и T
SimpleAggregateFunctionТо же, что и базовый тип
Nested(…)Tuple[]
Variant(T1, T2, …)См. примечание
QBit(T, dimension)T[]
Типы Dynamic и Variant преобразуются в тип, соответствующий фактическому базовому типу в каждой строке.

Геометрические типы

Тип ClickHouseТип .NET
PointTuple<double, double>
RingTuple<double, double>[]
LineStringTuple<double, double>[]
PolygonRing[]
MultiLineStringLineString[]
MultiPolygonPolygon[]
GeometryСм. примечание
Тип Geometry — это Variant, который может содержать любой из геометрических типов. Он будет преобразован в соответствующий тип.

Сопоставление типов: запись в ClickHouse

При вставке данных драйвер преобразует типы .NET в соответствующие типы ClickHouse. В таблицах ниже показано, какие типы .NET допускаются для каждого типа столбца ClickHouse.

Целочисленные типы

Тип ClickHouseДопустимые типы .NETПримечания
Int8sbyte, любой тип, совместимый с Convert.ToSByte()
UInt8byte, любой тип, совместимый с Convert.ToByte()
Int16short, любой тип, совместимый с Convert.ToInt16()
UInt16ushort, любой тип, совместимый с Convert.ToUInt16()
Int32int, любой тип, совместимый с Convert.ToInt32()
UInt32uint, любой тип, совместимый с Convert.ToUInt32()
Int64long, любой тип, совместимый с Convert.ToInt64()
UInt64ulong, любой тип, совместимый с Convert.ToUInt64()
Int128BigInteger, decimal, double, float, int, uint, long, ulong, любой тип, совместимый с Convert.ToInt64()
UInt128BigInteger, decimal, double, float, int, uint, long, ulong, любой тип, совместимый с Convert.ToInt64()
Int256BigInteger, decimal, double, float, int, uint, long, ulong, любой тип, совместимый с Convert.ToInt64()
UInt256BigInteger, decimal, double, float, int, uint, long, ulong, любой тип, совместимый с Convert.ToInt64()

Типы с плавающей точкой

Тип ClickHouseДопустимые типы .NETПримечания
Float32float, любой тип, совместимый с Convert.ToSingle()
Float64double, любой тип, совместимый с Convert.ToDouble()
BFloat16float, любой тип, совместимый с Convert.ToSingle()Преобразуется в 16-битный формат brain floating point с усечением

Логический тип

Тип ClickHouseДопустимые типы .NETПримечания
Boolbool

Строковые типы

Тип ClickHouseДопустимые типы .NETПримечания
Stringstring, byte[], ReadOnlyMemory<byte>, StreamБинарные типы записываются напрямую; потоки могут поддерживать seek или не поддерживать его
FixedString(N)string, byte[], ReadOnlyMemory<byte>, StreamСтрока кодируется в UTF-8 и дополняется; бинарные типы должны содержать ровно N байт

Типы даты и времени

Тип ClickHouseДопустимые типы .NETПримечания
DateDateTime, DateTimeOffset, DateOnly, типы NodaTimeПреобразуется в дни Unix как UInt16
Date32DateTime, DateTimeOffset, DateOnly, типы NodaTimeПреобразуется в дни Unix как Int32
DateTimeDateTime, DateTimeOffset, DateOnly, типы NodaTimeПодробности см. ниже
DateTime32DateTime, DateTimeOffset, DateOnly, типы NodaTimeТо же, что и DateTime
DateTime64DateTime, DateTimeOffset, DateOnly, типы NodaTimeТочность зависит от параметра Scale
TimeTimeSpan, intОграничивается диапазоном ±999:59:59; int интерпретируется как секунды
Time64TimeSpan, decimal, double, float, int, long, stringСтрока разбирается как [-]HHH:MM:SS[.fraction]; ограничивается диапазоном ±999:59:59.999999999
Драйвер учитывает DateTime.Kind при записи значений:
DateTime.KindHTTP-параметрыПакетная вставка
UtcТочный момент времени сохраняетсяТочный момент времени сохраняется
LocalТочный момент времени сохраняетсяТочный момент времени сохраняется
UnspecifiedИнтерпретируется как локальное время в часовом поясе типа параметра (по умолчанию UTC)Интерпретируется как локальное время в часовом поясе столбца
Значения DateTimeOffset всегда сохраняют точный момент времени. Пример: UTC DateTime (точный момент времени сохраняется)
var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Сохраняется как 12:00 UTC
// Чтение из столбца DateTime('Europe/Amsterdam'): 13:00 (UTC+1)
// Чтение из столбца DateTime('UTC'): 12:00 UTC
Пример: DateTime без указания часового пояса (время по местным часам)
var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Записано в столбец DateTime('Europe/Amsterdam'): сохранено как 14:30 по амстердамскому времени
// Считано из столбца DateTime('Europe/Amsterdam'): 14:30
Рекомендация: для максимально простого и предсказуемого поведения используйте DateTimeKind.Utc или DateTimeOffset во всех операциях с DateTime. Это гарантирует, что ваш код будет работать одинаково независимо от часового пояса сервера, клиента или столбца.

HTTP-параметры vs пакетная загрузка

При записи значений DateTime с Unspecified есть важное различие между привязкой HTTP-параметров и пакетной загрузкой: Bulk Copy знает часовой пояс целевого столбца и корректно интерпретирует значения Unspecified в этом часовом поясе. HTTP Parameters не знают часовой пояс столбца автоматически. Его необходимо указать в подсказке типа SQL:
// ВЕРНО: Часовой пояс в подсказке типа SQL — тип извлекается автоматически
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// НЕВЕРНО: Без подсказки часового пояса интерпретируется как UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// Строковое значение "2024-01-15 14:30:00" интерпретируется как UTC, а не как амстердамское время!
DateTime.KindЦелевой столбецHTTP-параметр (с указанием часового пояса)HTTP-параметр (без указания часового пояса)Пакетная загрузка
UtcUTCТочный момент времени сохраняетсяТочный момент времени сохраняетсяТочный момент времени сохраняется
UtcEurope/AmsterdamТочный момент времени сохраняетсяТочный момент времени сохраняетсяТочный момент времени сохраняется
LocalЛюбойТочный момент времени сохраняетсяТочный момент времени сохраняетсяТочный момент времени сохраняется
UnspecifiedUTCИнтерпретируется как UTCИнтерпретируется как UTCИнтерпретируется как UTC
UnspecifiedEurope/AmsterdamИнтерпретируется как время АмстердамаИнтерпретируется как UTCИнтерпретируется как время Амстердама

Десятичные типы

Тип ClickHouseДопустимые типы .NETПримечания
Decimal(P,S)decimal, ClickHouseDecimal, любой тип, совместимый с Convert.ToDecimal()Вызывает OverflowException при превышении точности
Decimal32decimal, ClickHouseDecimal, любой тип, совместимый с Convert.ToDecimal()Максимальная точность 9
Decimal64decimal, ClickHouseDecimal, любой тип, совместимый с Convert.ToDecimal()Максимальная точность 18
Decimal128decimal, ClickHouseDecimal, любой тип, совместимый с Convert.ToDecimal()Максимальная точность 38
Decimal256decimal, ClickHouseDecimal, любой тип, совместимый с Convert.ToDecimal()Максимальная точность 76

Тип JSON

Тип ClickHouseДопустимые типы .NETПримечания
Jsonstring, JsonObject, JsonNode, любой объектПоведение зависит от настройки JsonWriteMode
Поведение при записи JSON определяется настройкой JsonWriteMode:
Тип входных данных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.
// Режим String по умолчанию работает с любыми входными данными
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Режим Binary требует явного включения и регистрации типа
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 (по умолчанию): POCO сериализуются через System.Text.Json.JsonSerializer. Регистрировать типы не требуется. Это самый простой вариант, и он работает с анонимными объектами. Бинарный режим: POCO сериализуются с использованием бинарного JSON-формата драйвера с полной поддержкой подсказок типа. Перед использованием типы необходимо зарегистрировать с помощью connection.RegisterJsonSerializationType<T>(). Этот режим поддерживает пользовательские сопоставления путей с помощью атрибутов:
  • [ClickHouseJsonPath("path")]: Связывает свойство с пользовательским JSON-путём. Полезно для вложенных структур или когда имя свойства отличается от нужного JSON-ключа. Работает только в бинарном режиме.
  • [ClickHouseJsonIgnore]: Исключает свойство из сериализации. Работает только в бинарном режиме.
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; }  // Не сериализуется
}

// Для бинарного режима: зарегистрируйте тип и включите бинарный режим
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Вставка POCO — сериализуется в JSON с вложенной структурой через пользовательские атрибуты пути
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Результирующий JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
Сопоставление имён свойств с подсказками типов столбцов чувствительно к регистру. Свойство UserId будет сопоставлено только с подсказкой, заданной как UserId, а не userid. Это соответствует поведению ClickHouse, где пути вроде userName и UserName могут сосуществовать как отдельные поля. Ограничения (только для режима Binary):
  • Типы POCO должны быть зарегистрированы для подключения с помощью connection.RegisterJsonSerializationType<T>() до сериализации. Попытка сериализовать незарегистрированный тип вызывает исключение ClickHouseJsonSerializationException.
  • Для корректной сериализации свойств словарей и массивов/списков требуются подсказки типов в определении столбца. Без таких подсказок используйте режим String.
  • Значения NULL в свойствах POCO записываются только в том случае, если для пути в определении столбца указана подсказка типа Nullable(T). ClickHouse не допускает типы Nullable внутри динамических JSON-путей, поэтому свойства со значением null без подсказок пропускаются.
  • Атрибуты ClickHouseJsonPath и ClickHouseJsonIgnore игнорируются в режиме String (они работают только в режиме Binary).

Другие типы

Тип ClickHouseДопустимые типы .NETПримечания
UUIDGuid, stringСтрока преобразуется в Guid
IPv4IPAddress, stringДолжен быть IPv4; строка разбирается с помощью IPAddress.Parse()
IPv6IPAddress, stringДолжен быть IPv6; строка разбирается с помощью IPAddress.Parse()
NothingAnyНичего не записывает (no-op)
DynamicНе поддерживается (генерирует NotImplementedException)
Array(T)IList, nullПри null записывается пустой массив
Tuple(T1, T2, …)ITuple, IListЧисло элементов должно соответствовать арности кортежа
Map(K, V)IDictionary
Nullable(T)null, DBNull, или типы, допустимые для TПеред значением записывается байт флага null
Enum8string, sbyte, числовые типыДля строки выполняется поиск в словаре enum
Enum16string, short, числовые типыДля строки выполняется поиск в словаре enum
LowCardinality(T)Типы, допустимые для TОбработка делегируется базовому типу
SimpleAggregateFunctionТипы, допустимые для базового типаОбработка делегируется базовому типу
Nested(…)IList из кортежейЧисло элементов должно соответствовать числу полей
Variant(T1, T2, …)Значение, соответствующее одному из T1, T2, …Генерирует ArgumentException, если не найдено совпадение ни с одним типом
QBit(T, dim)IListОбработка делегируется Array; размерность используется только как метаданные

Геометрические типы

Тип ClickHouseДопустимые типы .NETПримечания
PointSystem.Drawing.Point, ITuple, IList (2 элемента)
RingIList из точек
LineStringIList из точек
PolygonIList из колец
MultiLineStringIList из объектов LineString
MultiPolygonIList из объектов Polygon
GeometryЛюбой из перечисленных выше геометрических типовVariant всех геометрических типов

Не поддерживается при записи

Тип ClickHouseПримечания
DynamicВозникает NotImplementedException
AggregateFunctionВозникает AggregateFunctionException

Обработка вложенных типов

Вложенные типы 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.ConnectionClickHouseConnectionЖизненный цикл соединения, выбор фабрики HTTP-клиентов, открытие/закрытие соединения, управление сеансом.
ClickHouse.Driver.CommandClickHouseCommandНачало/завершение выполнения запроса, время выполнения, идентификаторы запросов, статистика сервера и сведения об ошибках.
ClickHouse.Driver.TransportClickHouseConnectionНизкоуровневые запросы для потоковой передачи по HTTP, флаги сжатия, коды состояния ответа и ошибки транспорта.
ClickHouse.Driver.ClientClickHouseClientБинарная вставка, запросы и другие операции
ClickHouse.Driver.NetTraceTraceHelperСетевая трассировка, только при включенном режиме отладки

Пример: Диагностика проблем с подключением

{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}
Будет записываться в журнал:
  • Выбор фабрики HTTP-клиента (пул по умолчанию или одиночное соединение)
  • Конфигурация HTTP-обработчика (SocketsHttpHandler или HttpClientHandler)
  • Настройки пула соединений (MaxConnectionsPerServer, PooledConnectionLifetime и т. д.)
  • Настройки тайм-аутов (ConnectTimeout, Expect100ContinueTimeout и т. д.)
  • Настройка SSL/TLS
  • События открытия и закрытия соединения
  • Отслеживание идентификатора сеанса

Режим отладки: сетевая трассировка и диагностика

Чтобы упростить диагностику сетевых проблем, библиотека драйвера содержит вспомогательный механизм, который включает низкоуровневую трассировку внутренних механизмов сетевой подсистемы .NET. Чтобы включить его, необходимо передать LoggerFactory с установленным уровнем Trace и задать EnableDebugMode = true (или включить его вручную через класс ClickHouse.Driver.Diagnostic.TraceHelper). События будут записываться в категорию ClickHouse.Driver.NetTrace. Предупреждение: это приведет к созданию чрезвычайно подробных журналов и повлияет на производительность. Включать режим отладки в продакшн не рекомендуется.
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Необходим уровень Trace для отображения сетевых событий
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Включить низкоуровневую трассировку сети
};

OpenTelemetry

Драйвер поддерживает встроенную распределённую трассировку OpenTelemetry через API .NET System.Diagnostics.Activity. При включении драйвер создаёт спаны для операций с базой данных, которые можно экспортировать в системы обсервабилити, такие как Jaeger или сам ClickHouse (через OpenTelemetry Collector).

Включение трассировки

В приложениях ASP.NET Core добавьте ActivitySource драйвера ClickHouse в конфигурацию OpenTelemetry:
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Подписка на spans драйвера ClickHouse
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Или AddJaegerExporter() и т.д.
Для консольных приложений, тестирования или ручной настройки:
using OpenTelemetry;
using OpenTelemetry.Trace;

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

Атрибуты спана

Каждый спан включает стандартные атрибуты OpenTelemetry для базы данных, а также специфичную для ClickHouse статистику запросов, которую можно использовать для отладки.
АтрибутОписание
db.systemВсегда "clickhouse"
db.nameИмя базы данных
db.userИмя пользователя
db.statementSQL-запрос (если включен)
db.clickhouse.read_rowsСтроки, прочитанные запросом
db.clickhouse.read_bytesБайты, прочитанные запросом
db.clickhouse.written_rowsСтроки, записанные запросом
db.clickhouse.written_bytesБайты, записанные запросом
db.clickhouse.elapsed_nsВремя выполнения на стороне сервера в наносекундах

Параметры конфигурации

Настройте поведение трассировки с помощью ClickHouseDiagnosticsOptions:
using ClickHouse.Driver.Diagnostic;

// Включать SQL-операторы в spans (по умолчанию: false из соображений безопасности)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Усекать длинные SQL-операторы (по умолчанию: 1000 символов)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
Включение IncludeSqlInActivityTags может привести к раскрытию конфиденциальных данных в трассировках. Используйте с осторожностью в продакшн-средах.

Настройка TLS

При подключении к ClickHouse по HTTPS поведение TLS/SSL можно настроить несколькими способами.

Пользовательская проверка сертификатов

Для продакшн-окружений, в которых требуется пользовательская логика проверки сертификатов, передайте собственный HttpClient с настроенным обработчиком ServerCertificateCustomValidationCallback:
using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // Обязательно, если сжатие включено (по умолчанию)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Пример: принять сертификат с определённым отпечатком
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Пример: принять сертификаты от определённого издателя
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // По умолчанию: использовать стандартную проверку
        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);
Важные моменты при использовании пользовательского HttpClient
  • Автоматическая декомпрессия: Нужно включить AutomaticDecompression, если сжатие не отключено (по умолчанию сжатие включено).
  • Тайм-аут простоя: Установите PooledConnectionIdleTimeout меньше значения keep_alive_timeout сервера (10 секунд для ClickHouse Cloud), чтобы избежать ошибок подключения из-за полуоткрытых соединений.

Поддержка ORM

Для ORM требуется API ADO.NET (ClickHouseConnection). Чтобы корректно управлять временем жизни подключения, создавайте подключения через ClickHouseDataSource:
// Зарегистрировать DataSource как singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Создать подключения для использования с ORM
await using var connection = await dataSource.OpenConnectionAsync();
// Передать подключение в вашу ORM...

Dapper

ClickHouse.Driver работает с Dapper. Драйвер автоматически преобразует синтаксис Dapper @parameter в нативный для ClickHouse синтаксис {parameter:Type}, при этом типы выводятся автоматически на основе значений .NET. Используйте ClickHouseDataSource для корректного управления временем жизни соединения:
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Зарегистрировать как singleton в 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 });
// или: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

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

Запросы в объекты POCO

Dapper сопоставляет столбцы со свойствами по имени (регистронезависимо):
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// Из таблицы
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// Из литерала
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();

Собственный синтаксис параметров ClickHouse

Если вам нужен явный контроль над типами, используйте непосредственно в SQL синтаксис ClickHouse {param:Type}, а значения параметров передавайте через 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

Встроенное в Dapper раскрытие IN работает:
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 (для столбцов Decimal64/128/256)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (для столбцов Int128/Int256/UInt128/UInt256)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (для столбцов IPv4/IPv6)
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
Вложенные типы как параметрыНе поддерживаетсяПо той же причине — Dapper отклоняет сложные типы в качестве значений параметров
Гео-типы как параметрыНе поддерживается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.
Этот провайдер активно развивается. Текущая версия поддерживает LINQ-запросы (включая JOIN, подзапросы и операции над множествами), INSERT через SaveChanges / BulkInsertAsync, миграции с полной поддержкой DDL (CREATE / ALTER / DROP), а также настройку движка таблицы ClickHouse. UPDATE / DELETE не поддерживаются.

Установка

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");
}

// Запрос
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, UInt8UInt64sbyte, short, int, long, byte, ushort, uint, ulong
Большие целые числаInt128, Int256, UInt128, UInt256BigInteger
Числа с плавающей точкойFloat32, Float64, BFloat16float, double
Десятичные числаDecimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S)decimal или ClickHouseDecimal
BoolBoolbool
СтрокиString, FixedString(N)string
ПеречисленияEnum8(...), Enum16(...)string или C# enum
Дата/времяDate, Date32, DateTime, DateTime64(P, 'TZ')DateOnly, DateTime
ВремяTime, Time64(N)TimeSpan
UUIDUUIDGuid
Сетевые типыIPv4, IPv6IPAddress
МассивыArray(T)T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
MapMap(K, V)Dictionary<K,V>
TupleTuple(T1, ...)Tuple<...> или ValueTuple<...>
VariantVariant(T1, T2, ...)object
DynamicDynamicobject
JSONJsonJsonNode или string
ГеопространственныеPoint, Ring, LineString, Polygon, MultiLineString, MultiPolygon, GeometryTuple<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, а также скалярные подзапросы в проекциях. Операции над множествами: Concat (→ UNION ALL), Union (→ UNION DISTINCT), Intersect, Except. Встроенные локальные коллекции: JOIN и Contains с коллекциями в памяти (int[], List<T> и т. д.) преобразуются в последовательность UNION. Строковые методы: 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 во все подключения, чтобы поведение JOIN соответствовало ожиданиям Entity Framework. Если ваш сервер ClickHouse или профиль запрещает изменять эту настройку (например, профиль readonly=1), отключите это поведение с помощью:
optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());
При включенном opt-out LEFT JOIN возвращает значения столбцов ClickHouse по умолчанию, и механизм определения навигации по null в EF больше не работает должным образом. Используйте явные сравнения с 0 / "" вместо == null.

Вставка данных

SaveChanges использует нативный API драйвера 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 после вставки, поэтому перехода состояния AddedUnchanged не происходит.

Перечисления

Столбцы ClickHouse Enum8/Enum16 можно сопоставить со свойствами string или типами C# enum. При использовании перечислений C# провайдер автоматически преобразует значения перечисления в их строковое представление и обратно:
public enum Status { Active, Inactive, Pending }

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

// Запрос с enum-значениями
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; }
}

// В 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] };
    }
}

// Применить к одному свойству:
.HasConversion<MoneyConverter>()

// Или применить ко всем свойствам типа через соглашения:
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 в .NET сопоставляются с object. Поскольку object — слишком общий тип для автоматического вывода типов, необходимо явно указать тип хранения через .HasColumnType():
public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// В OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// или:
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; }
}

// В 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, а для столбца — тип JsonValueConverter будет применён автоматически:
public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // необработанная JSON-строка
}

entity.Property(e => e.Data).HasColumnType("Json");
  • Пути JSON не транслируютсяentity.Data["name"] в LINQ не преобразуется в SQL-синтаксис ClickHouse data.name. Фильтруйте по не-JSON-столбцам и анализируйте JSON в памяти.
  • Семантика NULL — JSON type в ClickHouse возвращает {} (пустой объект) для значений NULL, а не SQL NULL.
  • Точность целых чисел — ClickHouse хранит все целые числа в JSON как Int64. При чтении через JsonNode используйте GetValue<long>(), а не GetValue<int>().

Движки таблиц

Настраивайте движки таблиц ClickHouse и специфичные для движка секции через fluent API 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"));
});
Поддерживаемые семейства движков:
EngineFluent methodNotes
MergeTreeHasMergeTreeEngine()Используется по умолчанию, если ничего не настроено
ReplacingMergeTreeHasReplacingMergeTreeEngine("Version", "IsDeleted") или HasReplacingMergeTreeEngine<T>(e => e.Version)Столбцы Version / IsDeleted необязательны
SummingMergeTreeHasSummingMergeTreeEngine(…) или HasSummingMergeTreeEngine<T>(e => new { … })Необязательные суммируемые столбцы
AggregatingMergeTreeHasAggregatingMergeTreeEngine()
CollapsingMergeTreeHasCollapsingMergeTreeEngine("Sign") или HasCollapsingMergeTreeEngine<T>(e => e.Sign)Столбец Sign должен иметь тип Int8
VersionedCollapsingMergeTreeHasVersionedCollapsingMergeTreeEngine("Sign", "Version") или <T>(e => e.Sign, e => e.Version)
GraphiteMergeTreeHasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, MemoryHasLogEngine(), 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);

// Индекс с параметрами (например, 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 не обеспечивает уникальность. Уникальные индексы вызывают исключение при выполнении миграции.
Значения, генерируемые сервером (auto-increment / IDENTITY)В ClickHouse нет эквивалента.
Столбцы Nested(…)Пока не поддерживаются как сопоставляемый тип CLR.
Принадлежащие сущности в JSON (.ToJson())Структурное сопоставление JSON для принадлежащих сущностей пока не реализовано. Вместо этого используйте JsonNode / string в столбце Json (см. JSON-столбцы).
Помимо миграций, провайдер также пока не поддерживает:
  • UPDATE / DELETE
  • Транзакции: BeginTransaction — no-op. ClickHouse не поддерживает ACID-транзакции.
  • Трансляцию запросов с JSON-путём: entity.Data["key"] в LINQ не транслируется в SQL-синтаксис ClickHouse data.key. Фильтруйте по не-JSON-столбцам, а JSON анализируйте в памяти.

Ограничения

Столбцы AggregateFunction

Столбцы типа AggregateFunction(...) нельзя запрашивать или напрямую вставлять в них данные. Чтобы выполнить вставку:
INSERT INTO t VALUES (uniqState(1));
Чтобы выбрать:
SELECT uniqMerge(c) FROM t;

Последнее изменение 10 июня 2026 г.