-
ClickHouseClient(рекомендуется): высокоуровневый потокобезопасный клиент, предназначенный для использования в качестве singleton. Предоставляет простой асинхронный API для запросов и массовых вставок. Лучше всего подходит для большинства приложений. -
ADO.NET (
ClickHouseDataSource,ClickHouseConnection,ClickHouseCommand): стандартные абстракции базы данных в .NET. Требуются для интеграции с ORM (Dapper, Linq2db) и в случаях, когда нужна совместимость с ADO.NET.ClickHouseBulkCopy— вспомогательный класс для эффективной вставки данных с использованием ADO.NET-соединения.ClickHouseBulkCopyустарел и будет удалён в одном из будущих релизов; вместо него используйтеClickHouseClient.InsertBinaryAsync.
Руководство по миграции
- Обновите файл
.csproj: укажите новое имя пакетаClickHouse.Driverи последнюю версию на NuGet. - Замените в кодовой базе все упоминания
ClickHouse.ClientнаClickHouse.Driver.
Поддерживаемые версии .NET
ClickHouse.Driver поддерживает следующие версии .NET:
- .NET 6.0
- .NET 8.0
- .NET 9.0
- .NET 10.0
Поддерживаемые версии ClickHouse
Клиент официально поддерживает три последних релиза, а также два последних LTS-релиза.Установка
Установите пакет из NuGet:Быстрый старт
Конфигурация
Существует два способа настроить подключение к ClickHouse:- Строка подключения: пары ключ/значение, разделённые точкой с запятой, которые задают хост, учётные данные для аутентификации и другие параметры подключения.
- Объект
ClickHouseClientSettings: строго типизированный объект конфигурации, который можно загрузить из файлов конфигурации или задать в коде.
Настройки подключения
Формат данных и сериализация
Управление сеансами
Флаг
UseSession включает сохранение сеанса на сервере, что позволяет использовать операторы SET и временные таблицы. Сеансы сбрасываются после 60 секунд бездействия (тайм-аут по умолчанию). Время жизни сеанса можно увеличить, задав настройку сеанса через команды ClickHouse или конфигурацию сервера.Класс ClickHouseConnection обычно поддерживает параллельную работу (несколько потоков могут выполнять запросы одновременно). Однако при включении флага UseSession для одного подключения в любой момент времени будет доступен только один активный запрос (это ограничение на стороне сервера).Безопасность
Конфигурация HTTP-клиента
Логирование и отладка
Пользовательские настройки и роли
Если вы задаёте пользовательские настройки через строку подключения, используйте префикс
set_, например: “set_max_threads=4”. Если вы используете объект ClickHouseClientSettings, префикс set_ указывать не нужно.Полный список доступных настроек см. здесь.Примеры строк подключения
Базовое подключение
С пользовательскими настройками ClickHouse
QueryOptions
QueryOptions позволяет переопределять настройки уровня клиента для отдельных запросов. Все свойства необязательны и переопределяют значения клиента по умолчанию только если они указаны.
Пример:
InsertOptions
InsertOptions дополняет QueryOptions настройками, специфичными для массовой вставки через InsertBinaryAsync.
Все свойства
QueryOptions также доступны в InsertOptions.
Пример:
Пропуск запроса для определения схемы
По умолчаниюInsertBinaryAsync перед каждой вставкой отправляет запрос SELECT ... WHERE 1=0, чтобы определить типы столбцов. В сценариях с высокой пропускной способностью эти накладные расходы можно исключить двумя способами:
Вариант 1: Явно указать типы столбцов
Если схема таблицы известна на этапе компиляции, передайте её напрямую через ColumnTypes. В этом случае запрос схемы вообще не отправляется:
UseSchemaCache = true, чтобы запросить схему один раз и повторно использовать её для последующих вставок через тот же экземпляр ClickHouseClient:
ColumnTypesимеет приоритет надUseSchemaCache. Если заданы оба параметра, используются явно указанные типы.- Кэш схемы не отслеживает изменения, внесённые командой
ALTER TABLE. Если вы изменяете схему таблицы, создайте новыйClickHouseClientили не используйтеUseSchemaCacheдля этой таблицы. - Кэш привязан к экземпляру
ClickHouseClient, а в качестве ключа используются (database, table). Разные подмножества столбцов одной и той же таблицы используют одну общую кэшированную схему.
ClickHouseClient
ClickHouseClient — рекомендуемый API для работы с ClickHouse. Он потокобезопасен, рассчитан на использование как singleton и самостоятельно управляет пулом HTTP-соединений.
Создание клиента
СоздайтеClickHouseClient с помощью строки подключения или объекта ClickHouseClientSettings. Доступные параметры см. в разделе Конфигурация.
Сведения о вашем сервисе ClickHouse Cloud доступны в консоли ClickHouse Cloud.
Выберите сервис и нажмите Connect:
Выберите C#. Ниже отобразятся сведения о подключении.
Если вы используете самоуправляемый ClickHouse, сведения о подключении задаёт ваш администратор ClickHouse.
Использование строки подключения:
ClickHouseClientSettings:
IHttpClientFactory:
ClickHouseClient рассчитан на длительное использование и совместное использование во всём приложении. Создайте его один раз (обычно как singleton) и затем повторно используйте для всех операций с базой данных. Клиент сам управляет пулом HTTP-соединений.Выполнение запросов
ИспользуйтеExecuteNonQueryAsync для команд, которые не возвращают результатов:
ExecuteScalarAsync, чтобы получить единственное значение:
Вставка данных
Параметризованные вставки
Для вставки данных с помощью параметризованных запросов используйтеExecuteNonQueryAsync. Типы параметров должны быть указаны в SQL с использованием синтаксиса {name:Type}:
Массовая вставка
ИспользуйтеInsertBinaryAsync для эффективной вставки большого количества строк. Метод передает данные в потоковом режиме в нативном бинарном формате строк ClickHouse, поддерживает параллельную загрузку батчей и позволяет избежать ошибок “URL too long”, которые могут возникать при параметризованных запросах.
InsertOptions:
- Перед вставкой клиент автоматически получает структуру таблицы с помощью
SELECT * FROM <table> WHERE 1=0. Передаваемые значения должны соответствовать типам целевых столбцов. Чтобы пропустить этот запрос, используйтеInsertOptions.ColumnTypesилиInsertOptions.UseSchemaCache. - Если
MaxDegreeOfParallelism > 1, батчи загружаются параллельно. Сеансы несовместимы с параллельной вставкой; либо отключите сеансы, либо задайтеMaxDegreeOfParallelism = 1. - Используйте
RowBinaryFormat.RowBinaryWithDefaultsвInsertOptions.Format, если хотите, чтобы сервер применял значения DEFAULT для столбцов, которые не были переданы.
Вставка POCO
Вместо создания массивовobject[] можно напрямую вставлять строго типизированные объекты POCO. Зарегистрируйте тип один раз, а затем передайте IEnumerable<T>:
Когда все сопоставленные свойства явно задают
Type, запрос для определения схемы полностью пропускается. Если явные типы указаны только у части свойств, драйвер возвращается к запросу для определения схемы для полного набора столбцов.
InsertBinaryAsync<T> поддерживает те же InsertOptions (батчинг, параллелизм, кэширование схемы), что и перегрузка object[].
В отличие от перегрузки
object[], InsertBinaryAsync<T> не принимает явный список столбцов. Столбцы определяются сопоставленными свойствами зарегистрированного типа. Чтобы управлять тем, какие столбцы вставляются, используйте [ClickHouseNotMapped], чтобы исключить свойства, или [ClickHouseColumn(Name = "...")], чтобы переименовать их.Если в InsertOptions задан ColumnTypes, он имеет приоритет над атрибутами POCO.Эволюция схемы
Вставка POCO работает без проблем, если после регистрации типа в целевую таблицу добавляются новые столбцы. Поскольку драйвер вставляет только те столбцы, которые сопоставлены с POCO, все новые столбцы сDEFAULT (или другими выражениями по умолчанию) сервер заполняет автоматически. Никаких изменений в коде или повторной регистрации не требуется.
Размещение запроса вставки
Бинарная вставка записывает операторINSERT INTO ... FORMAT ... в первой строке тела запроса, перед строками данных. Тело по умолчанию сжимается, поэтому механизмы маршрутизации и логирования, которые анализируют только URL, этот оператор не увидят. Задайте для InsertOptions.QueryPlacement значение InsertQueryPlacement.Url, чтобы оператор передавался в URL-параметре query, а в теле оставались только строки данных:
query или анализирует его, либо когда нужно, чтобы оператор попадал в журналы доступа и инструменты обсервабилити. Режим включается явно, поскольку оператор при этом учитывается в длине URL. Фактическое ограничение определяется наименьшим из тех, что накладывают .NET runtime, посредник и server. В .NET 6 — .NET 9 System.Uri ограничивает полный закодированный URI запроса 65 519 символами; при превышении этого предела driver генерирует исключение InvalidOperationException, которое подсказывает вернуться к InsertQueryPlacement.Body. В ClickHouse параметр http_max_uri_size по умолчанию равен 1 МиБ, однако посредник может устанавливать более низкий предел. В режиме body на оператор и строки такое ограничение длины 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>.
При ручном переборе через reader используйте ClickHouseDataReader.MapTo<T>(), чтобы материализовать текущую строку в зарегистрированный объект POCO, не продвигая reader дальше:
MapTo<T>, когда цикл чтения нужно вести самостоятельно — например, чтобы сочетать
прямой доступ к столбцам с материализацией в POCO. Метод читает строку через упакованные значения reader,
поэтому не предоставляет альтернативные типы свойств, описанные выше, и выделяет больше памяти, чем
QueryAsync<T>. Если вам нужны только строки, предпочтительнее QueryAsync<T>; цифры приведены в разделе
выбор способа материализации.
Конвертер значений при чтении, заданный на уровне client или для отдельного запроса, применяется к обоим путям и
не отключает чтение без упаковки. Driver преобразует каждый столбец той перегрузкой, которая соответствует способу
чтения этого столбца: типизированной ConvertValue<T> — для столбца, прочитанного без упаковки,
и упакованной ConvertValue — для составного столбца. Реализуйте обе перегрузки
согласованно, иначе один и тот же столбец будет давать разные результаты на разных путях.
Если настроен LoggerFactory, RegisterPocoType<T>() и RegisterBinaryInsertType<T>() выводят сообщение журнала уровня Debug (категория ClickHouse.Driver.Client) со списком того, какие свойства сопоставлены с какими столбцами, а также какие были пропущены и почему. См. Логирование и диагностика.
Параметры SQL
В ClickHouse стандартный формат параметров в SQL-запросах —{parameter_name:DataType}.
Примеры:
SQL-параметры ‘bind’ передаются как параметры HTTP-запроса в URI, поэтому их слишком большое количество может привести к исключению “URL too long”. Чтобы избежать этого ограничения при массовой вставке данных, используйте
InsertBinaryAsync.Плейсхолдеры @name в стиле ADO
Драйвер также принимает плейсхолдеры @name, которые генерируют ORM вроде Dapper. Это удобство
на стороне клиента: перед отправкой запроса каждый из них переписывается в
{name:ResolvedType}, так что сервер никогда не видит @. О том, как выбирается тип, см.
разрешение типов. Там, где это возможно, используйте явную
форму {name:Type}.
@name, для которого нет соответствующего параметра, остаётся без изменений — его отклонит сервер. Сопоставление
чувствительно к регистру, поэтому @ID не привяжет параметр с именем id.
Чтобы отключить переписывание, установите переключатель AppContext
ClickHouse.Driver.DisableReplacingParameters
до первого использования драйвера. Прекращается только переписывание текста; параметры по-прежнему отправляются, поэтому
запросы, написанные с использованием нативного синтаксиса {name:Type}, продолжают работать.Параметры Identifier
Тип параметра Identifier позволяет безопасно подставлять имя базы данных, таблицы или столбца вместо строкового литерала в кавычках. Используйте его с помощью синтаксиса {name:Identifier} в SQL или задав ClickHouseDbParameter.ClickHouseType = "Identifier":
Query ID
Каждому запросу назначается уникальныйquery_id, который можно использовать, чтобы получить данные из таблицы system.query_log или отменить долго выполняющиеся запросы. Вы можете указать собственный идентификатор запроса через QueryOptions:
Пользовательское сопоставление типов параметров
При использовании параметров в стиле@ (например, WHERE id = @id) драйвер автоматически определяет тип ClickHouse по типу значения .NET. Например, int сопоставляется с Int32.
Чтобы переопределить эти значения по умолчанию, задайте ParameterTypeResolver в ClickHouseClientSettings. Это полезно, если вы хотите, чтобы все параметры DateTime использовали DateTime64(3) с точностью до миллисекунд, или чтобы для всех десятичных значений использовался определённый масштаб, без необходимости задавать ClickHouseType для каждого отдельного параметра.
Использование DictionaryParameterTypeResolver для простых сопоставлений типов:
IParameterTypeResolver для расширенных сценариев:
Если нужно определять тип по значению или имени, реализуйте интерфейс IParameterTypeResolver напрямую. Верните null, чтобы использовать определение типа по умолчанию:
QueryOptions.ParameterTypeResolver. Если он задан, он имеет приоритет над resolver на уровне клиента.
Приоритет разрешения типов:
Resolver — это один из шагов в цепочке приоритетов. От наивысшего приоритета к наименьшему:
- Явно заданный
ClickHouseTypeу параметра - Подсказка типа SQL из синтаксиса
{name:Type}в запросе IParameterTypeResolver(изQueryOptions.ParameterTypeResolverс откатом кClickHouseClientSettings.ParameterTypeResolver)- Встроенный вывод типов (
TypeConverter.ToClickHouseType)
ClickHouseConnection — настройки наследуются соединениями, созданными клиентом.
Пользовательское форматирование значений параметров
IParameterFormatter — это хук, который определяет, как сериализуются значения параметров. Используйте его, если встроенное форматирование (например, точность для DateTime, локаль для decimal, экранирование строк, представление чисел) не соответствует тому, что ожидают ваша схема или последующие инструменты.
Задайте ParameterFormatter в ClickHouseClientSettings, чтобы подключить форматтер для всех параметризованных запросов. Форматтер получает значение, разрешённое имя типа ClickHouse и имя параметра, а затем возвращает строковое представление, которое отправляется на сервер. Верните null, чтобы использовать форматтер по умолчанию.
Использование DictionaryParameterFormatter для простого форматирования по типам CLR:
IParameterFormatter для сложных сценариев:
QueryOptions.ParameterFormatter. Если он задан, то имеет приоритет над форматтером на уровне клиента.
Составные значения:
Форматтер применяется как к параметрам-коллекциям верхнего уровня, так и к каждому элементу внутри составных значений (Array, Tuple, Map, Nullable, LowCardinality, Variant). Например, сопоставление typeof(int) форматирует каждый элемент Int32 в Array(Int32) по отдельности.
Оборачивание в одинарные кавычки в составных контекстах:
Для строкоподобных типов ClickHouse (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID), встроенных в составной литерал, драйвер оборачивает вывод форматтера в одинарные кавычки, но не экранирует его содержимое. Если возвращаемая строка содержит неэкранированную одинарную кавычку или обратную косую черту, составной литерал будет некорректным, и сервер отклонит запрос.
Строковые параметры верхнего уровня (не встроенные в составной тип) используются как есть, без оборачивания, поэтому экранирование там не требуется.
Приоритет форматтера:
IParameterFormatter(изQueryOptions.ParameterFormatter, с переходом кClickHouseClientSettings.ParameterFormatter). Если он возвращает не-NULL, используется это значение.- Встроенное форматирование для конкретных типов в
HttpParameterFormatter.
null или DBNull; они всегда сериализуются как null-маркер ClickHouse (\N).
Пользовательское преобразование значений при чтении
IReadValueConverter позволяет преобразовывать значения, возвращаемые средством чтения данных после десериализации, не меняя их CLR-тип. Типичные сценарии использования: установка DateTime.Kind = Utc для столбца DateTime без часового пояса, обрезка или нормализация строк, а также постобработка JSON-столбца перед передачей в прикладной код.
Задайте ReadValueConverter в ClickHouseClientSettings, чтобы установить преобразователь для всех операций чтения. Преобразователь вызывается один раз для каждого столбца в каждой строке как через boxed-путь (GetValue), так и через обобщённый путь (GetFieldValue<T>). Если преобразователь не задан, дополнительная нагрузка отсутствует — средство чтения возвращает значения напрямую.
Использование DictionaryReadValueConverter для простого преобразования по CLR-типу:
For<T>, передаются без изменений. Диспетчеризация выполняется по точному типу CLR, поэтому регистрируйте фактический тип, который возвращает reader (например, For<JsonObject> для JSON-столбца в JsonReadMode.Binary).
Пользовательский IReadValueConverter для продвинутых сценариев:
Если вам нужно выполнять диспетчеризацию по строковому представлению типа на стороне ClickHouse (например, чтобы различать DateTime и DateTime('UTC') — хотя оба отображаются как один и тот же тип CLR), реализуйте IReadValueConverter напрямую:
GetFieldType, GetSchemaTable) не перенаправляются через него и должны оставаться согласованными с возвращаемым значением.
Вы также можете задать конвертер для каждого запроса через QueryOptions.ReadValueConverter; если он задан, приоритет будет у него, а не у конвертера уровня клиента.
Граница диспетчеризации:
Конвертер вызывается один раз для каждого столбца и получает всё десериализованное значение ячейки целиком; он не выполняет рекурсивную обработку составных контейнеров. Для столбца Array(Int32) передаваемым значением будет int[]; для Tuple(Int32, String) — ITuple.
Какая перегрузка выполняется:
Обе перегрузки должны быть согласованы, поскольку то, какую из них вызовет драйвер, зависит от того, как вызывающая сторона прочитала
столбец:
ConvertValue<T>— типизированные аксессорыGetByte,GetSByte,GetInt16/32/64,GetUInt16/32/64,GetFloat,GetDouble,GetGuid,GetDateTime,GetIPAddress,GetBigIntegerиGetFieldValue<T>, а также каждый столбец без упаковки на пути чтения POCO.ConvertValue(boxed) —GetValue,GetValues, индексаторы,GetChar,GetTuple, а также пути с приведением типов вGetBoolean,GetDecimalиGetString.
IsDBNull не запускает конвертер вовсе: он считывает флаг null напрямую, поэтому конвертер никак не может
повлиять на то, считается ли значение null. TryGetEnumOrdinal также обходит его — см.
чтение порядкового номера enum.
Конвертер работает с ADO.NET-путём ClickHouseConnection — настройки наследуются соединениями, созданными из клиента.
Прямая потоковая передача
ИспользуйтеExecuteRawResultAsync, чтобы напрямую передавать результаты запроса в указанном формате, минуя средство чтения данных. Это удобно для экспорта данных в файлы или передачи в другие системы:
JSONEachRow, CSV, TSV, Parquet, Native. Все доступные варианты см. в документации по форматам.
Сжатие передачи для отдельного запроса
По умолчанию клиент согласовываетzstd, lz4, gzip, deflate, когда Compression=true (значение по умолчанию в строке подключения), и сам прозрачно декодирует поток.
Для экспорта в исходном формате (например, Parquet, Arrow, Native) может потребоваться согласовать другой кодек (например, zstd или lz4), чтобы снизить нагрузку на CPU в обмен на пропускную способность, не меняя настройку для всего соединения. QueryOptions.AcceptEncoding и ClickHouseCommand.AcceptEncoding задают HTTP-заголовок Accept-Encoding для одного запроса, заменяя любое значение по умолчанию, и принудительно устанавливают enable_http_compression=1 в URL (ClickHouse требует этого, чтобы учитывать Accept-Encoding).
Настройка HttpClient
Настраивать ничего не нужно: вHttpClient, который создаёт драйвер, параметр AutomaticDecompression остаётся равным DecompressionMethods.None, а декодированием ответов занимается сам драйвер, поэтому Content-Encoding никогда не удаляется незаметно для вас, и необработанное тело ответа доходит до вас ровно в том виде, в каком его отправил сервер.
Тела ошибок
Когда сервер отвечает кодом 4xx/5xx и задан параметрenable_http_compression=1, он сжимает тело ошибки тем же кодеком, который использовал бы для успешного ответа. Драйвер декодирует такие ответы для всех поддерживаемых кодеков (lz4, zstd, gzip, deflate, br/brotli), поэтому сообщение, передаваемое в ClickHouseServerException, остаётся читаемым. Для всего остального (snappy, …) он возвращает сообщение-заполнитель, в котором указан кодек и дана ссылка на system.query_log, где можно найти исходный текст ошибки.
Распаковка ответа
Accept-Encoding лишь просит сервер сжать ответ — декодировать его всё равно кто-то должен. Драйвер делает это сам, ориентируясь на Content-Encoding ответа, поэтому все обычные API чтения (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) работают со сжатым ответом без какой-либо настройки. Поддерживается декодирование lz4, zstd, gzip, deflate и br; snappy не поддерживается.
По умолчанию драйвер объявляет zstd, lz4, gzip, deflate, и ClickHouse отвечает в zstd. Чтобы выбрать другой вариант, задайте Accept-Encoding самостоятельно — для всего клиента:
ClickHouseClientSettings:
enable_http_compression=1 в URL — без этого ClickHouse вообще не учитывает заголовок. Это происходит и тогда, когда UseCompression равно false, поскольку явное указание кодека воспринимается как запрос сжатия. Если значение не задано, при UseCompression=false заголовок Accept-Encoding не отправляется вовсе.
Accept-Encoding можно задать в четырёх местах. Побеждает первое из них, в котором назван кодек:
QueryOptions.AcceptEncoding(илиClickHouseCommand.AcceptEncoding)CustomHeaders["Accept-Encoding"]на уровне запросаCustomHeaders["Accept-Encoding"]на уровне клиентаClickHouseClientSettings.AcceptEncodingили ключевое слово строки подключенияAcceptEncoding
identity.
Кодек выбирает сервер, а не клиент. ClickHouse ищет в Accept-Encoding токены в собственном фиксированном порядке предпочтения — zstd > br > lz4 > snappy > gzip > deflate — и игнорирует как порядок их перечисления, так и любые q-значения. Таким образом, этот заголовок сообщает о поддерживаемых возможностях, а не выдвигает требование, и единственный способ повлиять на выбор — исключить часть токенов. Список по умолчанию включает zstd, поэтому обычный запрос получает ответ, сжатый zstd; остальные токены служат резервным вариантом. br поддерживается при декодировании, но по умолчанию не объявляется.
Соотношение кодеков по размеру полезной нагрузки, нагрузке на CPU сервера и клиента зависит от ваших данных, канала связи и серверного параметра http_zlib_compression_level (значение по умолчанию в поставке: 3) — см. Настройка сжатия.
http_zlib_compression_level. Этот параметр применяется ко всем HTTP-кодекам, значение по умолчанию — 3. Его следует подбирать исходя из ваших данных, скорости канала и потребления CPU.- Клиент, ограниченный CPU, на быстром канале. Драйвер декодирует тело ответа в вызывающем потоке, поэтому, когда сеть не является узким местом, ограничивающим фактором может стать скорость декодирования на стороне клиента.
Content-Encoding, независимо от того, что было запрошено: при отсутствии заголовка или значении identity тело передаётся без изменений, поддерживаемый кодек декодируется, а всё остальное приводит к ошибке с указанием этого кодека. Риска двойного декодирования нет: если AutomaticDecompression у обработчика, предоставленного вызывающей стороной, уже декодировал тело, он также удаляет Content-Encoding, поэтому драйвер видит незашифрованные данные и не трогает их.
Raw-результаты не объявляют кодек. ExecuteRawResultAsync (а также публичные PostStreamAsync / InsertRawStreamAsync) отдают вам тело ответа дословно, поэтому, если вы сами не укажете кодек, они вообще не запрашивают никакого сжатия — драйвер такое тело не декодирует, и предложить кодек здесь означало бы незаметно превратить экспорт в сжатый файл. Отсюда простое правило, не зависящее от настроек конкретного HttpClient: дословное тело приходит ровно в том виде, в каком его отправил сервер, а сервер отправляет незашифрованный текст, если вы не запросили кодек. Запросить его (для всего клиента или для отдельного запроса) — это и есть способ намеренно выгрузить сжатые байты.
Явно заданный AcceptEncoding (на любом из уровней) по-прежнему применяется к raw-запросам, а 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оператор отправляется в открытом виде. Команды невелики, поэтому об этом редко стоит задумываться — но это полезно знать, когда вы наблюдаете за запросами через proxy или в перехвате пакетов. - Multipart-тело — запрос, параметры которого отправляются как form data (
UseFormDataParameters=true) — всегда отправляется без сжатия, независимо от значенияUseCompression. - Raw-загрузка (
InsertRawStreamAsync,PostStreamAsync) использует собственный флаг для каждого вызова и не учитывает ниUseCompression, ниInsertOptions.Compressor: gzip, если флаг установлен, и без сжатия в противном случае. Обратите внимание, что параметрuseCompressionуInsertRawStreamAsyncпо умолчанию равенtrue, поэтому raw-загрузка сжимается gzip, если не передатьfalse— даже приCompression=falseна клиенте.
Настройка сжатия
Сжатие меняет ресурсы CPU на объём передаваемых байт. Окажется ли такой размен выгодным, почти полностью зависит от того, насколько быстр ваш канал по сравнению со скоростью работы кодека. Единой настройки, подходящей всем, не существует.Одно число, которое всё решает
Сжатие оправдано до тех пор, пока кодек работает быстрее сети. На пути чтения этот порог ниже, чем принято считать, поскольку ClickHouse сжимает HTTP-ответы в один поток в выходном буфере. По измерениям на сервисе ClickHouse Cloud с 16 vCPU (hits, RowBinary, уровень 3) сервер выдаёт сжатый вывод со скоростью примерно 100-200 МБ/с.
Поэтому для большого результата и при условии, что одновременно обрабатывается один запрос, сжатие перестаёт окупаться где-то в районе 100 МБ/с. Один HTTPS-поток
внутри одного облачного региона обычно превышает это значение, тогда как всё, что идёт через публичный интернет, VPN или границу региона, как правило, остаётся ниже.
Путь вставки допускает сжатие при более высоких скоростях канала, поскольку ваш клиент сжимает данные на отдельном ядре и обычно работает быстрее, чем сжатие ответа на сервере.
Ориентировочные рекомендации по вариантам развертывания
Три момента, которые эта таблица не учитывает:
- Стоимость исходящего трафика: если передача данных тарифицируется, байты стоят денег, а не только времени, и это склоняет к более высокой степени сжатия независимо от скорости канала.
- Небольшие результаты: всё сказанное выше относится к большим полезным нагрузкам. Для небольших ответов кодек почти не имеет значения — определяющими становятся накладные расходы на каждый запрос.
- Параллельные вставки поднимают пороговые значения для вставок. Все приведенные выше показатели пропускной способности относятся к одному потоку. Значение
InsertOptions.MaxDegreeOfParallelismпо умолчанию равно1, но при его увеличении батчи сжимаются параллельно, поэтому совокупная скорость кодирования на стороне клиента растет примерно пропорционально числу выделенных ядер. Поэтому на быстром канале параллельную вставку по-прежнему выгодно сжимать даже на тех скоростях, при которых однопоточная вставка уже не выигрывает от сжатия. Считайте строки таблицы, относящиеся к вставкам, нижней границей: если вы уже формируете батчи параллельно, проведите повторные измерения, прежде чем делать вывод, что ваш канал слишком быстр для сжатия.
Выбор кодека
Levels
Сжатие ответов управляется одной настройкой сервера —http_zlib_compression_level, которая применяется к каждому HTTP-кодеку, а не только к zlib. Значение по умолчанию — 3.
Не трогайте её без измеренных оснований. Выше значения по умолчанию выигрыш в размере минимален, а расход CPU велик (для zstd переход с 3 на 6 примерно удваивает нагрузку на CPU сервера ради ~14% экономии байтов), а br ведёт себя патологически. Ниже, на уровне 1, картина действительно меняется: lz4 становится намного дешевле, а zstd теряет своё преимущество по CPU перед ним. При необходимости задавайте её на уровне запроса:
Измерение собственной точки перехода
Быстрее всего подобрать оптимальный кодек и уровень сжатия можно, замерив время выполнения одного и того же запроса на нескольких кодеках и сравнив результаты.ProfileEvents из system.query_log — задайте
QueryOptions.QueryId, чтобы найти нужную строку:
LIMIT n без ORDER BY возвращает разные строки
при каждом запуске, поэтому в каждом повторе сжимаются разные данные, а соотношения превращаются в шум. Сравнивайте
на фиксированном результирующем наборе.
Вставка из сырого потока
ИспользуйтеInsertRawStreamAsync для вставки данных напрямую из файловых потоков или потоков в памяти в таких форматах, как CSV, JSON, Parquet или любой другой поддерживаемый формат ClickHouse.
Вставка из CSV-файла:
Опции управления поведением ингестии данных см. в документации по настройкам форматов.
Дополнительные примеры
Дополнительные практические примеры использования см. в каталоге examples репозитория GitHub.ADO.NET
Библиотека предоставляет полную поддержку ADO.NET черезClickHouseConnection, ClickHouseCommand и ClickHouseDataReader. Этот API необходим для интеграции с ORM (Dapper, Linq2db), а также в случаях, когда нужны стандартные абстракции базы данных .NET.
Управление жизненным циклом с ClickHouseDataSource
Всегда создавайте соединения черезClickHouseDataSource, чтобы обеспечить корректное управление жизненным циклом и использование пула соединений. DataSource внутренне использует один ClickHouseClient, и все соединения совместно используют его пул HTTP-соединений.
Использование ClickHouseCommand
Создавайте команды на основе соединения для выполнения SQL:ExecuteNonQueryAsync()— Для операторов INSERT, UPDATE, DELETE и DDL-операторовExecuteScalarAsync()— Возвращает первый столбец первой строкиExecuteReaderAsync()— ВозвращаетClickHouseDataReaderдля перебора результатов
Использование ClickHouseDataReader
ClickHouseDataReader обеспечивает типизированный доступ к результатам запроса:
Чтение порядкового номера enum
СтолбецEnum8 или Enum16 материализуется как его метка: GetFieldType возвращает string, а
GetString, GetValue и GetFieldValue<string> — саму метку. Числовые аксессоры
для столбца enum генерируют исключение InvalidCastException, поскольку хранимое значение является строкой.
Чтобы получить число, стоящее за меткой, используйте TryGetEnumOrdinal:
true и задаёт value для столбца типа Enum8/Enum16, а также для столбца
Nullable(Enum...), ячейка которого не равна NULL. Он возвращает false, а value устанавливается в 0,
если ячейка содержит NULL или если столбец не является перечислением. Порядковый номер — это
знаковое значение, полученное из сетевого представления данных, поэтому оно может быть отрицательным, а порядковый номер Enum16 может
не помещаться в один байт.
Рекомендации
Время жизни соединений и пул соединений
ClickHouse.Driver использует System.Net.Http.HttpClient внутри. У HttpClient есть отдельный пул соединений для каждой конечной точки. В результате:
- Сеансы базы данных мультиплексируются через HTTP-соединения, которыми управляет пул соединений.
- HTTP-соединения автоматически переиспользуются пулом.
- Соединения могут оставаться активными даже после освобождения объектов
ClickHouseClientилиClickHouseConnection.
Обработка DateTime
-
По возможности используйте UTC. Храните временные метки в столбцах
DateTime('UTC')и используйтеDateTimeKind.Utcв коде. Это устраняет неоднозначность, связанную с часовыми поясами. -
Используйте
DateTimeOffsetдля явной работы с часовыми поясами. Он всегда представляет конкретный момент времени и содержит информацию о смещении. -
Указывайте часовой пояс в подсказках типов SQL. Если вы используете параметры со значениями DateTime
Unspecifiedдля столбцов не в UTC, указывайте часовой пояс прямо в SQL:
Асинхронные вставки
Асинхронные вставки переносят ответственность за батчинг с клиента на сервер. Вместо батчинга на стороне клиента сервер буферизует входящие данные и сбрасывает их в хранилище при достижении настраиваемых пороговых значений. Это особенно полезно в сценариях с высоким параллелизмом, например для рабочих нагрузок обсервабилити, где множество агентов отправляют небольшие полезные нагрузки. Включите асинхронные вставки черезCustomSettings или строку подключения:
wait_for_async_insert):
Ключевые настройки:
Сеансы
Включайте сеансы только при необходимости использовать возможности сервера с сохранением состояния, например:- Временные таблицы (
CREATE TEMPORARY TABLE) - Сохранение контекста запроса между несколькими командами
- Настройки уровня сеанса (
SET max_threads = 4)
Поддерживаемые типы данных
ClickHouse.Driver поддерживает все типы данных ClickHouse. В таблицах ниже показано соответствие между типами ClickHouse и встроенными типами .NET при чтении данных из базы данных.
Сопоставление типов: чтение из ClickHouse
Целочисленные типы
Типы чисел с плавающей точкой
Десятичные типы
Преобразование типа Decimal регулируется настройкой UseCustomDecimals.
Логический тип
Строковые типы
По умолчанию столбцы
String и FixedString(N) возвращаются как string. Установите ReadStringsAsByteArrays=true в строке подключения, чтобы вместо этого считывать их как byte[]. Это полезно при хранении бинарных данных, которые могут быть не в корректной кодировке UTF-8.Эта настройка распространяется и на строки, вложенные в другие типы, поэтому Array(String) считывается как byte[][],
а Map(String, String) — как Dictionary<byte[], byte[]>, включая ключи. Единственное исключение —
JSON-столбец, строковые листья которого всегда представлены текстом; см. JSON type.Типы даты и времени
ClickHouse хранит значения
DateTime и DateTime64 внутри как Unix-временные метки (секунды или доли секунды с начала эпохи Unix). Хотя хранение всегда выполняется в UTC, со столбцами может быть связан часовой пояс, который влияет на то, как значения отображаются и интерпретируются.
При чтении значений DateTime свойство DateTime.Kind устанавливается на основе часового пояса столбца:
Для столбцов не в UTC возвращаемый
DateTime представляет локальное время в этом часовом поясе. Используйте ClickHouseDataReader.GetDateTimeOffset(), чтобы получить DateTimeOffset с корректным смещением для этого часового пояса:
DateTime, а не DateTime('Europe/Amsterdam')) драйвер возвращает DateTime с Kind=Unspecified. Это позволяет сохранить локальное время в точности в том виде, в котором оно хранится, не делая предположений о часовом поясе.
Если для столбцов без явно заданных часовых поясов вам нужно поведение с учётом часового пояса, сделайте одно из следующего:
- Используйте явные часовые пояса в определениях столбцов:
DateTime('UTC')илиDateTime('Europe/Amsterdam') - Задайте часовой пояс самостоятельно после чтения.
Тип JSON
Возвращаемый тип для JSON-столбцов задаётся параметром
JsonReadMode:
-
Binary(по умолчанию): ВозвращаетSystem.Text.Json.Nodes.JsonObject. Обеспечивает структурированный доступ к JSON-данным, но специализированные типы ClickHouse (например, IP-адреса, UUID и большие decimal-значения) внутри структуры JSON преобразуются в строковое представление. -
String: Возвращает исходный JSON в видеstring. Сохраняет точное представление JSON из ClickHouse, что полезно, когда JSON нужно передать дальше без парсинга или если вы хотите самостоятельно выполнять десериализацию.
None — третий режим. Данные читаются точно так же, как в Binary, но настройка сервера не отправляется вместе с запросом — используйте его для подключений, которым не разрешено её задавать.
Путь, объявленный в типе столбца, — это typed path; любой другой путь в документе — это
dynamic path. Различия между ними проявляются, когда значение равно null.
Typed path всегда присутствует в JsonObject. Будучи объявленным как Nullable(T) или Dynamic, он возвращается
как JSON null и в случае, когда сохранённое значение равно null, и в случае, когда в документе такого пути нет, — эти
два случая неразличимы:
JSON(x String)
даёт {"x":""}, а JSON(x Int64) — {"x":0}.
Динамический path со значением null полностью удаляется из объекта, поэтому ContainsKey возвращает для него false. Чтение {"x":null} из обычного столбца JSON даёт {}.
Вложенные typed paths создают свои parents, поэтому JSON(a.b Nullable(Int64)) даёт {"a":{"b":null}}
даже для пустого документа.
Именно так это отображает сам server, поэтому режимы
Binary и String теперь согласованы. До версии 1.4.0
typed path со значением null удалялся из JsonObject, из-за чего {"x":null} считывался как
{}, а для вложенного path вида JSON(a.b Nullable(Int64)) исчезало всё поддерево a.JSON всегда возвращаются как текст, независимо от значения
ReadStringsAsByteArrays — у JsonValue нет формы массива байтов, поэтому byte[] отображался бы
в base64. Это справедливо для String, FixedString, а также для них же, обёрнутых в
LowCardinality, Nullable или SimpleAggregateFunction, и для строк внутри Array и Map,
включая ключи map.
Массив байтов, тип которого JSON-считыватель определить не может, всё же отображается в base64:
типизированный путь
Variant или Dynamic содержит значение, тип которого известен только для каждой отдельной строки, поэтому строка
под Variant(Array(UInt8), String) возвращается закодированной в base64. Это одинаково при обоих значениях настройки.Тип ключа JSON-map, отличный от строго String — например, Map(LowCardinality(String), String) —
приводит к NotSupportedException.JSON(a Int64, a.b Int64). Оба пути присутствуют в каждой строке, поэтому сервер
формирует строку с дублирующимся ключом: {"a":0,"a":{"b":7}}. Объект JsonObject не может хранить два значения
для одного ключа, поэтому JsonReadMode.Binary генерирует исключение SerializationException с указанием обоих путей. То
же самое происходит, когда значением является Map, как в случае JSON(a Map(String, Int64)), прочитанного из строки, в
которой также есть динамический a.b.
Это относится только к случаям, когда в данной строке значение есть у обеих сторон. Сторона, в которой ничего нет — null,
пустой объект или поддерево, все значения которого равны null, — уступает стороне с данными,
независимо от того, какой из двух путей сервер отправит первым. Поэтому пересечение, объявленное с типами Nullable, заполняет по одной стороне на строку и читается без
ошибок: JSON(a Nullable(Int64), a.b Nullable(Int64)) даёт {"a":5} и {"a":{"b":7}}, как и
ожидается.
Читайте такой столбец в режиме JsonReadMode.String, чтобы получить JSON-текст сервера без изменений, включая дублирующийся
ключ.
Задайте AllowDuplicateJsonKeys, чтобы столбец по-прежнему читался как JsonObject, а не приводил к исключению. В этом случае
драйвер сохраняет то из двух значений, которое идёт в строке последним, и отбрасывает второе, поэтому
результат получается с потерями: JSON(a Int64, a.b Int64), содержащий {"a.b":7}, читается как {"a":0}. Путь, у которого
есть значение и родитель которого содержит скаляр или массив, всё равно приводит к исключению, поскольку поддерево нельзя
разместить ни под тем, ни под другим.
Map type
Тип
Map(K, V) в ClickHouse физически представляет собой Array(Tuple(K, V)) и может содержать несколько записей с одним и тем же ключом. Dictionary этого не допускает, поэтому в режиме по умолчанию для повторяющегося ключа сохраняется только последнее значение, а предыдущие пары отбрасываются. Настройка MapReadMode задаёт используемое представление:
-
Dictionary(по умолчанию): возвращаетDictionary<K, V>. -
KeyValuePairs: возвращаетList<KeyValuePair<K, V>>в том порядке, в котором пары были отправлены сервером, поэтому сохраняются все пары, включая записи с повторяющимися ключами.
Map, поэтому он влияет и на GetFieldValue<T>, и на типы схемы, сообщаемые драйвером, и на сопоставление свойств POCO. Он действует везде, где map встречается в дереве типов столбца, включая Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) и Dynamic.
При записи в любом из режимов принимаются оба представления — см. запись maps.
Другие типы
Типы Dynamic и Variant преобразуются в тип, соответствующий фактическому базовому типу в каждой строке.
Геометрические типы
Тип Geometry — это Variant, который может содержать любой из геометрических типов. Он будет преобразован в соответствующий тип.
Сопоставление типов: запись в ClickHouse
При вставке данных драйвер преобразует типы .NET в соответствующие типы ClickHouse. В таблицах ниже показано, какие типы .NET допускаются для каждого типа столбца ClickHouse.Целочисленные типы
Типы с плавающей точкой
Логический тип
Строковые типы
Типы даты и времени
Значения вне диапазонаПри записи по бинарному пути значения
Date, Date32, DateTime и DateTime32, выходящие за пределы поддерживаемого диапазона, вызывают ArgumentOutOfRangeException во время Write, при этом указываются тип столбца и поддерживаемый диапазон. Ранее значения вне диапазона могли молча усекаться через 32-битное целое число и затем переинтерпретироваться сервером, что приводило к появлению реальных, но неверных временных меток.DateTime.Kind при записи значений:
Значения
DateTimeOffset всегда сохраняют точный момент времени.
Пример: UTC DateTime (точный момент времени сохраняется)
DateTimeKind.Utc или DateTimeOffset во всех операциях с DateTime. Это гарантирует, что ваш код будет работать одинаково независимо от часового пояса сервера, клиента или столбца.
HTTP-параметры vs пакетная загрузка
При записи значений DateTime сUnspecified есть важное различие между привязкой HTTP-параметров и пакетной загрузкой:
Bulk Copy знает часовой пояс целевого столбца и корректно интерпретирует значения Unspecified в этом часовом поясе.
HTTP Parameters не знают часовой пояс столбца автоматически. Его необходимо указать в подсказке типа SQL:
Десятичные типы
Тип JSON
Поведение при записи JSON определяется настройкой
JsonWriteMode:
-
String(по умолчанию): Принимаетstring,JsonObject,JsonNodeили любой объект. Все входные данные сериализуются черезSystem.Text.Json.JsonSerializerи отправляются как JSON-строки для разбора на стороне сервера. Это самый гибкий режим, который работает без регистрации типов. -
Binary: Принимает только зарегистрированные типы POCO. На стороне клиента данные преобразуются в двоичный JSON-формат ClickHouse с полной поддержкой подсказок типов. Перед использованием необходимо вызватьconnection.RegisterJsonSerializationType<T>(). Запись значенийstringилиJsonNodeв этом режиме генерируетArgumentException.
JSON(id UInt64, price Decimal128(2))), драйвер использует их для сериализации значений с полным сохранением точности типов. Это позволяет сохранить точность для таких типов, как UInt64, Decimal, UUID и DateTime64, которая иначе могла бы теряться при сериализации в обычный JSON.
POCO можно записывать в JSON-столбцы двумя способами в зависимости от JsonWriteMode:
Режим String (по умолчанию): POCO сериализуются через System.Text.Json.JsonSerializer. Регистрировать типы не требуется. Это самый простой вариант, и он работает с анонимными объектами.
Бинарный режим: POCO сериализуются с использованием бинарного JSON-формата драйвера с полной поддержкой подсказок типа. Перед использованием типы необходимо зарегистрировать с помощью connection.RegisterJsonSerializationType<T>(). Этот режим поддерживает пользовательские сопоставления путей с помощью атрибутов:
-
[ClickHouseJsonPath("path")]: Связывает свойство с пользовательским JSON-путём. Полезно для вложенных структур или когда имя свойства отличается от нужного JSON-ключа. Работает только в бинарном режиме. -
[ClickHouseJsonIgnore]: Исключает свойство из сериализации. Работает только в бинарном режиме.
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 (Nested(...)) можно читать и записывать как массивы.
Логирование и диагностика
Клиент ClickHouse для .NET интегрируется с абстракциямиMicrosoft.Extensions.Logging и предоставляет легковесное логирование, которое можно включить при необходимости. Когда оно включено, драйвер выводит структурированные сообщения о событиях жизненного цикла соединения, выполнении команд, транспортных операциях и операциях массовой вставки. Логирование полностью опционально — приложения, в которых не настроен логгер, продолжают работать без дополнительной нагрузки.
Быстрый старт
Использование appsettings.json
Вы можете настроить уровни логирования с помощью стандартной конфигурации .NET:Использование конфигурации в памяти
Вы также можете настроить в коде уровень детализации журналирования по категориям:Категории и источники
Драйвер использует отдельные категории, чтобы можно было тонко настраивать уровни логирования для каждого компонента:Пример: Диагностика проблем с подключением
- Выбор фабрики HTTP-клиента (пул по умолчанию или одиночное соединение)
- Конфигурация HTTP-обработчика (SocketsHttpHandler или HttpClientHandler)
- Настройки пула соединений (MaxConnectionsPerServer, PooledConnectionLifetime и т. д.)
- Настройки тайм-аутов (ConnectTimeout, Expect100ContinueTimeout и т. д.)
- Настройка SSL/TLS
- События открытия и закрытия соединения
- Отслеживание идентификатора сеанса
Режим отладки: сетевая трассировка и диагностика
Чтобы упростить диагностику сетевых проблем, библиотека драйвера содержит вспомогательный механизм, который включает низкоуровневую трассировку внутренних механизмов сетевой подсистемы .NET. Чтобы включить его, необходимо передать LoggerFactory с установленным уровнем Trace и задать EnableDebugMode = true (или включить его вручную через классClickHouse.Driver.Diagnostic.TraceHelper). События будут записываться в категорию ClickHouse.Driver.NetTrace. Предупреждение: это приведет к созданию чрезвычайно подробных журналов и повлияет на производительность. Включать режим отладки в продакшн не рекомендуется.
OpenTelemetry
Драйвер поддерживает встроенную распределённую трассировку OpenTelemetry через API .NETSystem.Diagnostics.Activity. При включении драйвер создаёт спаны для операций с базой данных, которые можно экспортировать в системы обсервабилити, такие как Jaeger или сам ClickHouse (через OpenTelemetry Collector).
Включение трассировки
В приложениях ASP.NET Core добавьтеActivitySource драйвера ClickHouse в конфигурацию OpenTelemetry:
Атрибуты спана
Каждый спан включает стандартные атрибуты OpenTelemetry для базы данных, а также специфичную для ClickHouse статистику запросов, которую можно использовать для отладки.Параметры конфигурации
Настройте поведение трассировки с помощьюClickHouseDiagnosticsOptions:
Настройка TLS
При подключении к ClickHouse по HTTPS поведение TLS/SSL можно настроить несколькими способами.Пользовательская проверка сертификатов
Для продакшн-окружений, в которых требуется пользовательская логика проверки сертификатов, передайте собственныйHttpClient с настроенным обработчиком ServerCertificateCustomValidationCallback:
Важные моменты при использовании пользовательского HttpClient
- Автоматическая декомпрессия: оставьте
AutomaticDecompressionвыключенным. Драйвер сам декодирует сжатые ответы, поэтому она не нужна — и её включение играет против вас на стороне запроса: при отправке обработчик дополнительно добавляет каждый алгоритм из своей маски в исходящий заголовокAccept-Encoding, расширяя набор, объявленный драйвером, так что ClickHouse может ответить кодеком, который вы не запрашивали. См. Декомпрессия ответа. - Тайм-аут простоя: Установите
PooledConnectionIdleTimeoutменьше значенияkeep_alive_timeoutсервера (10 секунд для ClickHouse Cloud), чтобы избежать ошибок подключения из-за полуоткрытых соединений.
Настройка производительности
В этом разделе описывается, как добиться оптимальной производительности при работе с клиентом, а также какие параметры можно настроить, чтобы клиент работал эффективно в вашем конкретном сценарии использования.Кратко
| Если вы | Сделайте так | |---|---|---| | Читаете строки в POCO | ИспользуйтеQueryAsync<T>, а не MapTo<T> |
| Выполняете крупные вставки | Увеличьте InsertOptions.BatchSize |
| Запускаете консольное или воркер-приложение с интенсивными вставками | Включите Server GC |
| Читаете большие результаты по сети | Оставьте сжатие ответов включённым (как по умолчанию) |
| Вставляете данные по быстрому каналу | Попробуйте InsertOptions.Compressor = null |
| Много раз вставляете в одну и ту же таблицу | Используйте UseSchemaCache или ColumnTypes |
| Читаете очень большие результаты | Увеличьте ReadBufferSize |
Чтение: выбор пути материализации
Получить строку из результата можно тремя способами, и обходятся они по-разному. Некоторые из путей упаковывают значения, что увеличивает число выделений памяти и снижает производительность.
Для чтения 1 000 000 строк по 105 столбцам набора данных hits:
ORM получают быстрый путь, когда используют типизированные аксессоры. linq2db регистрирует
GetInt64,
GetDouble и GetDateTime для каждого столбца, поэтому чтение выполняется без упаковки. Код, который читает через
GetValue (включая результат типа dynamic из Dapper), упаковывает каждое значение. Если запрос ORM выполняется часто
и читает через GetValue, используйте для него QueryAsync<T>.Вставка: размер батча и параллелизм
Размер батча — главный фактор, влияющий на пропускную способность вставки. ЗначениеInsertOptions.BatchSize по умолчанию — 100 000 строк.
Используйте большие батчи. При вставке 1 000 000 строк увеличение размера батча с 10 000 до 100 000 строк дало:
Если размером батча управлять нельзя (например, когда множество мелких producer’ов отправляют строки независимо друг от друга), используйте async inserts и доверьте батчинг серверу.
Параллельная отправка. Значение
InsertOptions.MaxDegreeOfParallelism по умолчанию равно 1. Увеличьте его, чтобы отправлять батчи одновременно. Наибольший эффект это даёт при включённом сжатии, поскольку каждый батч сжимается в собственном потоке. Сеансы с параллельными вставками не работают: либо отключите сеансы, либо оставьте MaxDegreeOfParallelism = 1.
Уберите schema probe. Каждый вызов InsertBinaryAsync сначала отправляет запрос SELECT ... WHERE 1=0, чтобы определить типы столбцов. См. Пропуск запроса schema probe — это позволит исключить лишний обмен с сервером с помощью ColumnTypes или UseSchemaCache.
Путь вставки без упаковки значений применяется к формату
RowBinary, используемому по умолчанию. RowBinaryWithDefaults вынужден проверять каждое значение на наличие маркера DBDefault, поэтому для него сохраняется более медленный путь.Сжатие: два направления передачи ведут себя по-разному
Сжатие меняет CPU на байты. Выгоден ли такой обмен, зависит от направления передачи, пропускной способности соединения с сервером ClickHouse, от того, насколько хорошо ваши данные поддаются выбранному алгоритму сжатия, а также от того, платите ли вы за каждый переданный байт. Чтение: оставьте сжатие включённым, если только сервер не работает на той же машине. Это поведение по умолчанию. По сравнению с передачей без сжатияzstd на уровне 1 дал:
Вставки: сначала измерьте, потом включайте сжатие. Экономия может оказаться недостаточной, чтобы оправдать его включение. Учитывайте также, что распаковка создаёт дополнительную нагрузку на сервер: для Zstd и LZ4 она умеренная, но для других алгоритмов (например, Brotli) может быть высокой.
Чтобы отключить сжатие при вставке:
Буферы
ReadBufferSize задаёт размер буфера, в который считываются HTTP-ответы. Значение по умолчанию — 64 КиБ.
Драйвер берёт этот буфер из общего пула и возвращает обратно при освобождении reader, поэтому память не выделяется заново для каждого запроса. Увеличьте размер буфера, чтобы сократить число его перезаполнений при больших результатах. Драйвер удерживает по одному буферу на каждый одновременно открытый reader, поэтому потребление памяти растёт вместе с размером буфера и числом параллельных reader.
Среда выполнения и сборка мусора
Включайте Server GC для приложений с интенсивной вставкой данных. При одном и том же коде и одинаковом количестве выделенных байтов Workstation GC работал на вставках до 97% медленнее, чем Server GC.Server GC — это настройка пропускной способности, а не задержки. В тех же измерениях Server GC суммарно провёл в паузах менее половины времени, но отдельные паузы были длиннее (95-й перцентиль — 114,6 мс против 61,9 мс). Если ваш сервис чувствителен к задержкам в хвосте распределения, измерьте оба режима, прежде чем делать выбор.
Задержка: переиспользуйте соединения
Установление нового TCP-соединения и выполнение TLS-рукопожатия занимает значительное время. Переиспользование соединений существенно снизит задержку ваших запросов.- Не создавайте отдельный клиент для каждого запроса. Каждый новый клиент со своим
HttpClientсоздаёт новый пул соединений и снова тратит время на рукопожатие. Используйте одинClickHouseClientна всё время жизни приложения. Он потокобезопасен и рассчитан на использование в качестве singleton. - Для ADO.NET и ORM используйте
ClickHouseDataSource, чтобы все подключения использовали общий пул.
Измеряйте сами
Во многих случаях производительность зависит от структуры ваших данных, скорости соединения с сервером, от того, готовы ли вы разменять CPU клиента на CPU сервера (или наоборот), от ограничений вашего оборудования и т. д. Поэтому рекомендуется измерять производительность самостоятельно, на своих данных и в своём окружении. Чтобы увидеть, какую часть работы выполняет сервер, задайтеQueryOptions.QueryId и прочитайте счётчики:
Поддержка ORM
Для ORM требуется API ADO.NET (ClickHouseConnection). Чтобы корректно управлять временем жизни подключения, создавайте подключения через ClickHouseDataSource:
Dapper
ClickHouse.Driver работает с Dapper. Драйвер автоматически преобразует синтаксис Dapper @parameter в нативный для ClickHouse синтаксис {parameter:Type}, при этом типы выводятся автоматически на основе значений .NET.
Используйте ClickHouseDataSource для корректного управления временем жизни соединения:
Способы передачи параметров
Поддерживаются все стандартные способы передачи параметров в Dapper: Анонимные объекты:DynamicParameters (из словаря или анонимного объекта):
Запросы в объекты POCO
Dapper сопоставляет столбцы со свойствами по имени (регистронезависимо):Собственный синтаксис параметров ClickHouse
Если вам нужен явный контроль над типами, используйте непосредственно в SQL синтаксис ClickHouse{param:Type}, а значения параметров передавайте через Dictionary<string, object>. Не используйте синтаксис @param и синтаксис {param:Type} одновременно для одного и того же параметра.
WHERE IN
Встроенное в Dapper раскрытие IN работает:WHERE id IN (@Ids1, @Ids2, @Ids3), а драйвер обрабатывает каждый развёрнутый параметр.
Функция has() в ClickHouse с параметром Array тоже работает:
Пользовательские обработчики типов
Для некоторых типов ClickHouse, напримерITuple, BigInteger и ClickHouseDecimal, необходимо зарегистрировать обработчики при запуске:
Dapper.Contrib
GetAll<T>() и Get<T>(id) работают. Insert<T>() не работает — этот метод генерирует синтаксис SQL Server (SCOPE_IDENTITY, []). Вместо него рекомендуется использовать нативный метод InsertBinaryAsync клиента ClickHouseClient.
Ограничения
Linq2db
Этот драйвер совместим с linq2db — легковесным ORM и LINQ-провайдером для .NET. Подробную документацию см. на сайте проекта. Пример использования: СоздайтеDataConnection, используя провайдер ClickHouse:
BulkCopyAsync для эффективной пакетной вставки.
Entity Framework Core
Официальный провайдер Entity Framework Core для ClickHouse. Сопоставляйте классы C# с таблицами ClickHouse, выполняйте запросы с помощью LINQ и добавляйте данные черезSaveChanges — всё это в привычных шаблонах EF Core.
- NuGet:
ClickHouse.EntityFrameworkCore - Source: GitHub
Этот провайдер активно развивается. Текущая версия поддерживает LINQ-запросы (включая JOIN, подзапросы и операции над множествами),
INSERT через SaveChanges / BulkInsertAsync, миграции с полной поддержкой DDL (CREATE / ALTER / DROP), а также настройку движка таблицы ClickHouse. UPDATE / DELETE не поддерживаются.Установка
Быстрый старт
Определите сущность иDbContext, затем выполните запрос с помощью LINQ:
Поддерживаемые типы
Используйте
ClickHouseDecimal (из ClickHouse.Driver.Numerics) вместо decimal, если нужна полная точность столбцов Decimal128/Decimal256: decimal в .NET ограничен 28–29 значащими цифрами.
Поддерживаемые операции LINQ
Запросы:Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking
GROUP BY и агрегатные функции: GroupBy с Count, LongCount, Sum, Average, Min, Max — включая HAVING (.Where() после .GroupBy()), несколько агрегатных функций в одной проекции и OrderBy по результатам агрегации.
JOIN: Join (INNER), шаблоны GroupJoin/SelectMany (LEFT и CROSS). LEFT JOIN возвращает реальный null для строк без совпадений (см. семантику NULL в LEFT JOIN ниже).
Подзапросы: коррелированные Contains / IN, Any / EXISTS, All, а также скалярные подзапросы в проекциях.
Операции над множествами: 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), отключите это поведение с помощью:
0 / "" вместо == null.
Вставка данных
SaveChanges использует нативный API драйвера InsertBinaryAsync — кодирование RowBinary со сжатым телом запроса, что гораздо эффективнее параметризованного SQL:
Added в Unchanged, как и у любого другого поставщика EF Core.
Размер батча можно настроить (по умолчанию — 1000):
Пакетная вставка
Для высоконагруженной вставки данных используйтеBulkInsertAsync вместо SaveChanges. Это метод расширения для DbContext, который полностью обходит механизм отслеживания изменений EF Core, разрешение идентичности и управление состоянием — он напрямую вызывает InsertBinaryAsync драйвера с кодированием RowBinary и сжатым телом запроса.
Поэтому он хорошо подходит для загрузки больших наборов данных, когда после вставки отслеживание сущностей не требуется:
IEnumerable<T> — сущности обрабатываются последовательно, без загрузки всех данных в память. Возвращаемое значение — количество вставленных строк. Сущности не прикрепляются к DbContext после вставки, поэтому перехода состояния Added → Unchanged не происходит.
Перечисления
Столбцы ClickHouseEnum8/Enum16 можно сопоставить со свойствами string или типами C# enum. При использовании перечислений C# провайдер автоматически преобразует значения перечисления в их строковое представление и обратно:
Пользовательские преобразования типов
СистемаValueConverter в EF Core позволяет сопоставлять пользовательские типы с типами, которые уже поддерживает провайдер. Сам провайдер ваш пользовательский тип не видит — EF Core выполняет преобразование на границе.
Преобразование для отдельного свойства:
Аннотации типов столбцов
Для скалярных типов, таких какstring, int, DateTime и т. д., провайдер автоматически определяет тип ClickHouse. Для параметризованных типов и типов-обёрток необходимо явно указать тип ClickHouse.
Использование аннотаций данных (атрибутов):
OnModelCreating:
Array(Nullable(Int32)) и LowCardinality(Nullable(String)) — провайдер автоматически снимает обёртки Nullable и LowCardinality на каждом уровне вложенности.
Столбцы Variant и Dynamic
Столбцы ClickHouseVariant(T1, T2, ...) и Dynamic в .NET сопоставляются с object. Поскольку object — слишком общий тип для автоматического вывода типов, необходимо явно указать тип хранения через .HasColumnType():
string, ulong, ulong[]).
JSON-столбцы
Провайдер поддерживает тип столбцаJson в ClickHouse, сопоставляя его с System.Text.Json.Nodes.JsonNode (по умолчанию) или string (через автоматический ValueConverter):
SaveChanges, так и через BulkInsertAsync:
string, а для столбца — тип Json — ValueConverter будет применён автоматически:
- Пути JSON не транслируются —
entity.Data["name"]в LINQ не преобразуется в SQL-синтаксис ClickHousedata.name. Фильтруйте по не-JSON-столбцам и анализируйте JSON в памяти. - Семантика NULL — JSON type в ClickHouse возвращает
{}(пустой объект) для значений NULL, а не SQL NULL. - Точность целых чисел — ClickHouse хранит все целые числа в JSON как
Int64. При чтении черезJsonNodeиспользуйтеGetValue<long>(), а неGetValue<int>().
Движки таблиц
Настраивайте движки таблиц ClickHouse и специфичные для движка секции через fluent APIToTable(name, t => ...). Если движок не настроен, провайдер по умолчанию использует MergeTree, а ORDER BY определяется на основе первичного ключа сущности.
Секции движка:
WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Все они применяются к построителю движка, который возвращает HasXxxEngine().
Возможности на уровне столбца: HasCodec, HasTtl, HasComment, HasDefault — все они участвуют в миграциях.
Индексы пропуска данных — через HasIndex(...).HasSkippingIndexType(...):
Миграции
Стандартный процесс миграций EF Core:Ограничения миграций
Помимо миграций, провайдер также пока не поддерживает:
UPDATE/DELETE- Транзакции:
BeginTransaction— no-op. ClickHouse не поддерживает ACID-транзакции. - Трансляцию запросов с JSON-путём:
entity.Data["key"]в LINQ не транслируется в SQL-синтаксис ClickHousedata.key. Фильтруйте по не-JSON-столбцам, а JSON анализируйте в памяти.
Ограничения
Tuple из 8+ элементов с вложенным кортежем на последней позиции
Типы C#ValueTuple, содержащие более 7 элементов, используют схему вложенности, генерируемую компилятором: 8-й универсальный аргумент (TRest) сам является ValueTuple, содержащим оставшиеся элементы. Например, (int, int, int, int, int, int, int, string, string) компилируется в ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
Из-за этого возникает неоднозначность, когда столбец ClickHouse представляет собой 8-элементный кортеж, у которого последний элемент сам является кортежем — например, Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). Драйвер не может различить:
- Плоский 9-элементный кортеж (вложенность TRest, сгенерированная компилятором)
- 8-элементный кортеж, у которого последний элемент — вложенный
Tuple(String, String)
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
Драйвер обрабатывает 8-й аргумент как TRest (то есть разворачивает его), а значит, случай с 8 элементами и вложенным кортежем будет сериализован некорректно.
Это затрагивает как System.Tuple, так и ValueTuple, поскольку оба используют вложенность TRest для случаев с более чем 7 элементами. Tuple с 7 или меньшим числом элементов, а также кортежи, у которых последний элемент сам по себе не является кортежем, этой проблеме не подвержены.
Обходной путь: оберните внутренний кортеж в дополнительный слой, чтобы драйвер мог отличить его от вложенности TRest:
Столбцы AggregateFunction
Столбцы типаAggregateFunction(...) нельзя запрашивать или напрямую вставлять в них данные.
Чтобы выполнить вставку: