-
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и использует в качестве ключа пару (база данных, таблица). Разные наборы столбцов одной таблицы используют одну и ту же схему из кэша.
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, а в теле запроса оставлять только строки данных:
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":
ID запроса
Каждому запросу присваивается уникальныйquery_id. С его помощью можно получать данные из таблицы system.query_log или отменять долго выполняющиеся запросы. Собственный ID запроса можно задать через QueryOptions:
Пользовательское сопоставление типов параметров
При использовании параметров вида@ (например, WHERE id = @id) драйвер автоматически определяет тип ClickHouse на основе типа значения .NET. Например, типу int соответствует Int32.
Чтобы изменить поведение по умолчанию, задайте ParameterTypeResolver в ClickHouseClientSettings. Например, так можно использовать DateTime64(3) для всех параметров DateTime, обеспечив точность до миллисекунд, или задать одинаковое число знаков после запятой для всех десятичных значений — без указания ClickHouseType для каждого параметра.
Использование DictionaryParameterTypeResolver для простого сопоставления типов:
IParameterTypeResolver для сложных сценариев:
Если тип параметра нужно определять по его значению или имени, реализуйте интерфейс IParameterTypeResolver напрямую. Верните null, чтобы перейти к определению типа по умолчанию:
QueryOptions.ParameterTypeResolver. В этом случае он имеет приоритет над резолвером на уровне клиента.
Порядок выбора типа:
Резолвер — лишь одно из звеньев в порядке выбора типа. От наивысшего приоритета к низшему:
ClickHouseType, явно заданный для параметра- Подсказка типа SQL, указанная в запросе с помощью синтаксиса
{name:Type} IParameterTypeResolver(изQueryOptions.ParameterTypeResolver, а если он не задан — изClickHouseClientSettings.ParameterTypeResolver)- Встроенный вывод типов (
TypeConverter.ToClickHouseType)
ClickHouseConnection через ADO.NET: соединения, созданные клиентом, наследуют его настройки.
Пользовательское форматирование значений параметров
IParameterFormatter — механизм расширения, который определяет, как сериализуются значения параметров. Используйте его, если встроенное форматирование (например, точность DateTime, представление десятичных чисел с учётом региональных настроек, экранирование строк или представление чисел) не соответствует требованиям вашей схемы или систем, в которые передаются данные.
Задайте ParameterFormatter в ClickHouseClientSettings, чтобы применять форматтер ко всем параметризованным запросам. Форматтер получает значение, определённое имя типа ClickHouse и имя параметра и возвращает строковое представление для отправки на сервер. Верните null, чтобы передать обработку форматтеру по умолчанию.
Использование DictionaryParameterFormatter для простого форматирования по типам CLR:
IParameterFormatter для сложных сценариев:
QueryOptions.ParameterFormatter. Такой форматтер имеет приоритет над форматтером на уровне клиента.
Составные значения:
Форматтер применяется и к параметрам-коллекциям верхнего уровня, и к каждому элементу составных значений (Array, Tuple, Map, Nullable, LowCardinality, Variant). Например, при сопоставлении с typeof(int) каждый элемент типа Int32 в Array(Int32) форматируется отдельно.
Заключение в одинарные кавычки в составных значениях:
Для строковых типов ClickHouse (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) внутри составного литерала драйвер заключает результат работы форматтера в одинарные кавычки, но не экранирует его содержимое. Если возвращённая строка содержит неэкранированную одинарную кавычку или обратную косую черту, составной литерал окажется некорректным, и сервер отклонит запрос.
Строковые параметры верхнего уровня (не входящие в составное значение) используются как есть, без заключения в кавычки, поэтому экранировать их не требуется.
Приоритет форматтеров:
IParameterFormatter(изQueryOptions.ParameterFormatter, а если он не задан — изClickHouseClientSettings.ParameterFormatter). Если он возвращает значение, отличное от null, используется это значение.- Встроенное форматирование по типу в
HttpParameterFormatter.
null и DBNull: они всегда сериализуются как маркер null в ClickHouse (\N).
Пользовательское преобразование значений при чтении
IReadValueConverter позволяет преобразовывать значения, возвращаемые считывателем данных после десериализации, не меняя их тип CLR. Например, с его помощью можно задать DateTime.Kind = Utc для столбца DateTime без часового пояса, удалить лишние символы из строк или нормализовать их, а также дополнительно обработать JSON-столбец перед передачей в прикладной код.
Чтобы использовать преобразователь при каждом чтении, задайте ReadValueConverter в ClickHouseClientSettings. Преобразователь вызывается для каждого значения столбца в каждой строке — как при получении значения в упакованном виде (GetValue), так и через обобщённый метод (GetFieldValue<T>). Если преобразователь не задан, дополнительных накладных расходов нет: считыватель возвращает значения напрямую.
Использование DictionaryReadValueConverter для простого преобразования значений в зависимости от типа CLR:
For<T>, передаются без изменений. Преобразователь выбирается по точному совпадению CLR-типа, поэтому зарегистрируйте тип, который фактически возвращает считыватель (например, For<JsonObject> для JSON-столбца в режиме JsonReadMode.Binary).
Пользовательская реализация IReadValueConverter для сложных сценариев:
Если преобразователь нужно выбирать по строке типа ClickHouse (например, чтобы различать DateTime и DateTime('UTC'), которые представлены одним и тем же CLR-типом), реализуйте интерфейс IReadValueConverter напрямую:
GetFieldType, GetSchemaTable) через него не обрабатываются и должны соответствовать возвращаемому значению.
Преобразователь можно также задать для отдельного запроса через QueryOptions.ReadValueConverter. В этом случае он имеет приоритет над преобразователем на уровне клиента.
Граница применения:
Преобразователь вызывается один раз для каждого столбца и получает десериализованное значение ячейки целиком; составные контейнеры он не обрабатывает рекурсивно. Для столбца Array(Int32) ему передаётся int[], а для Tuple(Int32, String) — ITuple.
Какая перегрузка вызывается:
Обе перегрузки должны работать согласованно: какую из них вызовет драйвер, зависит от того, как вызывающий код считывает
столбец:
ConvertValue<T>— типизированные аксессорыGetByte,GetSByte,GetInt16/32/64,GetUInt16/32/64,GetFloat,GetDouble,GetGuid,GetDateTime,GetIPAddress,GetBigIntegerиGetFieldValue<T>, а также все столбцы, считываемые без упаковки значений через механизм чтения POCO.ConvertValue(с упаковкой значения) —GetValue,GetValues, индексаторы,GetChar,GetTupleи пути чтения с приведением типов вGetBoolean,GetDecimalиGetString.
IsDBNull вообще не вызывает преобразователь: он считывает признак 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 не удаляется без вашего ведома, а исходное тело ответа поступает к вам именно в том виде, в каком его отправил сервер.
Тела ответов с ошибками
Если сервер возвращает ответ с кодом 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: без этого 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 сервера и 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-файла:
Параметры управления ингестией данных описаны в документации по настройкам форматов.
Дополнительные примеры
Другие примеры практического использования см. в каталоге примеров репозитория GitHub.ADO.NET
Библиотека полностью поддерживает ADO.NET с помощьюClickHouseConnection, ClickHouseCommand и ClickHouseDataReader. Этот API необходим для интеграции с ORM (Dapper, Linq2db) и использования стандартных абстракций .NET для работы с базами данных.
Управление жизненным циклом с помощью ClickHouseDataSource
Всегда создавайте соединения черезClickHouseDataSource, чтобы обеспечить корректное управление их жизненным циклом и использование пула соединений. DataSource содержит один экземпляр ClickHouseClient, пул HTTP-соединений которого используется всеми соединениями.
Использование ClickHouseCommand
Создавайте команды через соединение, чтобы выполнять SQL-запросы:ExecuteNonQueryAsync()- Для выполнения операторов INSERT, UPDATE, DELETE и DDLExecuteScalarAsync()- Возвращает значение первого столбца первой строки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.
Работа с DateTime
-
По возможности используйте UTC. Храните временные метки в столбцах
DateTime('UTC')и используйтеDateTimeKind.Utcв коде. Это устраняет неоднозначность часовых поясов. -
Используйте
DateTimeOffsetдля явного учёта часового пояса. Этот тип всегда обозначает конкретный момент времени и содержит смещение относительно UTC. -
Указывайте часовой пояс в подсказках типов SQL. Если вы передаёте значения DateTime с
Unspecifiedв качестве параметров для столбцов с часовым поясом, отличным от UTC, укажите часовой пояс в SQL:
Асинхронные вставки
Асинхронные вставки перекладывают группировку данных в пакеты с клиента на сервер. Клиенту больше не нужно группировать данные: сервер буферизует поступающие данные и сбрасывает их в хранилище при достижении настраиваемых пороговых значений. Это полезно при высокой степени параллелизма, например для задач обсервабилити, когда множество агентов отправляют небольшие порции данных. Включите асинхронные вставки черезCustomSettings или строку подключения:
wait_for_async_insert):
Основные настройки:
Сеансы
Включайте сеансы только тогда, когда вам нужны серверные возможности с сохранением состояния, например:- Временные таблицы (
CREATE TEMPORARY TABLE) - Сохранение контекста запроса при выполнении нескольких команд
- Настройки на уровне сеанса (
SET max_threads = 4)
Поддерживаемые типы данных
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. Так сохраняется исходное локальное время без каких-либо предположений о часовом поясе.
Если при работе со столбцами без явно указанного часового пояса необходимо учитывать часовой пояс, выполните одно из следующих действий:
- Укажите часовой пояс в определении столбца:
DateTime('UTC')илиDateTime('Europe/Amsterdam') - Задайте часовой пояс самостоятельно после чтения.
Тип 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, так и в случае, когда такого
пути нет в документе. Различить эти случаи невозможно:
JSON(x String)
даёт {"x":""}, а JSON(x Int64) — {"x":0}.
Динамический путь со значением null полностью исключается из объекта, поэтому ContainsKey возвращает
для него false. При чтении {"x":null} из обычного JSON-столбца получается {}.
Вложенные типизированные пути создают родительские узлы, поэтому JSON(a.b Nullable(Int64)) даёт {"a":{"b":null}}
даже для пустого документа.
Именно так объект отображает сам сервер, поэтому режимы
Binary и String теперь дают одинаковый результат. До версии 1.4.0
типизированный путь со значением null удалялся из JsonObject, из-за чего {"x":null} при чтении
превращался в {}. Для вложенного пути, например JSON(a.b Nullable(Int64)), исчезало всё поддерево a.JSON-столбца всегда возвращаются как текст, независимо от значения
ReadStringsAsByteArrays: у JsonValue нет представления в виде массива байтов, поэтому byte[]
отображался бы в кодировке base64. Это относится к String, FixedString, типам, обёрнутым в
LowCardinality, Nullable или SimpleAggregateFunction, а также к строкам внутри Array и Map,
включая ключи карт.
Если считыватель JSON не может определить тип массива байтов, тот всё равно отображается в кодировке base64:
путь типа
Variant или Dynamic содержит значение, тип которого определяется отдельно для каждой строки, поэтому строка
в Variant(Array(UInt8), String) возвращается в кодировке base64. Это справедливо при обоих значениях настройки.Если тип ключа карты JSON не совпадает в точности с String — например, это Map(LowCardinality(String), String) —
возникает NotSupportedException.JSON(a Int64, a.b Int64). Оба пути присутствуют в каждой строке, поэтому сервер
формирует строку с повторяющимся ключом: {"a":0,"a":{"b":7}}. JsonObject не может содержать два значения
для одного ключа, поэтому JsonReadMode.Binary генерирует SerializationException с указанием обоих путей. То
же происходит, если значение имеет тип Map, как в JSON(a Map(String, Int64)), а в считываемой строке
также есть динамический путь a.b.
Это относится только к строкам, в которых оба пути содержат значения. Если в одном из них нет значения — null,
пустой объект или поддерево, все значения которого равны null, — сохраняется значение другого пути
независимо от того, какой из них сервер отправляет первым. Поэтому при пересечении путей с типами Nullable в каждой строке значение берётся только из одного пути, и чтение проходит без
ошибки: JSON(a Nullable(Int64), a.b Nullable(Int64)) даёт {"a":5} и {"a":{"b":7}}, как
и ожидается.
Чтобы получить 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>>в порядке отправки пар сервером. При этом сохраняются все пары, в том числе с повторяющимися ключами.
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(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 .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 строк
дало следующие результаты:
Если вы не можете управлять размером батча (например, когда множество источников независимо друг от друга отправляют небольшие порции строк), используйте асинхронные вставки: сервер сам объединит строки в батчи.
Параллельная отправка. По умолчанию
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 КиБ.
Драйвер берёт буфер из общего пула и возвращает его при освобождении считывателя, поэтому
выделять память под буфер для каждого запроса не требуется. Увеличьте размер буфера, чтобы сократить число его повторных заполнений при обработке больших
результатов. Драйвер использует отдельный буфер для каждого открытого считывателя, поэтому потребление памяти
растёт как с размером буфера, так и с числом одновременно работающих считывателей.
Среда выполнения и сборка мусора
Включите Server GC для приложений, которые часто выполняют вставку данных. При одном и том же коде и одинаковом объёме выделенной памяти вставка данных с Workstation GC выполнялась до 97 % медленнее, чем с Server 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: Анонимные объекты:DynamicParameters (на основе словаря или анонимного объекта):
Получение результатов запросов в виде POCO
Dapper сопоставляет столбцы со свойствами по именам (без учёта регистра):Нативный синтаксис параметров ClickHouse
Если нужно явно задать тип, используйте синтаксис ClickHouse{param:Type} непосредственно в SQL, передавая значения параметров в Dictionary<string, object>. Не используйте одновременно синтаксис @param и {param:Type} для одного и того же параметра.
WHERE IN
Штатный механизм развёртывания IN в Dapper работает:WHERE id IN (@Ids1, @Ids2, @Ids3), а драйвер преобразует каждый параметр, полученный при развёртывании.
Функция ClickHouse has() также работает с параметром типа Array:
Пользовательские обработчики типов
Для некоторых типов ClickHouse, напримерITuple, BigInteger и ClickHouseDecimal, необходимо регистрировать обработчики при запуске:
Dapper.Contrib
GetAll<T>() и Get<T>(id) работают. Insert<T>() не работает: этот метод генерирует код в синтаксисе SQL Server (SCOPE_IDENTITY, []). Вместо него рекомендуется использовать метод InsertBinaryAsync, предоставляемый непосредственно клиентом ClickHouseClient.
Ограничения
Linq2db
Этот драйвер совместим с linq2db — легковесной ORM и LINQ-провайдером для .NET. Подробную документацию см. на сайте проекта. Пример использования: СоздайтеDataConnection с помощью провайдера ClickHouse:
BulkCopyAsync.
Entity Framework Core
Официальный провайдер Entity Framework Core для ClickHouse. Он позволяет сопоставлять классы C# с таблицами ClickHouse, выполнять запросы с помощью LINQ и вставлять данные черезSaveChanges — всё с использованием привычных механизмов EF Core.
- NuGet:
ClickHouse.EntityFrameworkCore - Исходный код: GitHub
Провайдер активно развивается. Текущая версия поддерживает запросы LINQ (в том числе JOIN, подзапросы и операции над множествами),
INSERT через SaveChanges / BulkInsertAsync, миграции с полной поддержкой DDL (CREATE / ALTER / DROP), а также настройку движков таблиц ClickHouse. UPDATE / DELETE не поддерживаются.Установка
Быстрый старт
Определите сущность и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), отключите её автоматическое добавление:
== 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 нужно указать явно.
С помощью атрибутов аннотаций данных:
OnModelCreating:
Array(Nullable(Int32)) и LowCardinality(Nullable(String)): провайдер автоматически снимает обёртки Nullable и LowCardinality на каждом уровне вложенности.
Столбцы типов Variant и Dynamic
Столбцы ClickHouse типовVariant(T1, T2, ...) и Dynamic сопоставляются с типом object в .NET. Поскольку object слишком универсален для автоматического вывода типов, тип столбца в базе данных необходимо задать явно с помощью .HasColumnType():
string, ulong, ulong[]).
JSON-столбцы
Провайдер поддерживает столбцы ClickHouse типаJson: по умолчанию они сопоставляются с System.Text.Json.Nodes.JsonNode, а при использовании автоматического ValueConverter — со string:
SaveChanges, так и с помощью BulkInsertAsync:
string, а для столбца — Json. Провайдер автоматически применит ValueConverter:
- Отсутствие преобразования JSON-путей — выражение
entity.Data["name"]в LINQ не преобразуется в SQL-синтаксис ClickHousedata.name. Фильтруйте данные по столбцам, не содержащим JSON, а JSON обрабатывайте в памяти. - Семантика NULL — для значений NULL JSON-тип ClickHouse возвращает
{}(пустой объект), а не 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не имеет эффекта. ACID-транзакции в ClickHouse не поддерживаются. - Преобразование запросов с JSON-путями: выражение
entity.Data["key"]в LINQ не преобразуется в SQL-синтаксис ClickHousedata.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)
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(...) нельзя запрашивать напрямую или напрямую вставлять в них данные.
Чтобы вставить данные: