Skip to main content
Официальный 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

Поддерживаемые версии ClickHouse

Клиент официально поддерживает три последних релиза и два последних LTS-релиза.

Установка

Установите пакет из NuGet:
Или через NuGet Package Manager:

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

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

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

Настройки соединения

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

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

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

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

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

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

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

При задании пользовательских настроек через строку подключения используйте префикс set_, например «set_max_threads=4». При использовании объекта ClickHouseClientSettings префикс set_ указывать не нужно.Полный список доступных настроек см. здесь.

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

Базовое соединение

С нестандартными настройками ClickHouse


QueryOptions

QueryOptions позволяет переопределять настройки уровня клиента для отдельных запросов. Все свойства необязательны: если свойство задано, оно переопределяет соответствующее значение клиента по умолчанию. Пример:

InsertOptions

InsertOptions расширяет QueryOptions настройками массовой вставки через InsertBinaryAsync. Все свойства QueryOptions также доступны в InsertOptions. Пример:

Отключение предварительного запроса для определения схемы

По умолчанию InsertBinaryAsync перед каждой вставкой отправляет запрос SELECT ... WHERE 1=0, чтобы определить типы столбцов. При высоких требованиях к пропускной способности избежать связанных с этим дополнительных затрат можно двумя способами: Вариант 1: Явно указать типы столбцов Если схема таблицы известна на этапе компиляции, передайте типы столбцов через ColumnTypes. Тогда запрос для определения схемы вообще не отправляется:
Вариант 2: кэширование схемы Если вы многократно выполняете вставку в одну и ту же таблицу, установите UseSchemaCache = true. Тогда схема будет запрошена только один раз и повторно использована при последующих вставках через тот же инстанс ClickHouseClient:
  • ColumnTypes имеет приоритет над UseSchemaCache. Если заданы оба параметра, используются явно указанные типы.
  • Кэш схемы не отслеживает изменения, внесённые командой ALTER TABLE. Если вы изменили схему таблицы, создайте новый ClickHouseClient или не используйте UseSchemaCache для этой таблицы.
  • Кэш привязан к инстансу ClickHouseClient и использует в качестве ключа пару (база данных, таблица). Разные наборы столбцов одной таблицы используют одну и ту же схему из кэша.

ClickHouseClient

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

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

Создайте ClickHouseClient с помощью строки подключения или объекта ClickHouseClientSettings. Доступные параметры описаны в разделе Конфигурация. Сведения о вашем сервисе ClickHouse Cloud можно найти в консоли ClickHouse Cloud. Выберите сервис и нажмите Подключение: Выберите C#. Ниже появятся сведения о подключении. Если вы используете самоуправляемый ClickHouse, сведения о подключении предоставляет администратор ClickHouse. Подключение с помощью строки подключения:
Или с помощью ClickHouseClientSettings:
При использовании внедрения зависимостей применяйте IHttpClientFactory:
ClickHouseClient рассчитан на длительную работу и использование во всём приложении. Создайте один экземпляр (обычно как singleton) и используйте его для всех операций с базой данных. Клиент самостоятельно управляет пулом HTTP-соединений.

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

Для команд, которые не возвращают результат, используйте ExecuteNonQueryAsync:
Для получения одного значения используйте ExecuteScalarAsync:

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

Параметризованные вставки

Вставляйте данные с помощью параметризованных запросов, используя ExecuteNonQueryAsync. Типы параметров необходимо указывать непосредственно в SQL с помощью синтаксиса {name:Type}:

Массовая вставка

Используйте InsertBinaryAsync, чтобы эффективно вставлять большое количество строк. Метод передаёт данные потоком в собственном двоичном формате строк ClickHouse, поддерживает параллельную загрузку батчей и позволяет избежать ошибок “URL слишком длинный”, которые могут возникать при использовании параметризованных запросов.
Для больших объёмов данных настройте пакетную вставку и степень параллелизма с помощью InsertOptions:
  • Перед вставкой клиент автоматически получает структуру таблицы с помощью запроса SELECT * FROM <table> WHERE 1=0. Передаваемые значения должны соответствовать типам столбцов целевой таблицы. Чтобы не выполнять этот запрос, используйте InsertOptions.ColumnTypes или InsertOptions.UseSchemaCache.
  • При MaxDegreeOfParallelism > 1 батчи загружаются параллельно. Сеансы несовместимы с параллельной вставкой: отключите сеансы или установите MaxDegreeOfParallelism = 1.
  • Чтобы сервер подставлял значения DEFAULT для столбцов, значения которых не переданы, укажите RowBinaryFormat.RowBinaryWithDefaults в InsertOptions.Format.

Вставка объектов POCO

Вместо формирования массивов object[] можно вставлять непосредственно строго типизированные объекты POCO. Достаточно один раз зарегистрировать тип, а затем передать IEnumerable<T>:
По умолчанию все публичные свойства, доступные для чтения, сопоставляются со столбцами по строгому совпадению имён с учётом регистра. Это сопоставление можно настроить с помощью атрибутов:
Если для всех сопоставленных свойств явно указан Type, предварительный запрос для определения схемы не выполняется. Если типы явно указаны только для части свойств, драйвер всё равно выполняет такой запрос для всего набора столбцов. InsertBinaryAsync<T> поддерживает те же параметры InsertOptions (группировку в батчи, параллелизм, кэширование схемы), что и перегрузка для object[].
В отличие от перегрузки для object[], InsertBinaryAsync<T> не принимает явный список столбцов. Набор столбцов определяется сопоставленными свойствами зарегистрированного типа. Чтобы управлять этим набором, используйте [ClickHouseNotMapped] для исключения свойств или [ClickHouseColumn(Name = "...")] для изменения имён столбцов.Если в InsertOptions задан ColumnTypes, указанные в нём типы имеют приоритет над атрибутами POCO.

Изменение схемы

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

Размещение запроса на вставку

При бинарной вставке оператор INSERT INTO ... FORMAT ... записывается в первой строке тела запроса, перед строками данных. По умолчанию тело запроса сжимается, поэтому средства маршрутизации и логирования, анализирующие только URL, не видят оператор. Задайте для InsertOptions.QueryPlacement значение InsertQueryPlacement.Url, чтобы передавать оператор в параметре URL query, а в теле запроса оставлять только строки данных:
Используйте этот режим, если прокси-сервер, load balancer или шлюз маршрутизирует запросы либо проверяет их по параметру query, а также если нужно, чтобы оператор попадал в журналы доступа и инструменты обсервабилити. Режим необходимо включить явно, поскольку в нём длина оператора учитывается при расчёте длины URL. Фактический предел определяется самым строгим из ограничений среды выполнения .NET, промежуточного компонента и сервера. В .NET 6–9 System.Uri ограничивает длину полного закодированного URI запроса 65 519 символами; при превышении этого предела драйвер генерирует исключение InvalidOperationException, предлагающее использовать InsertQueryPlacement.Body. По умолчанию значение http_max_uri_size в ClickHouse составляет 1 МиБ, но промежуточный компонент может устанавливать более низкий предел. При размещении в теле запроса это ограничение длины URL не распространяется на оператор и строки данных; другие параметры запроса по-прежнему могут передаваться в URL. Эта настройка не зависит от Compressor: в обоих режимах тело запроса кодируется одинаково.

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

Используйте ExecuteReaderAsync для выполнения запросов SELECT. Возвращаемый объект ClickHouseDataReader предоставляет типизированный доступ к столбцам результата с помощью методов GetInt64(), GetString() и GetFieldValue<T>(). Вызывайте Read() для перехода к следующей строке. Когда строк больше нет, метод возвращает false. Обращайтесь к столбцам по индексу (с нуля) или по имени.

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

Вместо того чтобы читать столбцы по индексу или имени, можно передавать результаты запроса напрямую в собственные классы в потоковом режиме. Достаточно один раз зарегистрировать тип в клиенте, а затем использовать QueryAsync<T>:
RegisterPocoType<T>() настраивает сопоставление типов для вставки и чтения и сразу проверяет оба варианта. RegisterBinaryInsertType<T>() работает как прежде: для обратной совместимости он настраивает сопоставление только для вставки. Зарегистрированный тип должен иметь:
  • Публичный конструктор без параметров.
  • Хотя бы одно публичное свойство с публичным сеттером, не помеченным init. Свойства с модификатором required поддерживаются.
При сопоставлении столбцов учитывается регистр. Если в результате нет нужного столбца, свойство сохраняет значение по умолчанию; лишние столбцы результата игнорируются. Драйвер не выполняет расширяющее или сужающее преобразование значений. За исключением альтернативных представлений, перечисленных ниже, тип столбца в используемом фреймворке должен быть совместим с типом свойства; в противном случае генерируется InvalidOperationException. Поэтому свойство типа object принимает значение из любого столбца. QueryAsync<T> считывает данные из каждого из этих столбцов напрямую в свойство соответствующего типа: Для всех перечисленных типов свойств допускаются также nullable-варианты (long?, DateOnly? и т. д.), даже если столбец не имеет типа Nullable(...). Свойство значимого типа, не допускающее NULL, можно зарегистрировать для столбца Nullable(T), но при получении NULL возникнет исключение. Типы-обёртки, такие как LowCardinality(T), SimpleAggregateFunction(f, T) и Object(T), сопоставляются с типами свойств так же, как T. Составные типы столбцов также поддерживаются. Для них используются типы платформы, указанные в справочнике типов для чтения: Array(T) преобразуется в T[], Tuple(...) — в System.Tuple<...>, Nested(...) — в Tuple<...>[], JSON — в JsonObject (или string при JsonReadMode=String), а Variant/Dynamic — в object. Столбец Map(K, V) — особый случай: данные считываются в свойство типа List<KeyValuePair<K, V>> или KeyValuePair<K, V>[] без упаковки значений, с сохранением порядка элементов в передаваемых данных и повторяющихся ключей при любом значении MapReadMode. Свойство типа Dictionary<K, V> работает только в режиме по умолчанию. Типы ключа и значения должны совпадать в точности, поэтому для Map(String, Nullable(Int32)) требуется KeyValuePair<string, int?>. Если для столбца доступно несколько типов свойств (например, для DateTime — DateTime, DateTimeOffset или DateOnly, а для String — string или byte[]), представление определяется объявленным типом свойства. Эти альтернативные представления доступны при чтении в POCO: QueryAsync<T> их поддерживает, а MapTo<T> — нет. При ручном обходе строк считывателя используйте ClickHouseDataReader.MapTo<T>(), чтобы преобразовать текущую строку в объект зарегистрированного класса POCO, не переходя к следующей строке:
Используйте MapTo<T>, если вам нужно самостоятельно управлять циклом считывания — например, чтобы сочетать доступ к необработанным данным столбцов с материализацией POCO. Этот метод считывает строку через упакованные значения считывателя, поэтому не поддерживает перечисленные выше альтернативные типы свойств и выделяет больше памяти, чем QueryAsync<T>. Если нужны только строки, используйте QueryAsync<T>; численные показатели приведены в разделе выбор способа материализации. Преобразователь значений при чтении, настроенный для всего клиента или отдельного запроса, действует в обоих случаях и не отключает чтение без упаковки значений. Драйвер преобразует каждый столбец с помощью перегрузки, соответствующей способу его чтения: типизированной ConvertValue<T> для столбца, прочитанного без упаковки значений, и ConvertValue с упакованными значениями для составного столбца. Реализуйте обе перегрузки согласованно, иначе результат чтения одного и того же столбца будет зависеть от выбранного способа. Если настроен LoggerFactory, методы RegisterPocoType<T>() и RegisterBinaryInsertType<T>() записывают в лог сообщение уровня Debug (категория ClickHouse.Driver.Client) о том, какие свойства сопоставлены со столбцами, а какие пропущены и почему. Подробнее см. в разделе Логирование и диагностика.

Параметры SQL-запросов

В ClickHouse для параметров SQL-запросов используется стандартный формат {parameter_name:DataType}. Примеры:
Параметры SQL-запроса передаются в параметрах запроса HTTP URI, поэтому, если их слишком много, может возникнуть ошибка “слишком длинный URL”. Чтобы избежать этого ограничения при массовой вставке данных, используйте InsertBinaryAsync.

Плейсхолдеры @name в стиле ADO

Драйвер также поддерживает плейсхолдеры @name, которые используют ORM, например Dapper. Это удобный клиентский синтаксис: перед отправкой запроса каждый такой плейсхолдер заменяется на {name:ResolvedType}, поэтому сервер не получает символ @. Подробнее о выборе типа см. в разделе «Определение типа». По возможности используйте явную форму {name:Type}. Если для @name нет соответствующего параметра, плейсхолдер остаётся без изменений, и сервер отклоняет запрос. При сопоставлении учитывается регистр, поэтому @ID не связывается с параметром id.
Чтобы отключить замену, задайте переключатель AppContext ClickHouse.Driver.DisableReplacingParameters до первого использования драйвера. Отключается только замена в тексте запроса: параметры по-прежнему отправляются, поэтому запросы с нативным синтаксисом {name:Type} продолжают работать.

Параметры типа Identifier

Тип параметра Identifier позволяет безопасно подставлять имена баз данных, таблиц и столбцов вместо строковых литералов в кавычках. Чтобы использовать его, укажите в SQL {name:Identifier} или задайте ClickHouseDbParameter.ClickHouseType = "Identifier":
Значение передаётся без изменений, а сервер подставляет его как SQL-идентификатор, самостоятельно заключая в обратные кавычки и экранируя специальные символы. Идентификаторы со специальными символами (в том числе с обратными кавычками) сохраняются без искажений при записи и последующем чтении.

ID запроса

Каждому запросу присваивается уникальный query_id. С его помощью можно получать данные из таблицы system.query_log или отменять долго выполняющиеся запросы. Собственный ID запроса можно задать через QueryOptions:
Если вы задаёте собственный QueryId, убедитесь, что для каждого вызова он отличается. Хороший вариант — случайный GUID.

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

При использовании параметров вида @ (например, WHERE id = @id) драйвер автоматически определяет тип ClickHouse на основе типа значения .NET. Например, типу int соответствует Int32.
Определение типа параметров DateTimeЕсли для параметра вида @ в SQL не указан тип через {name:Type} и не задан ClickHouseType, для значений, представляющих конкретный момент времени, определяется тип DateTime('UTC'), а не просто DateTime. Значения DateTime с Kind, равным Utc или Local, и все значения DateTimeOffset передаются как DateTime('UTC'). Это позволяет сохранить момент времени независимо от часового пояса сервера.Явное указание типа ({name:DateTime}) имеет приоритет перед автоматическим определением. При составлении запросов рекомендуется указывать тип явно.
Чтобы изменить поведение по умолчанию, задайте ParameterTypeResolver в ClickHouseClientSettings. Например, так можно использовать DateTime64(3) для всех параметров DateTime, обеспечив точность до миллисекунд, или задать одинаковое число знаков после запятой для всех десятичных значений — без указания ClickHouseType для каждого параметра. Использование DictionaryParameterTypeResolver для простого сопоставления типов:
Собственная реализация IParameterTypeResolver для сложных сценариев: Если тип параметра нужно определять по его значению или имени, реализуйте интерфейс IParameterTypeResolver напрямую. Верните null, чтобы перейти к определению типа по умолчанию:
Вы также можете задать резолвер для отдельного запроса с помощью QueryOptions.ParameterTypeResolver. В этом случае он имеет приоритет над резолвером на уровне клиента. Порядок выбора типа: Резолвер — лишь одно из звеньев в порядке выбора типа. От наивысшего приоритета к низшему:
  1. ClickHouseType, явно заданный для параметра
  2. Подсказка типа SQL, указанная в запросе с помощью синтаксиса {name:Type}
  3. IParameterTypeResolver (из QueryOptions.ParameterTypeResolver, а если он не задан — из ClickHouseClientSettings.ParameterTypeResolver)
  4. Встроенный вывод типов (TypeConverter.ToClickHouseType)
Резолвер также работает при использовании ClickHouseConnection через ADO.NET: соединения, созданные клиентом, наследуют его настройки.

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

IParameterFormatter — механизм расширения, который определяет, как сериализуются значения параметров. Используйте его, если встроенное форматирование (например, точность DateTime, представление десятичных чисел с учётом региональных настроек, экранирование строк или представление чисел) не соответствует требованиям вашей схемы или систем, в которые передаются данные. Задайте ParameterFormatter в ClickHouseClientSettings, чтобы применять форматтер ко всем параметризованным запросам. Форматтер получает значение, определённое имя типа ClickHouse и имя параметра и возвращает строковое представление для отправки на сервер. Верните null, чтобы передать обработку форматтеру по умолчанию. Использование DictionaryParameterFormatter для простого форматирования по типам CLR:
Пользовательская реализация IParameterFormatter для сложных сценариев:
Вы также можете задать форматтер для отдельного запроса через QueryOptions.ParameterFormatter. Такой форматтер имеет приоритет над форматтером на уровне клиента. Составные значения: Форматтер применяется и к параметрам-коллекциям верхнего уровня, и к каждому элементу составных значений (Array, Tuple, Map, Nullable, LowCardinality, Variant). Например, при сопоставлении с typeof(int) каждый элемент типа Int32 в Array(Int32) форматируется отдельно. Заключение в одинарные кавычки в составных значениях: Для строковых типов ClickHouse (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) внутри составного литерала драйвер заключает результат работы форматтера в одинарные кавычки, но не экранирует его содержимое. Если возвращённая строка содержит неэкранированную одинарную кавычку или обратную косую черту, составной литерал окажется некорректным, и сервер отклонит запрос. Строковые параметры верхнего уровня (не входящие в составное значение) используются как есть, без заключения в кавычки, поэтому экранировать их не требуется. Приоритет форматтеров:
  1. IParameterFormatter (из QueryOptions.ParameterFormatter, а если он не задан — из ClickHouseClientSettings.ParameterFormatter). Если он возвращает значение, отличное от null, используется это значение.
  2. Встроенное форматирование по типу в HttpParameterFormatter.
Форматтер не вызывается для значений null и DBNull: они всегда сериализуются как маркер null в ClickHouse (\N).

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

IReadValueConverter позволяет преобразовывать значения, возвращаемые считывателем данных после десериализации, не меняя их тип CLR. Например, с его помощью можно задать DateTime.Kind = Utc для столбца DateTime без часового пояса, удалить лишние символы из строк или нормализовать их, а также дополнительно обработать JSON-столбец перед передачей в прикладной код. Чтобы использовать преобразователь при каждом чтении, задайте ReadValueConverter в ClickHouseClientSettings. Преобразователь вызывается для каждого значения столбца в каждой строке — как при получении значения в упакованном виде (GetValue), так и через обобщённый метод (GetFieldValue<T>). Если преобразователь не задан, дополнительных накладных расходов нет: считыватель возвращает значения напрямую. Использование DictionaryReadValueConverter для простого преобразования значений в зависимости от типа CLR:
Значения, CLR-тип которых во время выполнения не зарегистрирован через For<T>, передаются без изменений. Преобразователь выбирается по точному совпадению CLR-типа, поэтому зарегистрируйте тип, который фактически возвращает считыватель (например, For<JsonObject> для JSON-столбца в режиме JsonReadMode.Binary). Пользовательская реализация IReadValueConverter для сложных сценариев: Если преобразователь нужно выбирать по строке типа ClickHouse (например, чтобы различать DateTime и DateTime('UTC'), которые представлены одним и тем же CLR-типом), реализуйте интерфейс IReadValueConverter напрямую:
Преобразователь должен сохранять тип CLR, определяемый во время выполнения; метаданные столбца (GetFieldType, GetSchemaTable) через него не обрабатываются и должны соответствовать возвращаемому значению. Преобразователь можно также задать для отдельного запроса через QueryOptions.ReadValueConverter. В этом случае он имеет приоритет над преобразователем на уровне клиента. Граница применения: Преобразователь вызывается один раз для каждого столбца и получает десериализованное значение ячейки целиком; составные контейнеры он не обрабатывает рекурсивно. Для столбца Array(Int32) ему передаётся int[], а для Tuple(Int32, String) — ITuple. Какая перегрузка вызывается: Обе перегрузки должны работать согласованно: какую из них вызовет драйвер, зависит от того, как вызывающий код считывает столбец:
  • ConvertValue<T> — типизированные аксессоры GetByte, GetSByte, GetInt16/32/64, GetUInt16/32/64, GetFloat, GetDouble, GetGuid, GetDateTime, GetIPAddress, GetBigInteger и GetFieldValue<T>, а также все столбцы, считываемые без упаковки значений через механизм чтения POCO.
  • ConvertValue (с упаковкой значения) — GetValue, GetValues, индексаторы, GetChar, GetTuple и пути чтения с приведением типов в GetBoolean, GetDecimal и GetString.
IsDBNull вообще не вызывает преобразователь: он считывает признак NULL напрямую, поэтому преобразователь не может повлиять на то, считается ли значение NULL. TryGetEnumOrdinal тоже не использует преобразователь — см. чтение порядкового номера значения enum. Преобразователь работает и при использовании ADO.NET через ClickHouseConnection: соединения, созданные клиентом, наследуют его настройки.

Потоковая передача необработанных данных

Используйте ExecuteRawResultAsync для прямой потоковой передачи результатов запроса в заданном формате, минуя считыватель данных. Это удобно для экспорта данных в файлы или их передачи в другие системы:
Распространённые форматы: JSONEachRow, CSV, TSV, Parquet, Native. Полный список доступных форматов см. в документации по форматам.

Сжатие при передаче данных для отдельного запроса

По умолчанию при Compression=true (значение по умолчанию в строке соединения) клиент согласовывает zstd, lz4, gzip, deflate и автоматически декодирует поток. При экспорте данных в исходном виде (например, в форматах Parquet, Arrow, Native) можно выбрать другой кодек (например, zstd или lz4), чтобы сбалансировать нагрузку на CPU и потребление пропускной способности, не меняя настройку для всего соединения. QueryOptions.AcceptEncoding и ClickHouseCommand.AcceptEncoding задают HTTP-заголовок Accept-Encoding для отдельного запроса вместо значения по умолчанию и принудительно добавляют enable_http_compression=1 к URL (без этого ClickHouse не учитывает Accept-Encoding).

Настройка HttpClient

Дополнительная настройка не требуется: у HttpClient, который создаёт драйвер, AutomaticDecompression остаётся равным DecompressionMethods.None, а ответы драйвер декодирует сам. Поэтому Content-Encoding не удаляется без вашего ведома, а исходное тело ответа поступает к вам именно в том виде, в каком его отправил сервер.
Если вы используете собственный HttpClient, также оставьте AutomaticDecompression отключённым. Этот параметр влияет не только на обработку ответов: при отправке обработчик добавляет в исходящий Accept-Encoding все алгоритмы из своей маски, которых там ещё нет. Например, обработчик с маской GZip | Deflate превращает явно заданное AcceptEncoding = "lz4" в lz4, gzip, deflate, а явно заданное "identity" — в identity, gzip, deflate при передаче. ClickHouse выбирает кодек по собственному фиксированному порядку предпочтения, игнорируя порядок перечисления и значения q, поэтому может ответить с использованием кодека, который вы не запрашивали. Затем обработчик декодирует ответ и удалит заголовок — и вы даже не узнаете, что это произошло. Если не задавать алгоритмы в маске, список предлагаемых кодеков останется именно таким, каким вы его указали.
Если AcceptEncoding запрашивает кодек, который драйвер не умеет декодировать (snappy), безопасно использовать только ExecuteRawResultAsync. Вызовы ExecuteReaderAsync, ExecuteScalarAsync и ExecuteNonQueryAsync завершаются исключением NotSupportedException с указанием кодека (раньше они пытались разобрать сжатые байты как данные в формате результата и выдавали некорректные данные).

Тела ответов с ошибками

Если сервер возвращает ответ с кодом 4xx/5xx и задан параметр enable_http_compression=1, он сжимает тело ответа с ошибкой тем же кодеком, который использовал бы для успешного ответа. Драйвер декодирует такие ответы, если они сжаты одним из поддерживаемых кодеков (lz4, zstd, gzip, deflate, br/brotli), поэтому сообщение в ClickHouseServerException остаётся читаемым. Для остальных кодеков (snappy и др.) драйвер возвращает плейсхолдер с названием кодека и указанием на system.query_log, где можно найти исходный текст ошибки.

Распаковка ответа

Заголовок Accept-Encoding лишь запрашивает у сервера сжатый ответ — полученный ответ ещё нужно распаковать. Драйвер делает это сам на основе заголовка Content-Encoding в ответе, поэтому все стандартные API чтения (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) работают со сжатыми ответами без дополнительной настройки. Драйвер распаковывает данные в форматах lz4, zstd, gzip, deflate и br; snappy не поддерживается. По умолчанию драйвер указывает zstd, lz4, gzip, deflate, а ClickHouse возвращает ответ в формате zstd. Чтобы выбрать другой формат, задайте Accept-Encoding самостоятельно — для всего клиента:
для отдельного запроса — эта настройка имеет приоритет:
или в строке подключения, если вы используете ORM и не обращаетесь напрямую к ClickHouseClientSettings:
При указании кодека в URL также принудительно добавляется enable_http_compression=1: без этого ClickHouse не учитывает заголовок. Это происходит даже при UseCompression=false, поскольку явное указание кодека считается запросом на его использование. Если значение не задано, при UseCompression=false заголовок Accept-Encoding не отправляется. Accept-Encoding можно задать в четырёх местах. Приоритет имеет первое из них, в котором указан кодек:
  1. QueryOptions.AcceptEncoding (или ClickHouseCommand.AcceptEncoding)
  2. CustomHeaders["Accept-Encoding"] для запроса
  3. CustomHeaders["Accept-Encoding"] для клиента
  4. ClickHouseClientSettings.AcceptEncoding или ключевое слово AcceptEncoding в строке соединения
Если кодек не указан ни в одном из них, драйвер отправляет свой список по умолчанию. Значение, не содержащее названия кодека (null, пустая строка, пробельные символы или только запятые), считается незаданным, и проверяется следующее место. Чтобы отключить сжатие, используйте identity. Кодек выбирает сервер, а не клиент. ClickHouse проверяет токены в Accept-Encoding в собственном фиксированном порядке предпочтения — zstd > br > lz4 > snappy > gzip > deflate — и игнорирует как порядок их перечисления, так и значения q. Таким образом, заголовок сообщает о поддерживаемых кодеках, а не требует использовать конкретный; повлиять на выбор можно только исключив некоторые токены. Список по умолчанию включает zstd, поэтому ответ на запрос с настройками по умолчанию сжимается с помощью zstd; остальные токены служат запасными вариантами. Клиент умеет декодировать br, но по умолчанию не объявляет о его поддержке. Соотношение кодеков по размеру полезной нагрузки, нагрузке на CPU сервера и CPU клиента зависит от ваших данных, скорости соединения и серверного параметра http_zlib_compression_level (значение по умолчанию: 3) — см. Настройка сжатия.
  • http_zlib_compression_level. Этот параметр применяется ко всем кодекам HTTP; его значение по умолчанию — 3. Подбирайте значение с учётом данных, скорости соединения и нагрузки на CPU.
  • Клиент, производительность которого ограничена CPU, при быстром соединении. Драйвер декодирует тело ответа в вызывающем потоке, поэтому, если сеть не является узким местом, скорость декодирования на стороне клиента может ограничивать производительность.
Если применимо что-либо из перечисленного, запрашивайте другой кодек для отдельного запроса или для клиента в целом:
Поскольку решение принимается по ответу, тело декодируется в соответствии с его заголовком Content-Encoding независимо от того, что запрашивалось: если заголовок отсутствует или имеет значение identity, тело остаётся без изменений; если указан поддерживаемый кодек — декодируется; любое другое значение вызывает ошибку, в которой оно указано. Повторное декодирование исключено: если AutomaticDecompression в обработчике, переданном вызывающим кодом, уже декодировал тело, обработчик также удаляет заголовок Content-Encoding. Поэтому драйвер получает данные в исходном виде и не декодирует их снова. Для необработанных результатов кодек не запрашивается. ExecuteRawResultAsync (как и общедоступные PostStreamAsync / InsertRawStreamAsync) передают вам тело ответа без изменений. Поэтому, если вы сами не укажете кодек, они не запросят сжатие: драйвер не декодирует такое тело, и запрошенный кодек незаметно превратил бы экспортируемые данные в сжатый файл. Правило простое и не зависит от настройки HttpClient: тело ответа поступает в точности таким, каким его отправил сервер, а сервер отправляет данные без сжатия, если вы не запросили кодек. Чтобы экспортировать сжатые данные, укажите кодек для всего клиента или отдельного запроса. Явно заданный AcceptEncoding (на любом из этих уровней) действует и для запросов с необработанным результатом. Если результат нужно декодировать, используйте ClickHouseRawResult.ReadDecompressedStreamAsync(). Методы ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync и CopyToAsync всегда возвращают байты в том виде, в каком они поступили.
Как и в примере выше, полностью прочитайте возвращённый поток, прежде чем он выйдет из области видимости. Если ответ сжат, возвращается декодер, созданный с leaveOpen: его освобождение не затрагивает ответ. Если ответ не сжат, возвращается непосредственно поток HTTP-содержимого, и его освобождение закрывает тело ответа. В обоих случаях ответом владеет ClickHouseRawResult — после освобождения потока не вызывайте другие методы чтения этого объекта. ClickHouseRawResult необходимо освобождать в любом случае, и одного этого достаточно: вместе с ним освобождаются ответ и созданный здесь декодер (декодеры используют буферы из пула). Поэтому конструкция await using выше необязательна, но её можно оставить. При повторных последовательных вызовах возвращается один и тот же поток; этот тип не предназначен для параллельного использования. Рабочий пример см. в Select_007_ResponseCompression.cs.

Сжатие запросов на вставку

По умолчанию для вставки используется кодек Zstd: начальное значение InsertOptions.Compressor — ZstdCompressor.Default, то есть zstd с уровнем сжатия 3. Чтобы изменить кодек, укажите другой алгоритм сжатия. Чтобы отправлять тело запроса без сжатия, укажите null.
В драйвер входят четыре кодека. У каждого есть экземпляр Default и конструктор, принимающий уровень сжатия и размер буфера записи:
Используйте общие инстансы компрессоров. Каждый Default — отдельный общий инстанс. Все четыре компрессора можно безопасно использовать одновременно в нескольких потоках. Именно так и происходит, когда InsertOptions.MaxDegreeOfParallelism больше 1: при вставке один и тот же компрессор используется для каждого батча. Ни один из компрессоров не реализует IDisposable. Создайте собственный инстанс один раз и используйте его повторно — так же, как используется Default.
IClickHouseCompressor — публичный интерфейс, реализация которого должна содержать всего два члена:
Сервер должен поддерживать указанный вами Content-Encoding. У остальных членов — Decompress, MethodByte, MaxEncodedLength, Encode и Decode — есть реализации по умолчанию, которые генерируют NotSupportedException, поэтому переопределяйте только те, которые нужны вашему кодеку. Реализуйте Decompress для декодирования тел ответов, если сжимаете запросы. Если тело повреждено или имеет неверный формат, возвращаемый этим методом поток должен генерировать InvalidDataException. InsertOptions.Compressor управляет только бинарной вставкой. Остальные тела запросов драйвера сжимаются по другим правилам и через него не проходят:
  • Каждый запрос с текстом SQL (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, ExecuteRawResultAsync, слой ADO.NET) отправляет оператор с Content-Encoding: gzip, если UseCompression имеет значение true, то есть по умолчанию. Настроить кодек нельзя: AcceptEncoding влияет только на ответ, поэтому варианты — gzip или отсутствие сжатия. При Compression=false оператор отправляется без сжатия. Операторы невелики, и обычно это не имеет значения, но об этом стоит помнить при анализе запросов через прокси или захват пакетов.
  • Тело multipart-запроса — запроса, параметры которого передаются как данные формы (UseFormDataParameters=true), — всегда отправляется без сжатия независимо от значения UseCompression.
  • Загрузка потока необработанных данных (InsertRawStreamAsync, PostStreamAsync) управляется отдельным флагом для каждого вызова и не зависит ни от UseCompression, ни от InsertOptions.Compressor: если флаг установлен, применяется gzip, иначе данные передаются без сжатия. Обратите внимание: параметр useCompression метода InsertRawStreamAsync по умолчанию имеет значение true. Поэтому при такой загрузке применяется gzip, если явно не передать false, — даже если для клиента задано Compression=false.

Настройка сжатия

Сжатие уменьшает объём передаваемых данных, но увеличивает нагрузку на CPU. Оправданно ли оно, почти полностью зависит от того, насколько быстро работает соединение по сравнению с кодеком. Универсальной настройки нет.

Ключевая цифра

Сжатие имеет смысл, пока кодек работает быстрее, чем сеть передаёт данные. При чтении этот порог ниже, чем многие ожидают: ClickHouse сжимает HTTP-ответы в выходном буфере в одном потоке. По результатам замеров на сервисе ClickHouse Cloud с 16 vCPU (hits, RowBinary, уровень 3) сервер выдаёт сжатые данные со скоростью около 100–200 МБ/с. Таким образом, если результат большой и одновременно обрабатывается только один запрос, сжатие перестаёт окупаться при скорости передачи около 100 МБ/с. Скорость одного HTTPS-потока в пределах одного облачного региона нередко выше, а при передаче через общедоступный интернет, VPN или между регионами — обычно ниже. При вставке сжатие остаётся полезным и на более быстрых соединениях: клиент выполняет его на отдельном ядре и обычно сжимает данные быстрее, чем сервер — ответы.

Ориентировочные рекомендации в зависимости от размещения

Таблица не учитывает три фактора:
  • Стоимость исходящего трафика: если передача данных тарифицируется, важна не только задержка, но и стоимость каждого байта, поэтому более сильное сжатие может быть выгодным независимо от скорости соединения.
  • Небольшие результаты: всё сказанное выше относится к большим объёмам данных. Для небольших ответов выбор кодека почти не имеет значения: преобладают накладные расходы на каждый запрос.
  • Параллельные вставки сдвигают пороговые значения вверх. Все приведённые выше показатели пропускной способности относятся к одному потоку. По умолчанию InsertOptions.MaxDegreeOfParallelism равен 1, но при увеличении этого значения батчи сжимаются параллельно, поэтому суммарная скорость кодирования на стороне клиента растёт примерно пропорционально числу выделенных ядер. Поэтому даже при высокой скорости соединения сжимать данные при параллельной вставке может быть выгодно там, где при однопоточной вставке сжатие уже не окупается. Рассматривайте значения в строках таблицы, относящихся к вставке, как нижнюю границу. Если вы уже обрабатываете батчи параллельно, повторите тесты, прежде чем решать, что соединение слишком быстрое для сжатия.
При чтении распараллеливание возможно только между несколькими запросами.

Выбор кодека

Уровни

Сжатием ответов управляет одна настройка сервера — http_zlib_compression_level. Она действует для всех кодеков HTTP, а не только для zlib. По умолчанию её значение — 3. Не меняйте её, если измерения не показали, что это необходимо. При значениях выше 3 выигрыш в размере невелик, а нагрузка на CPU существенно возрастает (для zstd повышение уровня с 3 до 6 примерно удваивает нагрузку на CPU сервера, уменьшая объём данных лишь на ~14 %). Для br затраты ресурсов становятся неприемлемыми. На уровне 1 ситуация действительно меняется: lz4 требует гораздо меньше ресурсов, а zstd теряет преимущество перед ним по нагрузке на CPU. При необходимости задайте уровень для отдельного запроса:

Определение порога эффективности сжатия

Самый быстрый способ подобрать кодек и уровень сжатия — измерить время выполнения одного и того же запроса с несколькими кодеками и сравнить результаты.
Чтобы увидеть ту же картину со стороны сервера, считайте ProfileEvents из system.query_log — задайте QueryOptions.QueryId, чтобы найти нужную строку:
Если вы проводите бенчмарк самостоятельно, учтите: простой LIMIT n без ORDER BY при каждом запуске возвращает разные строки, поэтому каждый раз сжимаются разные данные, а сравнение коэффициентов сжатия теряет смысл. Используйте для сравнения один и тот же результирующий набор.

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

Используйте InsertRawStreamAsync, чтобы вставлять данные напрямую из файловых потоков или потоков в памяти. Поддерживаются CSV, JSON, Parquet и другие форматы ClickHouse. Вставка данных из CSV-файла:
Драйвер берёт на себя управление жизненным циклом потока. InsertRawStreamAsync и PostStreamAsync освобождают переданный им поток после завершения запроса — как успешного, так и неуспешного. Не освобождайте поток самостоятельно и не используйте его повторно. Именно поэтому в примере выше FileStream не заключён в блок using.Если вы добавите using, он вызовет Dispose уже после того, как драйвер освободит поток. Для FileStream и MemoryStream повторный вызов безвреден. Но если Dispose возвращает буфер в пул или уменьшает счётчик ссылок, ресурс будет освобождён дважды.Драйвер принимает на себя управление потоком только после проверки аргументов: если вызов генерирует ArgumentException или ArgumentNullException из-за отсутствия таблицы, потока или формата, поток по-прежнему остаётся в вашем распоряжении.
Параметры управления ингестией данных описаны в документации по настройкам форматов.

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

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

ADO.NET

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

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

Всегда создавайте соединения через ClickHouseDataSource, чтобы обеспечить корректное управление их жизненным циклом и использование пула соединений. DataSource содержит один экземпляр ClickHouseClient, пул HTTP-соединений которого используется всеми соединениями.
Для внедрения зависимостей:
Не создавайте ClickHouseConnection напрямую в продакшн-коде. При каждом таком создании инициализируются новый HTTP-клиент и пул соединений, что под нагрузкой может привести к исчерпанию доступных сокетов:
Вместо этого всегда используйте ClickHouseDataSource или один общий экземпляр ClickHouseClient.

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

Создавайте команды через соединение, чтобы выполнять SQL-запросы:
Методы выполнения команд:
  • ExecuteNonQueryAsync() - Для выполнения операторов INSERT, UPDATE, DELETE и DDL
  • ExecuteScalarAsync() - Возвращает значение первого столбца первой строки
  • ExecuteReaderAsync() - Возвращает ClickHouseDataReader для перебора результатов

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

ClickHouseDataReader обеспечивает типизированный доступ к результатам запроса:

Чтение числового значения enum

Столбец Enum8 или Enum16 возвращается в виде метки: GetFieldType возвращает string, а GetString, GetValue и GetFieldValue<string> — саму метку. Числовые аксессоры при обращении к такому столбцу генерируют InvalidCastException, поскольку его значение хранится как строка. Чтобы получить число, соответствующее метке, используйте TryGetEnumOrdinal:
Для столбца типа Enum8/Enum16, а также для столбца типа Nullable(Enum...), если ячейка не содержит NULL, метод возвращает true и записывает порядковый номер в value. Если ячейка содержит NULL или столбец не относится к перечислимому типу, метод возвращает false и записывает в value значение 0. Порядковый номер — это знаковое значение из двоичного представления данных при передаче, поэтому оно может быть отрицательным, а порядковый номер Enum16 может не помещаться в один байт.

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

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

ClickHouse.Driver использует System.Net.Http.HttpClient. HttpClient поддерживает отдельный пул соединений для каждой конечной точки. Поэтому:
  • Сеансы базы данных передаются через HTTP-соединения, которыми управляет пул соединений.
  • Пул автоматически обновляет HTTP-соединения.
  • Соединения могут оставаться открытыми после освобождения объектов ClickHouseClient или ClickHouseConnection.
Рекомендуемые подходы:
Если вы используете собственный HttpClient или HttpClientFactory, убедитесь, что значение PooledConnectionIdleTimeout меньше значения keep_alive_timeout на сервере. Это позволит избежать ошибок из-за полузакрытых соединений. Для развёртываний в Cloud значение keep_alive_timeout по умолчанию составляет 10 секунд.
Не создавайте несколько экземпляров ClickHouseClient или отдельные экземпляры ClickHouseConnection без общего HttpClient: каждый из них создаёт собственный пул соединений.

Работа с DateTime

  1. По возможности используйте UTC. Храните временные метки в столбцах DateTime('UTC') и используйте DateTimeKind.Utc в коде. Это устраняет неоднозначность часовых поясов.
  2. Используйте DateTimeOffset для явного учёта часового пояса. Этот тип всегда обозначает конкретный момент времени и содержит смещение относительно UTC.
  3. Указывайте часовой пояс в подсказках типов SQL. Если вы передаёте значения DateTime с Unspecified в качестве параметров для столбцов с часовым поясом, отличным от UTC, укажите часовой пояс в SQL:

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

Асинхронные вставки перекладывают группировку данных в пакеты с клиента на сервер. Клиенту больше не нужно группировать данные: сервер буферизует поступающие данные и сбрасывает их в хранилище при достижении настраиваемых пороговых значений. Это полезно при высокой степени параллелизма, например для задач обсервабилити, когда множество агентов отправляют небольшие порции данных. Включите асинхронные вставки через CustomSettings или строку подключения:
Два режима (определяются параметром wait_for_async_insert):
При wait_for_async_insert=0 ошибки проявляются только во время сброса данных на диск, поэтому их нельзя связать с исходной операцией вставки. Кроме того, клиент не замедляет отправку данных при перегрузке сервера, что может усугубить её.
Основные настройки:

Сеансы

Включайте сеансы только тогда, когда вам нужны серверные возможности с сохранением состояния, например:
  • Временные таблицы (CREATE TEMPORARY TABLE)
  • Сохранение контекста запроса при выполнении нескольких команд
  • Настройки на уровне сеанса (SET max_threads = 4)
При включённых сеансах запросы обрабатываются последовательно, чтобы исключить одновременное использование одного сеанса. Это создаёт дополнительные накладные расходы для рабочих нагрузок, которым не требуется сохранять состояние сеанса.
Использование ADO.NET (для совместимости с ORM):

Поддерживаемые типы данных

ClickHouse.Driver поддерживает все типы данных ClickHouse. В таблицах ниже показано, каким типам .NET соответствуют типы ClickHouse при чтении данных из базы данных.

Сопоставление типов при чтении из ClickHouse

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


Типы с плавающей запятой


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

Настройка UseCustomDecimals определяет, как преобразуются десятичные типы.

Булевый тип


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

По умолчанию значения столбцов типов String и FixedString(N) возвращаются как string. Чтобы получать их как byte[], укажите ReadStringsAsByteArrays=true в строке подключения. Это полезно для хранения двоичных данных, которые могут не быть корректным текстом в кодировке UTF-8.Настройка действует и для строк внутри других типов: Array(String) считывается как byte[][], а Map(String, String) — как Dictionary<byte[], byte[]>, в том числе для ключей. Исключение составляет JSON-столбец: строковые значения на конечных уровнях его структуры всегда считываются как текст; см. раздел Тип JSON.

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

ClickHouse хранит значения DateTime и DateTime64 в виде Unix-временных меток (числа секунд или долей секунды с начала эпохи Unix). Данные всегда хранятся в UTC, но у столбца может быть задан часовой пояс, который влияет на отображение и интерпретацию значений. При чтении значений DateTime свойство DateTime.Kind устанавливается в зависимости от часового пояса столбца: Для столбцов с часовым поясом, отличным от UTC, возвращаемое значение DateTime отражает время в указанном часовом поясе. Чтобы получить значение DateTimeOffset с соответствующим смещением, используйте ClickHouseDataReader.GetDateTimeOffset():
Для столбцов без явно указанного часового пояса (то есть с типом DateTime, а не DateTime('Europe/Amsterdam')) драйвер возвращает значение DateTime с Kind=Unspecified. Так сохраняется исходное локальное время без каких-либо предположений о часовом поясе. Если при работе со столбцами без явно указанного часового пояса необходимо учитывать часовой пояс, выполните одно из следующих действий:
  1. Укажите часовой пояс в определении столбца: DateTime('UTC') или DateTime('Europe/Amsterdam')
  2. Задайте часовой пояс самостоятельно после чтения.

Тип JSON

Тип возвращаемого значения для JSON-столбцов зависит от настройки JsonReadMode:
  • Binary (по умолчанию): Возвращает System.Text.Json.Nodes.JsonObject. Позволяет работать со структурой JSON-данных, но некоторые типы ClickHouse (например, IP-адреса, UUID и десятичные числа высокой точности) внутри JSON преобразуются в строковое представление.
  • String: Возвращает JSON без обработки в виде string. Сохраняет JSON в точности в том виде, в каком он получен из ClickHouse. Это удобно, если нужно передать JSON дальше без разбора или самостоятельно выполнить десериализацию.
None — третий режим. Он считывает данные так же, как Binary, но не передаёт настройку сервера вместе с запросом. Используйте его, если для соединения запрещено задавать эту настройку. Путь, объявленный в типе столбца, — это типизированный путь; любой другой путь в документе — динамический путь. Разница между ними проявляется, когда значение равно null. Типизированный путь всегда присутствует в JsonObject. Если он объявлен с типом Nullable(T) или Dynamic, ему соответствует значение null в JSON как в случае, когда сохранённое значение равно null, так и в случае, когда такого пути нет в документе. Различить эти случаи невозможно:
Если путь объявлен с типом, не допускающим NULL, то при отсутствии пути используется значение этого типа по умолчанию: JSON(x String) даёт {"x":""}, а JSON(x Int64) — {"x":0}. Динамический путь со значением null полностью исключается из объекта, поэтому ContainsKey возвращает для него false. При чтении {"x":null} из обычного JSON-столбца получается {}. Вложенные типизированные пути создают родительские узлы, поэтому JSON(a.b Nullable(Int64)) даёт {"a":{"b":null}} даже для пустого документа.
Именно так объект отображает сам сервер, поэтому режимы Binary и String теперь дают одинаковый результат. До версии 1.4.0 типизированный путь со значением null удалялся из JsonObject, из-за чего {"x":null} при чтении превращался в {}. Для вложенного пути, например JSON(a.b Nullable(Int64)), исчезало всё поддерево a.
Строковые значения в конечных узлах JSON-столбца всегда возвращаются как текст, независимо от значения ReadStringsAsByteArrays: у JsonValue нет представления в виде массива байтов, поэтому byte[] отображался бы в кодировке base64. Это относится к String, FixedString, типам, обёрнутым в LowCardinality, Nullable или SimpleAggregateFunction, а также к строкам внутри Array и Map, включая ключи карт.
Если считыватель JSON не может определить тип массива байтов, тот всё равно отображается в кодировке base64: путь типа Variant или Dynamic содержит значение, тип которого определяется отдельно для каждой строки, поэтому строка в Variant(Array(UInt8), String) возвращается в кодировке base64. Это справедливо при обоих значениях настройки.Если тип ключа карты JSON не совпадает в точности с String — например, это Map(LowCardinality(String), String) — возникает NotSupportedException.
ClickHouse допускает объявление столбца, в котором один путь служит и значением, и родительским путём для другого, например JSON(a Int64, a.b Int64). Оба пути присутствуют в каждой строке, поэтому сервер формирует строку с повторяющимся ключом: {"a":0,"a":{"b":7}}. JsonObject не может содержать два значения для одного ключа, поэтому JsonReadMode.Binary генерирует SerializationException с указанием обоих путей. То же происходит, если значение имеет тип Map, как в JSON(a Map(String, Int64)), а в считываемой строке также есть динамический путь a.b. Это относится только к строкам, в которых оба пути содержат значения. Если в одном из них нет значения — null, пустой объект или поддерево, все значения которого равны null, — сохраняется значение другого пути независимо от того, какой из них сервер отправляет первым. Поэтому при пересечении путей с типами Nullable в каждой строке значение берётся только из одного пути, и чтение проходит без ошибки: JSON(a Nullable(Int64), a.b Nullable(Int64)) даёт {"a":5} и {"a":{"b":7}}, как и ожидается. Чтобы получить JSON-текст сервера без изменений, включая повторяющийся ключ, считывайте такой столбец в режиме JsonReadMode.String. Установите AllowDuplicateJsonKeys, чтобы считывать столбец как JsonObject без исключения. В этом случае драйвер сохраняет последнее из двух значений в строке и отбрасывает другое, поэтому часть данных теряется: JSON(a Int64, a.b Int64) со значением {"a.b":7} считывается как {"a":0}. Если путь содержит значение, а его родительский путь — скалярное значение или массив, исключение всё равно возникает: поддерево нельзя поместить ни в скалярное значение, ни в массив.

Тип Map

Тип Map(K, V) в ClickHouse физически хранится как Array(Tuple(K, V)) и может содержать несколько пар с одинаковым ключом. Dictionary этого не допускает, поэтому в режиме по умолчанию для каждого ключа сохраняется только последнее значение, а предыдущие пары отбрасываются. Настройка MapReadMode определяет, в каком виде возвращаются данные:
  • Dictionary (по умолчанию): возвращает Dictionary<K, V>.
  • KeyValuePairs: возвращает List<KeyValuePair<K, V>> в порядке отправки пар сервером. При этом сохраняются все пары, в том числе с повторяющимися ключами.
Режим определяет, каким типом .NET представлен столбец Map. Он также влияет на GetFieldValue<T>, типы схемы, возвращаемые драйвером, и сопоставление свойств POCO. Режим действует на любом уровне дерева типов столбца, где встречается Map, в том числе в Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) и Dynamic. В любом режиме при записи принимаются оба представления — см. раздел «Запись значений Map».

Другие типы

Значения типов Dynamic и Variant в каждой строке преобразуются в тип, соответствующий фактическому типу хранимого значения.

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

Geometry — это тип Variant, который может хранить значения любого геометрического типа. При чтении значение преобразуется в соответствующий тип.

Соответствие типов при записи в ClickHouse

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

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


Типы с плавающей запятой


Булевый тип


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


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

Значения вне допустимого диапазонаПри записи в бинарном формате значения Date, Date32, DateTime и DateTime32 вне поддерживаемого диапазона приводят к исключению ArgumentOutOfRangeException при вызове Write. В сообщении указываются тип столбца и поддерживаемый диапазон. Ранее такие значения могли незаметно усекаться при преобразовании в 32-битное целое число, а затем переинтерпретироваться сервером. В результате получались корректные по формату, но неверные временные метки.
При записи значений драйвер учитывает DateTime.Kind: Значения DateTimeOffset всегда сохраняют точный момент времени. Пример: DateTime в UTC (точный момент времени сохраняется)
Пример: дата и время без указания часового пояса (местное время)
Рекомендация: чтобы работа с датой и временем была максимально простой и предсказуемой, всегда используйте DateTimeKind.Utc или DateTimeOffset. Тогда код будет работать одинаково независимо от часового пояса сервера, клиента или столбца.

Параметры HTTP и пакетная загрузка

При записи значений DateTime с типом Unspecified привязка параметров HTTP и пакетная загрузка отличаются в одном важном отношении: Пакетная загрузка учитывает часовой пояс целевого столбца и правильно интерпретирует в нём значения Unspecified. Параметры HTTP не учитывают часовой пояс столбца автоматически. Его необходимо указать в подсказке типа SQL:

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


Тип JSON

Поведение при записи JSON определяется настройкой JsonWriteMode:
  • String (по умолчанию): Принимает string, JsonObject, JsonNode или любой объект. Все входные данные сериализуются с помощью System.Text.Json.JsonSerializer и передаются как JSON-строки для разбора на стороне сервера. Это наиболее гибкий режим: он не требует регистрации типов.
  • Binary: Принимает только зарегистрированные типы POCO. Данные преобразуются на стороне клиента в двоичный JSON-формат ClickHouse с полной поддержкой подсказок типов. Перед использованием необходимо вызвать connection.RegisterJsonSerializationType<T>(). При записи значений string или JsonNode в этом режиме генерируется ArgumentException.
Если для JSON-столбца заданы подсказки типов (например, JSON(id UInt64, price Decimal128(2))), драйвер использует их при сериализации, чтобы точно сохранить типы значений. Это позволяет избежать потери точности для таких типов, как UInt64, Decimal, UUID и DateTime64, при сериализации в обычный JSON. Объекты POCO можно записывать в JSON-столбцы двумя способами в зависимости от значения JsonWriteMode: Строковый режим (по умолчанию): объекты POCO сериализуются с помощью System.Text.Json.JsonSerializer. Регистрировать типы не нужно. Это самый простой способ, который подходит и для анонимных объектов. Двоичный режим: объекты POCO сериализуются в двоичный JSON-формат драйвера с полной поддержкой подсказок типов. Перед использованием типы необходимо зарегистрировать с помощью connection.RegisterJsonSerializationType<T>(). В этом режиме атрибуты позволяют сопоставлять свойства с произвольными путями:
  • [ClickHouseJsonPath("path")]: сопоставляет свойство с заданным JSON-путём. Это полезно для вложенных структур или когда имя свойства не совпадает с нужным ключом JSON. Работает только в двоичном режиме.
  • [ClickHouseJsonIgnore]: исключает свойство из сериализации. Работает только в двоичном режиме.
При сопоставлении имён свойств с подсказками типов столбца учитывается регистр. Свойство UserId будет сопоставлено только с подсказкой UserId, но не с userid. Это соответствует поведению ClickHouse: пути userName и UserName могут существовать как разные поля. Ограничения (только для режима Binary mode):
  • Перед сериализацией типы POCO необходимо зарегистрировать для соединения с помощью connection.RegisterJsonSerializationType<T>(). Попытка сериализовать незарегистрированный тип приводит к исключению ClickHouseJsonSerializationException.
  • Для корректной сериализации свойств, содержащих словари, массивы или списки, в определении столбца необходимо указать подсказки типов. Если подсказок нет, используйте режим String mode.
  • Значения null в свойствах POCO записываются только в том случае, если для соответствующего пути в определении столбца указана подсказка типа Nullable(T). ClickHouse не допускает типы Nullable внутри динамических JSON-путей, поэтому свойства со значением null без подсказок пропускаются.
  • Атрибуты ClickHouseJsonPath и ClickHouseJsonIgnore игнорируются в режиме String mode (они работают только в режиме Binary mode).

Другие типы


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


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


Работа с типом Nested

Типы Nested в ClickHouse (Nested(...)) можно читать и записывать с использованием семантики массивов.

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

Клиент ClickHouse для .NET поддерживает абстракции Microsoft.Extensions.Logging для легковесного логирования, которое можно включить при необходимости. Если логирование включено, драйвер формирует структурированные сообщения о событиях жизненного цикла соединения, выполнении команд, транспортных операциях и операциях массовой вставки. Логирование необязательно: если в приложении не настроен логгер, оно продолжает работать без дополнительных накладных расходов.

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

Использование appsettings.json

Уровни логирования можно настроить стандартными средствами конфигурации .NET:

Использование конфигурации в оперативной памяти

Уровень детализации логирования для каждой категории можно также настроить в коде:

Категории и источники сообщений

Драйвер использует отдельные категории, позволяющие гибко настраивать уровень лога для каждого компонента:

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

В журнал будут записываться сведения о следующем:
  • Выбор фабрики HTTP-клиентов (пул по умолчанию или одно соединение)
  • Конфигурация обработчика HTTP-запросов (SocketsHttpHandler или HttpClientHandler)
  • Настройки пула соединений (MaxConnectionsPerServer, PooledConnectionLifetime и т. д.)
  • Настройки тайм-аутов (ConnectTimeout, Expect100ContinueTimeout и т. д.)
  • Настройка SSL/TLS
  • События открытия и закрытия соединений
  • Отслеживание ID сеанса

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

Для диагностики сетевых проблем библиотека драйвера содержит вспомогательный компонент, который включает низкоуровневую трассировку работы сетевых подсистем .NET. Для этого передайте LoggerFactory с уровнем Trace и установите для EnableDebugMode значение true (или включите трассировку вручную через класс ClickHouse.Driver.Diagnostic.TraceHelper). События будут записываться в журнал в категории ClickHouse.Driver.NetTrace. Предупреждение: журналы станут чрезвычайно подробными, что снизит производительность. Включать режим отладки в продакшне не рекомендуется.

OpenTelemetry

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

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

В приложениях ASP.NET Core добавьте ActivitySource драйвера ClickHouse в конфигурацию OpenTelemetry:
Для консольных приложений, тестирования или настройки вручную:

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

Каждый спан содержит стандартные атрибуты OpenTelemetry для баз данных, а также статистику запросов ClickHouse, полезную при диагностике неполадок.

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

Настройте поведение трассировки с помощью ClickHouseDiagnosticsOptions:
При включении IncludeSqlInActivityTags конфиденциальные данные могут попасть в трассировку. Используйте эту настройку с осторожностью в продакшне.

Настройка TLS

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

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

Если для продакшна требуется особая логика проверки сертификата, передайте собственный HttpClient с настроенным обработчиком ServerCertificateCustomValidationCallback:
Что важно учитывать при использовании собственного HttpClient
  • Автоматическая распаковка: не включайте AutomaticDecompression. Драйвер сам распаковывает сжатые ответы, поэтому эта функция не нужна. Более того, при её включении обработчик во время отправки запроса также добавляет в исходящий заголовок Accept-Encoding все алгоритмы из своей маски. Это расширяет список, указанный драйвером, и ClickHouse может ответить, используя кодек, который вы не запрашивали. См. Распаковка ответов.
  • Тайм-аут простоя: задайте для PooledConnectionIdleTimeout значение меньше серверного keep_alive_timeout (10 секунд для ClickHouse Cloud), чтобы избежать ошибок из-за полуоткрытых соединений.

Настройка производительности

В этом разделе описано, как добиться оптимальной производительности клиента и какие параметры можно настроить, чтобы оптимизировать его работу под конкретную задачу.

Кратко о настройке производительности

| Если вы | Рекомендуется | |---|---|---| | Считываете строки в объекты POCO | Используйте QueryAsync<T> вместо MapTo<T> | | Выполняете вставку больших объёмов данных | Увеличьте InsertOptions.BatchSize | | Запускаете консольное приложение или воркер, часто выполняющий вставку данных | Включите Server GC | | Считываете большие объёмы данных по сети | Не отключайте сжатие ответа (оно включено по умолчанию) | | Вставляете данные через быстрое соединение | Попробуйте InsertOptions.Compressor = null | | Многократно вставляете данные в одну и ту же таблицу | Используйте UseSchemaCache или ColumnTypes | | Считываете очень большие объёмы данных | Увеличьте ReadBufferSize |

Чтение: выберите способ материализации

Строку результата можно получить разными способами, и затраты на них различаются. При некоторых способах значения упаковываются в объекты, что увеличивает число выделений памяти и снижает производительность. При чтении 1,000,000 строк и 105 столбцов из набора данных hits:
При использовании типизированных аксессоров ORM задействуют быстрый путь. linq2db регистрирует GetInt64, GetDouble и GetDateTime для каждого столбца и считывает значения без упаковки. Код, который считывает значения через GetValue (в том числе результат типа dynamic из Dapper), упаковывает каждое значение. Если запрос ORM выполняется часто и считывает значения через GetValue, используйте QueryAsync<T> для этого конкретного запроса.

Вставка: размер батча и степень параллелизма

Размер батча сильнее всего влияет на пропускную способность вставки. По умолчанию InsertOptions.BatchSize составляет 100 000 строк. Используйте большие батчи. При вставке 1 000 000 строк увеличение размера батча с 10 000 до 100 000 строк дало следующие результаты: Если вы не можете управлять размером батча (например, когда множество источников независимо друг от друга отправляют небольшие порции строк), используйте асинхронные вставки: сервер сам объединит строки в батчи. Параллельная отправка. По умолчанию InsertOptions.MaxDegreeOfParallelism равен 1. Увеличьте это значение, чтобы отправлять батчи одновременно. Это особенно полезно при включённом сжатии: каждый батч будет сжиматься в отдельном потоке. Сеансы несовместимы с параллельными вставками: либо отключите сеансы, либо оставьте MaxDegreeOfParallelism = 1. Отключите предварительный запрос для определения схемы. Каждый вызов InsertBinaryAsync сначала отправляет запрос SELECT ... WHERE 1=0, чтобы определить типы столбцов. Как избежать этого дополнительного обращения к серверу с помощью ColumnTypes или UseSchemaCache, см. в разделе Отключение предварительного запроса для определения схемы.
Вставка без упаковки значений применяется к формату RowBinary, используемому по умолчанию. При использовании RowBinaryWithDefaults необходимо проверять каждое значение на наличие маркера DBDefault, поэтому вставка выполняется более медленным способом.

Сжатие: результат зависит от направления передачи

Сжатие сокращает объём передаваемых данных за счёт дополнительных затрат CPU. Выгодно ли оно, зависит от направления передачи, пропускной способности соединения с сервером ClickHouse, эффективности выбранного алгоритма сжатия для ваших данных и того, платите ли вы за каждый переданный байт. Чтение: оставьте сжатие включённым, если сервер не работает на том же компьютере. По умолчанию оно включено. По сравнению с передачей без сжатия zstd на уровне 1 показал следующие результаты: Вставка: прежде чем включать сжатие, проведите измерения. Выигрыш может оказаться слишком небольшим, чтобы оправдать его использование. Учитывайте также, что распаковка создаёт дополнительную нагрузку на сервер: для Zstd и LZ4 она невелика, но для других алгоритмов (например, Brotli) может быть высокой. Чтобы отключить сжатие при вставке:
О выборе кодека, уровнях сжатия и определении точки, после которой сжатие становится выгодным в ваших условиях, см. в разделе «Настройка сжатия».

Буферы

ReadBufferSize задаёт размер буфера для чтения HTTP-ответов. По умолчанию он составляет 64 КиБ. Драйвер берёт буфер из общего пула и возвращает его при освобождении считывателя, поэтому выделять память под буфер для каждого запроса не требуется. Увеличьте размер буфера, чтобы сократить число его повторных заполнений при обработке больших результатов. Драйвер использует отдельный буфер для каждого открытого считывателя, поэтому потребление памяти растёт как с размером буфера, так и с числом одновременно работающих считывателей.
Всегда освобождайте считыватели. При освобождении считывателя его буфер возвращается в пул, а HTTP-соединение освобождается. Если оставить считыватель без освобождения, буфер не вернётся в пул, а HTTP-соединение может остаться недоступным; обычная сборка мусора не заменяет освобождение считывателя.

Среда выполнения и сборка мусора

Включите Server GC для приложений, которые часто выполняют вставку данных. При одном и том же коде и одинаковом объёме выделенной памяти вставка данных с Workstation GC выполнялась до 97 % медленнее, чем с Server GC.
В проектах ASP.NET Core этот параметр уже задан. В консольных приложениях, службах-воркерах и большинстве контейнерных образов — нет. Причина — размер бюджета для поколения 0. В режиме Workstation GC он невелик, поэтому кратковременно используемые буферы, создаваемые при вставке, не успевают освободиться в поколении 0. Вместо этого они переходят в поколение 1, из-за чего чаще продвигаются в следующие поколения и значительно увеличивают объём работы сборщика мусора в поколении 2. В одном из сценариев вставки на каждую 1 000 операций приходилось 4 000 сборок мусора в поколении 2 при Server GC и 73 000 при Workstation GC.
Server GC предназначен для повышения пропускной способности, а не для снижения задержек. В тех же измерениях при Server GC суммарное время пауз сократилось более чем вдвое, но отдельные паузы были длиннее (95-й процентиль — 114,6 мс против 61,9 мс). Если для вашего сервиса важна задержка самых медленных запросов, измерьте оба режима, прежде чем сделать выбор.

Задержка: повторное использование соединений

Установление нового TCP-соединения и TLS-рукопожатие занимают значительное время. Повторное использование соединений существенно снижает задержку при выполнении запросов.
  • Не создавайте клиент для каждого запроса. Каждый новый клиент с собственным HttpClient создаёт отдельный пул соединений, поэтому TLS-рукопожатие приходится выполнять заново. Используйте один ClickHouseClient в течение всего времени работы приложения. Он безопасен для использования из нескольких потоков и предназначен для использования как singleton.
  • При работе с ADO.NET и ORM используйте ClickHouseDataSource, чтобы все соединения использовали общий пул.
Полный набор рекомендуемых подходов приведён в разделе Время жизни подключения и управление пулом соединений.

Измерьте производительность самостоятельно

Во многих случаях производительность зависит от структуры данных, скорости соединения с сервером, распределения нагрузки на CPU между клиентом и сервером, ограничений оборудования и других факторов. Поэтому измеряйте производительность на своих данных и в своей среде. Чтобы оценить, какую часть работы выполняет сервер, задайте QueryOptions.QueryId и получите значения счётчиков:

Поддержка ORM

Для работы с ORM необходим API ADO.NET (ClickHouseConnection). Чтобы корректно управлять временем жизни соединений, создавайте их через ClickHouseDataSource:

Dapper

ClickHouse.Driver работает с Dapper. Драйвер автоматически преобразует синтаксис параметров Dapper @parameter в синтаксис ClickHouse {parameter:Type}, определяя типы по значениям .NET. Используйте ClickHouseDataSource для корректного управления временем жизни подключений:

Способы передачи параметров

Поддерживаются все стандартные способы передачи параметров в Dapper: Анонимные объекты:
Классы POCO:
Словарь:
DynamicParameters (на основе словаря или анонимного объекта):

Получение результатов запросов в виде POCO

Dapper сопоставляет столбцы со свойствами по именам (без учёта регистра):

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

Если нужно явно задать тип, используйте синтаксис ClickHouse {param:Type} непосредственно в SQL, передавая значения параметров в Dictionary<string, object>. Не используйте одновременно синтаксис @param и {param:Type} для одного и того же параметра.

WHERE IN

Штатный механизм развёртывания IN в Dapper работает:
Dapper переписывает условие как WHERE id IN (@Ids1, @Ids2, @Ids3), а драйвер преобразует каждый параметр, полученный при развёртывании. Функция ClickHouse has() также работает с параметром типа Array:

Пользовательские обработчики типов

Для некоторых типов ClickHouse, например ITuple, BigInteger и ClickHouseDecimal, необходимо регистрировать обработчики при запуске:
Пример реализации пользовательского обработчика типов см. в примере с Dapper.

Dapper.Contrib

GetAll<T>() и Get<T>(id) работают. Insert<T>() не работает: этот метод генерирует код в синтаксисе SQL Server (SCOPE_IDENTITY, []). Вместо него рекомендуется использовать метод InsertBinaryAsync, предоставляемый непосредственно клиентом ClickHouseClient.
Имена свойств должны точно совпадать с именами столбцов ClickHouse (с учётом регистра).

Ограничения

Linq2db

Этот драйвер совместим с linq2db — легковесной ORM и LINQ-провайдером для .NET. Подробную документацию см. на сайте проекта. Пример использования: Создайте DataConnection с помощью провайдера ClickHouse:
Сопоставление с таблицами можно настроить с помощью атрибутов или программного API конфигурации. Если имена классов и свойств в точности совпадают с именами таблиц и столбцов, дополнительная настройка не требуется:
Выполнение запросов:
Пакетная загрузка: Для эффективной массовой вставки данных используйте BulkCopyAsync.

Entity Framework Core

Официальный провайдер Entity Framework Core для ClickHouse. Он позволяет сопоставлять классы C# с таблицами ClickHouse, выполнять запросы с помощью LINQ и вставлять данные через SaveChanges — всё с использованием привычных механизмов EF Core.
Провайдер активно развивается. Текущая версия поддерживает запросы LINQ (в том числе JOIN, подзапросы и операции над множествами), INSERT через SaveChanges / BulkInsertAsync, миграции с полной поддержкой DDL (CREATE / ALTER / DROP), а также настройку движков таблиц ClickHouse. UPDATE / DELETE не поддерживаются.

Установка

Требуются .NET 10.0 и EF Core 10.

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

Определите сущность и DbContext, затем выполните запрос с помощью LINQ:

Поддерживаемые типы

Если вам нужна полная точность столбцов Decimal128/Decimal256, используйте ClickHouseDecimal (из ClickHouse.Driver.Numerics) вместо decimal: тип .NET decimal поддерживает не более 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. Провайдер автоматически добавляет set_join_use_nulls=1 при любом способе подключения, чтобы поведение JOIN соответствовало ожиданиям Entity Framework. Если сервер ClickHouse или профиль запрещает изменять эту настройку (например, профиль с readonly=1), отключите её автоматическое добавление:
Если включён отказ от этой функции, LEFT JOIN возвращает значения по умолчанию для столбцов ClickHouse, поэтому проверка наличия связанных объектов в EF по значениям NULL больше не работает как ожидается. Вместо == null явно сравнивайте значения с 0 / "".

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

SaveChanges использует встроенный API драйвера InsertBinaryAsync: данные кодируются в формате RowBinary, а тело запроса сжимается. Это гораздо эффективнее параметризованного SQL:
После сохранения сущности переходят из состояния Added в Unchanged, как и при работе с любым другим провайдером EF Core. Размер батча можно настроить (по умолчанию — 1000):

Массовая вставка

При загрузке больших объёмов данных с высокой пропускной способностью используйте BulkInsertAsync вместо SaveChanges. Этот метод расширения для DbContext полностью обходит механизмы отслеживания изменений, сопоставления идентичностей и управления состоянием в EF Core: он напрямую вызывает метод драйвера InsertBinaryAsync, используя кодирование RowBinary и сжатое тело запроса. Поэтому он подходит для загрузки больших наборов данных, если после вставки не требуется отслеживать сущности:
На вход можно передать любой IEnumerable<T> — метод обрабатывает объекты по мере поступления, не загружая их все в память. Возвращаемое значение — количество вставленных строк. После вставки объекты не прикрепляются к DbContext, поэтому перехода из состояния Added в Unchanged не происходит.

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

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

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

Система ValueConverter в EF Core позволяет сопоставлять пользовательские типы с типами, которые уже поддерживает провайдер. Провайдер не работает с пользовательским типом напрямую — EF Core преобразует значение на границе взаимодействия с ним. Преобразование для отдельного свойства:
Класс преобразователя для повторного использования:

Аннотации типов столбцов

Для скалярных типов, таких как string, int, DateTime и т. д., провайдер автоматически определяет тип ClickHouse. Для параметризованных типов и обёрток тип ClickHouse нужно указать явно. С помощью атрибутов аннотаций данных:
Использование fluent API в OnModelCreating:
Поддерживаются вложенные обёртки, такие как Array(Nullable(Int32)) и LowCardinality(Nullable(String)): провайдер автоматически снимает обёртки Nullable и LowCardinality на каждом уровне вложенности.

Столбцы типов Variant и Dynamic

Столбцы ClickHouse типов Variant(T1, T2, ...) и Dynamic сопоставляются с типом object в .NET. Поскольку object слишком универсален для автоматического вывода типов, тип столбца в базе данных необходимо задать явно с помощью .HasColumnType():
При чтении значение автоматически десериализуется в тип .NET, соответствующий сохранённому дискриминатору (например, string, ulong, ulong[]).

JSON-столбцы

Провайдер поддерживает столбцы ClickHouse типа Json: по умолчанию они сопоставляются с System.Text.Json.Nodes.JsonNode, а при использовании автоматического ValueConverter — со string:
Читать и записывать JSON можно как с помощью SaveChanges, так и с помощью BulkInsertAsync:
Если вы предпочитаете хранить JSON как строки без обработки, настройте для свойства тип string, а для столбца — Json. Провайдер автоматически применит ValueConverter:
  • Отсутствие преобразования JSON-путей — выражение entity.Data["name"] в LINQ не преобразуется в SQL-синтаксис ClickHouse data.name. Фильтруйте данные по столбцам, не содержащим JSON, а JSON обрабатывайте в памяти.
  • Семантика NULL — для значений NULL JSON-тип ClickHouse возвращает {} (пустой объект), а не SQL NULL.
  • Точность целых чисел — ClickHouse хранит все целые числа в JSON как Int64. При чтении через JsonNode используйте GetValue<long>() вместо GetValue<int>().

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

Настраивайте движки таблиц ClickHouse и относящиеся к ним секции через fluent API ToTable(name, t => ...). Если движок не задан, провайдер использует MergeTree, а секция ORDER BY определяется по первичному ключу сущности.
Поддерживаемые семейства движков: Секции движка: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Все они применяются к построителю движка, который возвращает HasXxxEngine(). Возможности на уровне столбца: HasCodec, HasTtl, HasComment, HasDefault — все поддерживаются при миграциях. Индексы пропуска данных — через HasIndex(...).HasSkippingIndexType(...):
Обычные индексы (не индексы пропуска данных) игнорируются без предупреждения, поскольку в ClickHouse нет их аналогов. При создании уникальных индексов возникает исключение, поскольку ClickHouse не контролирует уникальность значений.

Миграции

Стандартный порядок работы с миграциями в EF Core:
Поддерживаемые операции:

Ограничения миграций

Помимо миграций, провайдер пока не поддерживает:
  • UPDATE / DELETE
  • Транзакции: BeginTransaction не имеет эффекта. ACID-транзакции в ClickHouse не поддерживаются.
  • Преобразование запросов с JSON-путями: выражение entity.Data["key"] в LINQ не преобразуется в SQL-синтаксис ClickHouse data.key. Фильтруйте данные по столбцам, не содержащим JSON, а JSON обрабатывайте в памяти.

Ограничения

Tuple с 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. Tuple с 7 или менее элементами, а также Tuple, у которых последний элемент сам не является Tuple, не затронуты. Обходное решение: Поместите внутренний Tuple в ещё один уровень вложенности, чтобы драйвер мог отличить его от вложенности TRest:

Столбцы типа AggregateFunction

Столбцы типа AggregateFunction(...) нельзя запрашивать напрямую или напрямую вставлять в них данные. Чтобы вставить данные:
Для выборки:

Последнее изменение 14 октября 2026 г.