> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-vortex-format.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Официальный клиент для подключения к ClickHouse по HTTP на C#.

# HTTP-клиент ClickHouse для C#

export const Image = ({img, alt, size = "lg", background}) => {
  const normalizedSize = ["sm", "md", "lg"].includes(size) ? size : "lg";
  const backgroundColor = background === "white" ? "white" : background === "black" ? "rgb(31 31 28)" : undefined;
  return <div className={`ch-image-${normalizedSize}`}>
      <Frame>
        <img src={img} alt={alt} style={{
    backgroundColor
  }} />
      </Frame>
    </div>;
};

Официальный C#-клиент для подключения к ClickHouse.
Исходный код клиента доступен в [репозитории GitHub](https://github.com/ClickHouse/clickhouse-cs).
Первоначальный разработчик — [Oleg V. Kozlyuk](https://github.com/DarkWanderer).

Библиотека предоставляет два основных API:

* **`ClickHouseClient`** (рекомендуется): высокоуровневый потокобезопасный клиент, предназначенный для использования в качестве singleton. Предоставляет простой асинхронный API для выполнения запросов и массовой вставки данных. Подходит для большинства приложений.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`): стандартные абстракции .NET для работы с базами данных. Они необходимы для интеграции с ORM (Dapper, Linq2db) и в случаях, когда требуется совместимость с ADO.NET. `ClickHouseBulkCopy` — вспомогательный класс для эффективной вставки данных через соединение ADO.NET. `ClickHouseBulkCopy` устарел и будет удалён в одном из будущих выпусков; вместо него используйте `ClickHouseClient.InsertBinaryAsync`.

Оба API используют один и тот же пул HTTP-соединений и могут применяться совместно в одном приложении.

<h2 id="migration-guide">
  Руководство по миграции
</h2>

1. Укажите в файле `.csproj` новое имя пакета `ClickHouse.Driver` и [последнюю версию из NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. Замените в кодовой базе все ссылки на `ClickHouse.Client` на `ClickHouse.Driver`.

***

<h2 id="supported-net-versions">
  Поддерживаемые версии .NET
</h2>

`ClickHouse.Driver` поддерживает следующие версии .NET:

* .NET 6.0
* .NET 8.0
* .NET 9.0
* .NET 10.0

<h2 id="supported-clickhouse-versions">
  Поддерживаемые версии ClickHouse
</h2>

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

<h2 id="installation">
  Установка
</h2>

Установите пакет из NuGet:

```bash theme={null}
dotnet add package ClickHouse.Driver
```

Или через NuGet Package Manager:

```bash theme={null}
Install-Package ClickHouse.Driver
```

<h2 id="quick-start">
  Быстрый старт
</h2>

```csharp theme={null}
using ClickHouse.Driver;

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

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

<h2 id="configuration">
  Конфигурация
</h2>

Соединение с ClickHouse можно настроить двумя способами:

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

Ниже приведён полный список настроек, их значений по умолчанию и описание того, на что они влияют.

<h3 id="connection-settings">
  Настройки соединения
</h3>

| Параметр | Тип | Значение по умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| Хост | `string` | `"localhost"` | `Host` | Имя хоста или IP-адрес сервера ClickHouse |
| Порт | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | Номер порта; порт по умолчанию зависит от протокола |
| Имя пользователя | `string` | `"default"` | `Username` | Имя пользователя для аутентификации |
| Пароль | `string` | `""` | `Password` | Пароль для аутентификации |
| База данных | `string` | `""` | `Database` | База данных по умолчанию; если значение пустое, используется база данных по умолчанию для сервера или пользователя |
| Протокол | `string` | `"http"` | `Protocol` | Протокол соединения: `"http"` или `"https"` |
| Путь | `string` | `null` | `Path` | Путь URL при использовании обратного прокси (например, `/clickhouse`) |
| Тайм-аут | `TimeSpan` | 2 минуты | `Timeout` | Тайм-аут операции (в строке подключения указывается в секундах) |

<h3 id="data-format-serialization">
  Формат данных и сериализация
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Управляет сжатием при передаче в обоих направлениях для обычного запроса: запрашивает у сервера сжатие ответа (`enable_http_compression`; кодек задаётся через `AcceptEncoding`, явное значение которого может запросить сжатие даже при отключённом параметре) **и** сжимает тело запроса с помощью gzip — за исключением режима `UseFormDataParameters`, в котором multipart-тело всегда отправляется без сжатия. Бинарные вставки этот параметр не учитывают: они используют `InsertOptions.Compressor` — см. [Сжатие при вставке](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | Заголовок `Accept-Encoding`, отправляемый с каждым запросом; заменяет кодеки, которые драйвер объявляет по умолчанию (`zstd, lz4, gzip, deflate`). Ответ сервера прозрачно декодируется, в каком бы формате он ни пришёл. См. [Распаковка ответа](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Использовать `ClickHouseDecimal` для произвольной точности; при значении false используется .NET `decimal` (не более 128 бит) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | Читать столбцы `String` и `FixedString` как `byte[]` вместо `string`; полезно для бинарных данных |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | Передавать параметры как данные формы, а не в строке запроса URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Размер (в байтах) буфера для чтения HTTP-ответов на запросы. Драйвер берёт буфер из общего пула и возвращает его при освобождении считывателя, поэтому память не выделяется заново на каждый запрос. Увеличьте значение, чтобы реже перезаполнять буфер при больших результирующих наборах. Драйвер удерживает по одному буферу на каждый параллельный считыватель, поэтому потребление памяти растёт вместе с размером буфера и числом параллельных считывателей. См. [Буферы](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Пользовательский резолвер для сопоставления типов параметров в стиле `@`; см. [Пользовательское сопоставление типов параметров](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Пользовательский форматтер для сериализации значений параметров; см. [Пользовательское форматирование значений параметров](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Пользовательское преобразование значений, возвращаемых считывателем данных; см. [Пользовательское преобразование значений при чтении](#read-value-conversion) |
| JsonReadMode | `JsonReadMode` | `Binary` | `JsonReadMode` | Формат возврата данных JSON: `Binary` (возвращает `JsonObject`) или `String` (возвращает исходную строку JSON) |
| JsonWriteMode | `JsonWriteMode` | `String` | `JsonWriteMode` | Формат отправки данных JSON: `String` (сериализация через `JsonSerializer`, принимает любые входные данные) или `Binary` (только зарегистрированные POCO с подсказками типов) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | Формат возврата данных `Map(K, V)`: `Dictionary` (возвращает `Dictionary<K, V>`; для повторяющегося ключа сохраняется только последнее значение) или `KeyValuePairs` (возвращает `List<KeyValuePair<K, V>>` со всеми парами). См. [Тип Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Определяет, как читать строку `JSON`, в которой оба перекрывающихся пути содержат значение. При `false` выбрасывается исключение, поскольку сохранить одно значение можно лишь ценой потери другого; при `true` сохраняется значение, идущее в строке последним. См. [Перекрывающиеся пути](#type-map-reading-json) |

<h3 id="session-management">
  Управление сеансами
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | Включает сеансы с сохранением состояния; запросы выполняются последовательно |
| SessionId | `string` | `null` | `SessionId` | ID сеанса; если значение равно null, а UseSession равно true, GUID генерируется автоматически |

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

  Класс `ClickHouseConnection` обычно поддерживает параллельную работу (несколько потоков могут выполнять запросы одновременно). Однако при включённом флаге `UseSession` в каждый момент времени на одном соединении может выполняться только один активный запрос (это ограничение на стороне сервера).
</Note>

<h3 id="security">
  Безопасность
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| SkipServerCertificateValidation | `bool` | `false` | — | Пропускает проверку HTTPS-сертификата; **не использовать в продакшне** |

<h3 id="http-client-configuration">
  Конфигурация HTTP-клиента
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | Пользовательский предварительно настроенный инстанс HttpClient |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | Пользовательская фабрика для создания инстансов HttpClient |
| HttpClientName | `string` | `null` | — | Имя, по которому HttpClientFactory создаёт нужный клиент |

<h3 id="logging-debugging">
  Логирование и отладка
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Фабрика логгеров для записи диагностических сообщений |
| EnableDebugMode | `bool` | `false` | — | Включает сетевую трассировку .NET (требуется LoggerFactory с уровнем логирования Trace); **существенно влияет на производительность** |

<h3 id="custom-settings-roles">
  Пользовательские настройки и роли
</h3>

| Свойство | Тип | Значение по умолчанию | Ключ в строке подключения | Описание |
| - | - | - | - | - |
| CustomSettings | `IDictionary<string, object>` | Пусто | префикс `set_*` | Настройки сервера ClickHouse, см. примечание ниже |
| Roles | `IReadOnlyList<string>` | Пусто | `Roles` | Роли ClickHouse через запятую (например, `Roles=admin,reader`) |
| ApplicationInfo | `IReadOnlyDictionary<string, string>` | Пусто | — | Произвольные теги, дописываемые к HTTP-заголовку `User-Agent`, чтобы определять, какое приложение выполнило запрос. |

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

  Полный список доступных настроек см. [здесь](/ru/reference/settings/session-settings).
</Note>

***

<h3 id="connection-string-examples">
  Примеры строк подключения
</h3>

<h4 id="basic-connection">
  Базовое соединение
</h4>

```text theme={null}
Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb
```

<h4 id="with-custom-clickhouse-settings">
  С нестандартными настройками ClickHouse
</h4>

```text theme={null}
Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000
```

***

<h3 id="query-options">
  QueryOptions
</h3>

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

| Свойство | Тип | Описание |
| - | - | - |
| QueryId | `string` | Пользовательский идентификатор запроса для отслеживания в `system.query_log` или отмены запроса |
| Database | `string` | Переопределяет базу данных по умолчанию для этого запроса |
| Roles | `IReadOnlyList<string>` | Переопределяет роли клиента для этого запроса |
| CustomSettings | `IDictionary<string, object>` | Настройки сервера ClickHouse для этого запроса (например, `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | Дополнительные HTTP-заголовки для этого запроса |
| UseSession | `bool?` | Переопределяет использование сеанса для этого запроса |
| SessionId | `string` | ID сеанса для этого запроса (требуется `UseSession = true`) |
| BearerToken | `string` | Переопределяет токен аутентификации для этого запроса |
| ParameterTypeResolver | `IParameterTypeResolver` | Переопределяет заданный на уровне клиента резолвер сопоставления типов параметров в стиле `@`; см. [Пользовательское сопоставление типов параметров](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | Переопределяет заданный на уровне клиента форматтер сериализации значений параметров в стиле `@`; см. [Пользовательское форматирование значений параметров](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Переопределяет заданное на уровне клиента преобразование значений, возвращаемых считывателем данных; см. [Пользовательское преобразование значений при чтении](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | Тайм-аут запроса на стороне сервера (передаётся как настройка `max_execution_time`); при его превышении сервер отменяет запрос |
| AcceptEncoding | `string` | Переопределение `Accept-Encoding` для отдельного запроса (например, `"br"`, `"identity"`), имеющее старшинство над `ClickHouseClientSettings.AcceptEncoding`; также принудительно добавляет `enable_http_compression=1` в URL. См. [Сжатие при передаче для отдельного запроса](#per-query-accept-encoding). |

**Пример:**

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

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

***

<h3 id="insert-options">
  InsertOptions
</h3>

`InsertOptions` расширяет `QueryOptions` настройками массовой вставки через `InsertBinaryAsync`.

| Свойство | Тип | По умолчанию | Описание |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Число строк в батче |
| MaxDegreeOfParallelism | `int` | 1 | Число батчей, загружаемых параллельно |
| Format | `RowBinaryFormat` | `RowBinary` | Бинарный формат: `RowBinary` или `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | Кодек для тела запроса на вставку (`Content-Encoding`). При значении `null` данные передаются без сжатия. См. [Сжатие при вставке](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Где передаётся оператор `INSERT INTO ... FORMAT ...`: в `Body` (перед строками) или в `Url` (в параметре URL `query`). См. [Размещение запроса вставки](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | Имя столбца → строка типа ClickHouse. Если значение задано, предварительный запрос для определения схемы не выполняется. |
| UseSchemaCache | `bool` | `false` | Кэширует полную схему таблицы для каждой пары (база данных, таблица) на всё время существования клиента. |

Все свойства `QueryOptions` также доступны в `InsertOptions`.

**Пример:**

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

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

<h4 id="skip-schema-query">
  Отключение предварительного запроса для определения схемы
</h4>

По умолчанию `InsertBinaryAsync` перед каждой вставкой отправляет запрос `SELECT ... WHERE 1=0`, чтобы определить типы столбцов. При высоких требованиях к пропускной способности избежать связанных с этим дополнительных затрат можно двумя способами:

**Вариант 1: Явно указать типы столбцов**

Если схема таблицы известна на этапе компиляции, передайте типы столбцов через `ColumnTypes`. Тогда запрос для определения схемы вообще не отправляется:

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

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

**Вариант 2: кэширование схемы**

Если вы многократно выполняете вставку в одну и ту же таблицу, установите `UseSchemaCache = true`. Тогда схема будет запрошена только один раз и повторно использована при последующих вставках через тот же инстанс `ClickHouseClient`:

```csharp theme={null}
var options = new InsertOptions { UseSchemaCache = true };

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

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

<Note>
  * `ColumnTypes` имеет приоритет над `UseSchemaCache`. Если заданы оба параметра, используются явно указанные типы.
  * Кэш схемы не отслеживает изменения, внесённые командой `ALTER TABLE`. Если вы изменили схему таблицы, создайте новый `ClickHouseClient` или не используйте `UseSchemaCache` для этой таблицы.
  * Кэш привязан к инстансу `ClickHouseClient` и использует в качестве ключа пару (база данных, таблица). Разные наборы столбцов одной таблицы используют одну и ту же схему из кэша.
</Note>

<h2 id="clickhouse-client">
  ClickHouseClient
</h2>

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

<h3 id="creating-a-client">
  Создание клиента
</h3>

Создайте `ClickHouseClient` с помощью строки подключения или объекта `ClickHouseClientSettings`. Доступные параметры описаны в разделе [Конфигурация](#configuration).

Сведения о вашем сервисе ClickHouse Cloud можно найти в консоли ClickHouse Cloud.

Выберите сервис и нажмите **Подключение**:

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=334245222c7a16f54a807110b260b71e" size="md" alt="Кнопка подключения к сервису ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

Выберите **C#**. Ниже появятся сведения о подключении.

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=60f04e40db829dd87b2407094cf1765d" size="md" alt="Сведения о подключении к ClickHouse Cloud для C#" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

Если вы используете самоуправляемый ClickHouse, сведения о подключении предоставляет администратор ClickHouse.

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

```csharp theme={null}
using ClickHouse.Driver;

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

Или с помощью `ClickHouseClientSettings`:

```csharp theme={null}
using ClickHouse.Driver;

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

При использовании внедрения зависимостей применяйте `IHttpClientFactory`:

```csharp theme={null}
// In your DI configuration. No AutomaticDecompression needed — the driver decodes
// compressed responses itself, and a mask here would widen its Accept-Encoding.
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
});

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

<Note>
  `ClickHouseClient` рассчитан на длительную работу и использование во всём приложении. Создайте один экземпляр (обычно как singleton) и используйте его для всех операций с базой данных. Клиент самостоятельно управляет пулом HTTP-соединений.
</Note>

***

<h3 id="executing-queries">
  Выполнение запросов
</h3>

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

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

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

Для получения одного значения используйте `ExecuteScalarAsync`:

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

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

***

<h3 id="inserting-data">
  Вставка данных
</h3>

<h4 id="parameterized-inserts">
  Параметризованные вставки
</h4>

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

```csharp theme={null}
using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

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

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

***

<h4 id="bulk-insert">
  Массовая вставка
</h4>

Используйте `InsertBinaryAsync`, чтобы эффективно вставлять большое количество строк. Метод передаёт данные потоком в собственном двоичном формате строк ClickHouse, поддерживает параллельную загрузку батчей и позволяет избежать ошибок "URL слишком длинный", которые могут возникать при использовании параметризованных запросов.

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

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

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

Для больших объёмов данных настройте пакетную вставку и степень параллелизма с помощью `InsertOptions`:

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

<Note>
  * Перед вставкой клиент автоматически получает структуру таблицы с помощью запроса `SELECT * FROM <table> WHERE 1=0`. Передаваемые значения должны соответствовать типам столбцов целевой таблицы. Чтобы не выполнять этот запрос, используйте [`InsertOptions.ColumnTypes` или `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * При `MaxDegreeOfParallelism > 1` батчи загружаются параллельно. Сеансы несовместимы с параллельной вставкой: отключите сеансы или установите `MaxDegreeOfParallelism = 1`.
  * Чтобы сервер подставлял значения DEFAULT для столбцов, значения которых не переданы, укажите `RowBinaryFormat.RowBinaryWithDefaults` в `InsertOptions.Format`.
</Note>

<h4 id="poco-insert">
  Вставка объектов POCO
</h4>

Вместо формирования массивов `object[]` можно вставлять непосредственно строго типизированные объекты POCO. Достаточно один раз зарегистрировать тип, а затем передать `IEnumerable<T>`:

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

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

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

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

По умолчанию все публичные свойства, доступные для чтения, сопоставляются со столбцами по строгому совпадению имён с учётом регистра. Это сопоставление можно настроить с помощью атрибутов:

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

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

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Exclude from insert
    public string InternalTag { get; set; }
}
```

| Атрибут | Назначение |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | Переопределить имя целевого столбца |
| `[ClickHouseColumn(Type = "...")]` | Явно указать тип ClickHouse |
| `[ClickHouseNotMapped]` | Исключить свойство из вставки |

Если для **всех** сопоставленных свойств явно указан `Type`, предварительный запрос для определения схемы не выполняется. Если типы явно указаны только для части свойств, драйвер всё равно выполняет такой запрос для всего набора столбцов.

`InsertBinaryAsync<T>` поддерживает те же параметры `InsertOptions` (группировку в батчи, параллелизм, кэширование схемы), что и перегрузка для `object[]`.

<Note>
  В отличие от перегрузки для `object[]`, `InsertBinaryAsync<T>` не принимает явный список столбцов. Набор столбцов определяется сопоставленными свойствами зарегистрированного типа. Чтобы управлять этим набором, используйте `[ClickHouseNotMapped]` для исключения свойств или `[ClickHouseColumn(Name = "...")]` для изменения имён столбцов.

  Если в `InsertOptions` задан `ColumnTypes`, указанные в нём типы имеют приоритет над атрибутами POCO.
</Note>

<h4 id="poco-insert-schema-evolution">
  Изменение схемы
</h4>

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

<h4 id="insert-query-placement">
  Размещение запроса на вставку
</h4>

При бинарной вставке оператор `INSERT INTO ... FORMAT ...` записывается в первой строке тела запроса, перед строками данных. По умолчанию тело запроса сжимается, поэтому средства маршрутизации и логирования, анализирующие только URL, не видят оператор. Задайте для `InsertOptions.QueryPlacement` значение `InsertQueryPlacement.Url`, чтобы передавать оператор в параметре URL `query`, а в теле запроса оставлять только строки данных:

```csharp theme={null}
var options = new InsertOptions { QueryPlacement = InsertQueryPlacement.Url };
await client.InsertBinaryAsync("events", columns, rows, options);
```

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

Эта настройка не зависит от `Compressor`: в обоих режимах тело запроса кодируется одинаково.

***

<h3 id="reading-data">
  Чтение данных
</h3>

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

Вызывайте `Read()` для перехода к следующей строке. Когда строк больше нет, метод возвращает `false`. Обращайтесь к столбцам по индексу (с нуля) или по имени.

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

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

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

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

<h4 id="poco-read">
  Чтение данных в POCO
</h4>

Вместо того чтобы читать столбцы по индексу или имени, можно передавать результаты запроса напрямую в собственные классы в потоковом режиме. Достаточно один раз зарегистрировать тип в клиенте, а затем использовать `QueryAsync<T>`:

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

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

}

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

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

<h5 id="poco-read-registration">
  Регистрация
</h5>

`RegisterPocoType<T>()` настраивает сопоставление типов для вставки и чтения и сразу проверяет оба варианта. `RegisterBinaryInsertType<T>()` работает как прежде: для обратной совместимости он настраивает сопоставление только для вставки.

Зарегистрированный тип должен иметь:

* Публичный конструктор без параметров.
* Хотя бы одно публичное свойство с публичным сеттером, не помеченным `init`. Свойства с модификатором `required` поддерживаются.

<h5 id="poco-read-column-matching">
  Сопоставление столбцов
</h5>

При сопоставлении столбцов учитывается регистр. Если в результате нет нужного столбца, свойство сохраняет значение по умолчанию; лишние столбцы результата игнорируются.

Драйвер не выполняет расширяющее или сужающее преобразование значений. За исключением альтернативных представлений, перечисленных ниже,
тип столбца в используемом фреймворке должен быть совместим с типом свойства; в противном случае генерируется
`InvalidOperationException`. Поэтому свойство типа `object` принимает значение из любого столбца.

<h5 id="poco-read-types">
  Поддерживаемые типы свойств
</h5>

`QueryAsync<T>` считывает данные из каждого из этих столбцов напрямую в свойство соответствующего типа:

| Столбец ClickHouse | Тип(ы) свойства |
| - | - |
| `Int8`/`Int16`/`Int32`/`Int64` | `sbyte`/`short`/`int`/`long` |
| `UInt8`/`UInt16`/`UInt32`/`UInt64` | `byte`/`ushort`/`uint`/`ulong` |
| `Int128`/`UInt128` | `BigInteger` или встроенные `System.Int128`/`System.UInt128` в .NET 8 и более поздних версиях |
| `Int256`/`UInt256` | `BigInteger` |
| `Float32`/`Float64`/`BFloat16` | `float`/`double`/`float` |
| `Bool` | `bool` |
| `Decimal` | `decimal` или `ClickHouseDecimal` |
| `Date`/`Date32`/`DateTime`/`DateTime64` | `DateTime`, `DateTimeOffset` или `DateOnly` |
| `Time`/`Time64` | `TimeSpan` |
| `UUID` | `Guid` |
| `IPv4`/`IPv6` | `IPAddress` |
| `Enum8`/`Enum16` | `string` (имя элемента) или `int` (порядковый номер в передаваемых данных) |
| `String`/`FixedString` | `string` или `byte[]` |

Для всех перечисленных типов свойств допускаются также nullable-варианты (`long?`, `DateOnly?` и т. д.),
даже если столбец не имеет типа `Nullable(...)`. Свойство значимого типа, не допускающее NULL, можно зарегистрировать для столбца `Nullable(T)`,
но при получении NULL возникнет исключение.

Типы-обёртки, такие как `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` и `Object(T)`, сопоставляются с типами свойств так же, как `T`.

Составные типы столбцов также поддерживаются. Для них используются типы платформы, указанные в
[справочнике типов для чтения](#clickhouse-native-type-map-reading): `Array(T)` преобразуется в `T[]`, `Tuple(...)`
— в `System.Tuple<...>`, `Nested(...)` — в `Tuple<...>[]`, `JSON` — в `JsonObject` (или `string`
при [`JsonReadMode=String`](#type-map-reading-json)), а `Variant`/`Dynamic` — в `object`.

Столбец `Map(K, V)` — особый случай: данные считываются в свойство типа `List<KeyValuePair<K, V>>` или `KeyValuePair<K, V>[]`
без упаковки значений, с сохранением порядка элементов в передаваемых данных и повторяющихся ключей
при любом значении [`MapReadMode`](#type-map-reading-map). Свойство типа `Dictionary<K, V>` работает только в режиме по умолчанию.
Типы ключа и значения должны совпадать в точности, поэтому для
`Map(String, Nullable(Int32))` требуется `KeyValuePair<string, int?>`.

Если для столбца доступно несколько типов свойств (например, для `DateTime` — `DateTime`,
`DateTimeOffset` или `DateOnly`, а для `String` — `string` или `byte[]`), представление определяется объявленным типом свойства. Эти альтернативные представления
доступны при чтении в POCO: `QueryAsync<T>` их поддерживает, а `MapTo<T>` — нет.

<h5 id="poco-read-mapto">
  Материализация одной строки
</h5>

При ручном обходе строк считывателя используйте `ClickHouseDataReader.MapTo<T>()`, чтобы преобразовать текущую строку в объект зарегистрированного класса POCO, не переходя к следующей строке:

```csharp theme={null}
var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

Используйте `MapTo<T>`, если вам нужно самостоятельно управлять циклом считывания — например, чтобы сочетать доступ к необработанным данным столбцов
с материализацией POCO. Этот метод считывает строку через упакованные значения считывателя, поэтому не поддерживает
перечисленные выше альтернативные типы свойств и выделяет больше памяти, чем `QueryAsync<T>`. Если нужны только строки, используйте
`QueryAsync<T>`; численные показатели приведены в разделе
[выбор способа материализации](#perf-read-path).

<h5 id="poco-read-converters">
  Преобразователи значений при чтении
</h5>

[Преобразователь значений при чтении](#read-value-conversion), настроенный для всего клиента или отдельного запроса, действует в обоих случаях и
не отключает чтение без упаковки значений. Драйвер преобразует каждый столбец с помощью перегрузки, соответствующей способу
его чтения: типизированной `ConvertValue<T>` для столбца, прочитанного без упаковки
значений, и `ConvertValue` с упакованными значениями для составного столбца. Реализуйте обе перегрузки
согласованно, иначе результат чтения одного и того же столбца будет зависеть от выбранного способа.

<h5 id="poco-read-diagnostics">
  Диагностика регистрации
</h5>

Если настроен `LoggerFactory`, методы `RegisterPocoType<T>()` и `RegisterBinaryInsertType<T>()` записывают в лог сообщение уровня `Debug` (категория `ClickHouse.Driver.Client`) о том, какие свойства сопоставлены со столбцами, а какие пропущены и почему. Подробнее см. в разделе [Логирование и диагностика](#logging-and-diagnostics).

***

<h3 id="sql-parameters">
  Параметры SQL-запросов
</h3>

В ClickHouse для параметров SQL-запросов используется стандартный формат `{parameter_name:DataType}`.

**Примеры:**

```sql theme={null}
SELECT {value:Array(UInt16)} as a
```

```sql theme={null}
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
```

```sql theme={null}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})
```

<Note>
  Параметры SQL-запроса передаются в параметрах запроса HTTP URI, поэтому, если их слишком много, может возникнуть ошибка "слишком длинный URL". Чтобы избежать этого ограничения при массовой вставке данных, используйте `InsertBinaryAsync`.
</Note>

<h4 id="at-style-placeholders">
  Плейсхолдеры `@name` в стиле ADO
</h4>

Драйвер также поддерживает плейсхолдеры `@name`, которые используют ORM, например Dapper. Это
удобный клиентский синтаксис: перед отправкой запроса каждый такой плейсхолдер заменяется на
`{name:ResolvedType}`, поэтому сервер не получает символ `@`. Подробнее о выборе типа см. в разделе
[«Определение типа»](#parameter-type-mapping). По возможности используйте явную
форму `{name:Type}`.

Если для `@name` нет соответствующего параметра, плейсхолдер остаётся без изменений, и сервер отклоняет запрос. При сопоставлении
учитывается регистр, поэтому `@ID` не связывается с параметром `id`.

<Note>
  Чтобы отключить замену, задайте переключатель AppContext `ClickHouse.Driver.DisableReplacingParameters`
  до первого использования драйвера. Отключается только замена в тексте запроса: параметры по-прежнему отправляются, поэтому
  запросы с нативным синтаксисом `{name:Type}` продолжают работать.
</Note>

<h4 id="identifier-parameters">
  Параметры типа Identifier
</h4>

Тип параметра `Identifier` позволяет безопасно подставлять имена баз данных, таблиц и столбцов вместо строковых литералов в кавычках. Чтобы использовать его, укажите в SQL `{name:Identifier}` или задайте `ClickHouseDbParameter.ClickHouseType = "Identifier"`:

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
```

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

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

Значение передаётся без изменений, а сервер подставляет его как SQL-идентификатор, самостоятельно заключая в обратные кавычки и экранируя специальные символы. Идентификаторы со специальными символами (в том числе с обратными кавычками) сохраняются без искажений при записи и последующем чтении.

***

<h3 id="query-id">
  ID запроса
</h3>

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

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

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

<Tip>
  Если вы задаёте собственный `QueryId`, убедитесь, что для каждого вызова он отличается. Хороший вариант — случайный GUID.
</Tip>

***

<h3 id="parameter-type-mapping">
  Пользовательское сопоставление типов параметров
</h3>

При использовании параметров вида `@` (например, `WHERE id = @id`) драйвер автоматически определяет тип ClickHouse на основе типа значения .NET. Например, типу `int` соответствует `Int32`.

<Warning>
  **Определение типа параметров DateTime**

  Если для параметра вида `@` в SQL не указан тип через `{name:Type}` и не задан `ClickHouseType`, для значений, представляющих конкретный момент времени, определяется тип `DateTime('UTC')`, а не просто `DateTime`. Значения `DateTime` с `Kind`, равным `Utc` или `Local`, и все значения `DateTimeOffset` передаются как `DateTime('UTC')`. Это позволяет сохранить момент времени независимо от часового пояса сервера.

  Явное указание типа (`{name:DateTime}`) имеет приоритет перед автоматическим определением. При составлении запросов рекомендуется указывать тип явно.
</Warning>

Чтобы изменить поведение по умолчанию, задайте `ParameterTypeResolver` в `ClickHouseClientSettings`. Например, так можно использовать `DateTime64(3)` для всех параметров `DateTime`, обеспечив точность до миллисекунд, или задать одинаковое число знаков после запятой для всех десятичных значений — без указания `ClickHouseType` для каждого параметра.

**Использование `DictionaryParameterTypeResolver` для простого сопоставления типов:**

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

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

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

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

**Собственная реализация `IParameterTypeResolver` для сложных сценариев:**

Если тип параметра нужно определять по его значению или имени, реализуйте интерфейс `IParameterTypeResolver` напрямую. Верните `null`, чтобы перейти к определению типа по умолчанию:

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

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

Вы также можете задать резолвер для отдельного запроса с помощью `QueryOptions.ParameterTypeResolver`. В этом случае он имеет приоритет над резолвером на уровне клиента.

**Порядок выбора типа:**

Резолвер — лишь одно из звеньев в порядке выбора типа. От наивысшего приоритета к низшему:

1. `ClickHouseType`, явно заданный для параметра
2. Подсказка типа SQL, указанная в запросе с помощью синтаксиса `{name:Type}`
3. `IParameterTypeResolver` (из `QueryOptions.ParameterTypeResolver`, а если он не задан — из `ClickHouseClientSettings.ParameterTypeResolver`)
4. Встроенный вывод типов (`TypeConverter.ToClickHouseType`)

Резолвер также работает при использовании `ClickHouseConnection` через ADO.NET: соединения, созданные клиентом, наследуют его настройки.

***

<h3 id="parameter-value-formatting">
  Пользовательское форматирование значений параметров
</h3>

`IParameterFormatter` — механизм расширения, который определяет, как сериализуются значения параметров. Используйте его, если встроенное форматирование (например, точность DateTime, представление десятичных чисел с учётом региональных настроек, экранирование строк или представление чисел) не соответствует требованиям вашей схемы или систем, в которые передаются данные.

Задайте `ParameterFormatter` в `ClickHouseClientSettings`, чтобы применять форматтер ко всем параметризованным запросам. Форматтер получает значение, определённое имя типа ClickHouse и имя параметра и возвращает строковое представление для отправки на сервер. Верните `null`, чтобы передать обработку форматтеру по умолчанию.

**Использование `DictionaryParameterFormatter` для простого форматирования по типам CLR:**

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

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

**Пользовательская реализация `IParameterFormatter` для сложных сценариев:**

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

Вы также можете задать форматтер для отдельного запроса через `QueryOptions.ParameterFormatter`. Такой форматтер имеет приоритет над форматтером на уровне клиента.

**Составные значения:**

Форматтер применяется и к параметрам-коллекциям верхнего уровня, и к каждому элементу составных значений (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Например, при сопоставлении с `typeof(int)` каждый элемент типа `Int32` в `Array(Int32)` форматируется отдельно.

**Заключение в одинарные кавычки в составных значениях:**

Для строковых типов ClickHouse (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) внутри составного литерала драйвер заключает результат работы форматтера в одинарные кавычки, но не экранирует его содержимое. Если возвращённая строка содержит неэкранированную одинарную кавычку или обратную косую черту, составной литерал окажется некорректным, и сервер отклонит запрос.

Строковые параметры верхнего уровня (не входящие в составное значение) используются как есть, без заключения в кавычки, поэтому экранировать их не требуется.

**Приоритет форматтеров:**

1. `IParameterFormatter` (из `QueryOptions.ParameterFormatter`, а если он не задан — из `ClickHouseClientSettings.ParameterFormatter`). Если он возвращает значение, отличное от null, используется это значение.
2. Встроенное форматирование по типу в `HttpParameterFormatter`.

Форматтер не вызывается для значений `null` и `DBNull`: они всегда сериализуются как маркер null в ClickHouse (`\N`).

***

<h3 id="read-value-conversion">
  Пользовательское преобразование значений при чтении
</h3>

`IReadValueConverter` позволяет преобразовывать значения, возвращаемые считывателем данных после десериализации, не меняя их тип CLR. Например, с его помощью можно задать `DateTime.Kind = Utc` для столбца `DateTime` без часового пояса, удалить лишние символы из строк или нормализовать их, а также дополнительно обработать JSON-столбец перед передачей в прикладной код.

Чтобы использовать преобразователь при каждом чтении, задайте `ReadValueConverter` в `ClickHouseClientSettings`. Преобразователь вызывается для каждого значения столбца в каждой строке — как при получении значения в упакованном виде (`GetValue`), так и через обобщённый метод (`GetFieldValue<T>`). Если преобразователь не задан, дополнительных накладных расходов нет: считыватель возвращает значения напрямую.

**Использование `DictionaryReadValueConverter` для простого преобразования значений в зависимости от типа CLR:**

```csharp theme={null}
using ClickHouse.Driver.ADO.Readers;

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

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

Значения, CLR-тип которых во время выполнения не зарегистрирован через `For<T>`, передаются без изменений. Преобразователь выбирается по точному совпадению CLR-типа, поэтому зарегистрируйте тип, который фактически возвращает считыватель (например, `For<JsonObject>` для JSON-столбца в режиме `JsonReadMode.Binary`).

**Пользовательская реализация `IReadValueConverter` для сложных сценариев:**

Если преобразователь нужно выбирать по строке типа ClickHouse (например, чтобы различать `DateTime` и `DateTime('UTC')`, которые представлены одним и тем же CLR-типом), реализуйте интерфейс `IReadValueConverter` напрямую:

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

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

Преобразователь должен сохранять тип CLR, определяемый во время выполнения; метаданные столбца (`GetFieldType`, `GetSchemaTable`) через него не обрабатываются и должны соответствовать возвращаемому значению.

Преобразователь можно также задать для отдельного запроса через `QueryOptions.ReadValueConverter`. В этом случае он имеет приоритет над преобразователем на уровне клиента.

**Граница применения:**

Преобразователь вызывается один раз для каждого столбца и получает десериализованное значение ячейки целиком; составные контейнеры он **не** обрабатывает рекурсивно. Для столбца `Array(Int32)` ему передаётся `int[]`, а для `Tuple(Int32, String)` — `ITuple`.

**Какая перегрузка вызывается:**

Обе перегрузки должны работать согласованно: какую из них вызовет драйвер, зависит от того, как вызывающий код считывает
столбец:

* `ConvertValue<T>` — типизированные аксессоры `GetByte`, `GetSByte`, `GetInt16`/`32`/`64`,
  `GetUInt16`/`32`/`64`, `GetFloat`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetIPAddress`,
  `GetBigInteger` и `GetFieldValue<T>`, а также все столбцы, считываемые без упаковки значений через
  [механизм чтения POCO](#poco-read-converters).
* `ConvertValue` (с упаковкой значения) — `GetValue`, `GetValues`, индексаторы, `GetChar`, `GetTuple` и
  пути чтения с приведением типов в `GetBoolean`, `GetDecimal` и `GetString`.

`IsDBNull` вообще не вызывает преобразователь: он считывает признак NULL напрямую, поэтому преобразователь не может
повлиять на то, считается ли значение NULL. `TryGetEnumOrdinal` тоже не использует преобразователь — см.
[чтение порядкового номера значения enum](#ado-net-reader-enum-ordinal).

Преобразователь работает и при использовании ADO.NET через `ClickHouseConnection`: соединения, созданные клиентом, наследуют его настройки.

***

<h3 id="raw-streaming">
  Потоковая передача необработанных данных
</h3>

Используйте `ExecuteRawResultAsync` для прямой потоковой передачи результатов запроса в заданном формате, минуя считыватель данных. Это удобно для экспорта данных в файлы или их передачи в другие системы:

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

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

Распространённые форматы: `JSONEachRow`, `CSV`, `TSV`, `Parquet`, `Native`. Полный список доступных форматов см. в [документации по форматам](/ru/reference/formats/index).

***

<h3 id="per-query-accept-encoding">
  Сжатие при передаче данных для отдельного запроса
</h3>

По умолчанию при `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`).

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

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

<h4 id="per-query-accept-encoding-httpclient">
  Настройка HttpClient
</h4>

Дополнительная настройка не требуется: у `HttpClient`, который создаёт драйвер, `AutomaticDecompression` остаётся равным `DecompressionMethods.None`, а ответы драйвер декодирует сам. Поэтому `Content-Encoding` не удаляется без вашего ведома, а исходное тело ответа поступает к вам именно в том виде, в каком его отправил сервер.

<Warning>
  Если вы используете собственный `HttpClient`, также оставьте `AutomaticDecompression` отключённым. Этот параметр влияет не только на обработку ответов: при отправке обработчик **добавляет в исходящий `Accept-Encoding` все алгоритмы из своей маски, которых там ещё нет**. Например, обработчик с маской `GZip | Deflate` превращает явно заданное `AcceptEncoding = "lz4"` в `lz4, gzip, deflate`, а явно заданное `"identity"` — в `identity, gzip, deflate` при передаче. ClickHouse выбирает кодек по собственному фиксированному порядку предпочтения, игнорируя порядок перечисления и значения q, поэтому может ответить с использованием кодека, который вы не запрашивали. Затем обработчик декодирует ответ и удалит заголовок — и вы даже не узнаете, что это произошло. Если не задавать алгоритмы в маске, список предлагаемых кодеков останется именно таким, каким вы его указали.
</Warning>

<Warning>
  Если `AcceptEncoding` запрашивает кодек, который драйвер не умеет декодировать (`snappy`), безопасно использовать только `ExecuteRawResultAsync`. Вызовы `ExecuteReaderAsync`, `ExecuteScalarAsync` и `ExecuteNonQueryAsync` завершаются исключением `NotSupportedException` с указанием кодека (раньше они пытались разобрать сжатые байты как данные в формате результата и выдавали некорректные данные).
</Warning>

<h4 id="per-query-accept-encoding-errors">
  Тела ответов с ошибками
</h4>

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

***

<h3 id="response-decompression">
  Распаковка ответа
</h3>

Заголовок `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` самостоятельно — для всего клиента:

```csharp theme={null}
using var client = new ClickHouseClient(new ClickHouseClientSettings("Host=localhost")
{
    AcceptEncoding = "br",      // decodable, but not advertised by default
});
```

для отдельного запроса — эта настройка имеет приоритет:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "identity" });   // opt this query out
```

или в строке подключения, если вы используете ORM и не обращаетесь напрямую к `ClickHouseClientSettings`:

```text theme={null}
Host=localhost;AcceptEncoding=br, gzip
```

При указании кодека в URL также принудительно добавляется `enable_http_compression=1`: без этого ClickHouse не учитывает заголовок. Это происходит даже при `UseCompression=false`, поскольку явное указание кодека считается запросом на его использование. Если значение не задано, при `UseCompression=false` заголовок `Accept-Encoding` не отправляется.

`Accept-Encoding` можно задать в четырёх местах. Приоритет имеет первое из них, в котором указан кодек:

1. `QueryOptions.AcceptEncoding` (или `ClickHouseCommand.AcceptEncoding`)
2. `CustomHeaders["Accept-Encoding"]` для запроса
3. `CustomHeaders["Accept-Encoding"]` для клиента
4. `ClickHouseClientSettings.AcceptEncoding` или ключевое слово `AcceptEncoding` в строке соединения

Если кодек не указан ни в одном из них, драйвер отправляет свой список по умолчанию. Значение, не содержащее названия кодека (null, пустая строка,
пробельные символы или только запятые), считается незаданным, и проверяется следующее место. Чтобы отключить сжатие, используйте `identity`.

**Кодек выбирает сервер, а не клиент.** ClickHouse проверяет токены в `Accept-Encoding` в собственном фиксированном порядке предпочтения — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — и игнорирует как порядок их перечисления, так и значения q. Таким образом, заголовок сообщает о поддерживаемых кодеках, а не требует использовать конкретный; повлиять на выбор можно только исключив некоторые токены. Список по умолчанию включает `zstd`, поэтому ответ на запрос с настройками по умолчанию сжимается с помощью zstd; остальные токены служат запасными вариантами. Клиент умеет декодировать `br`, но по умолчанию не объявляет о его поддержке.

Соотношение кодеков по размеру полезной нагрузки, нагрузке на CPU сервера и CPU клиента зависит от ваших данных, скорости соединения и серверного параметра `http_zlib_compression_level` (значение по умолчанию: 3) — см. [Настройка сжатия](#tuning-compression).

* **`http_zlib_compression_level`.** Этот параметр применяется ко всем кодекам HTTP; его значение по умолчанию — 3. Подбирайте значение с учётом данных, скорости соединения и нагрузки на CPU.
* **Клиент, производительность которого ограничена CPU, при быстром соединении.** Драйвер декодирует тело ответа в вызывающем потоке, поэтому, если сеть не является узким местом, скорость декодирования на стороне клиента может ограничивать производительность.

Если применимо что-либо из перечисленного, запрашивайте другой кодек для отдельного запроса или для клиента в целом:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "lz4" });   // decode this one with lz4 instead
```

Поскольку решение принимается по ответу, тело декодируется в соответствии с его заголовком `Content-Encoding` независимо от того, что запрашивалось: если заголовок отсутствует или имеет значение `identity`, тело остаётся без изменений; если указан поддерживаемый кодек — декодируется; любое другое значение вызывает ошибку, в которой оно указано. Повторное декодирование исключено: если `AutomaticDecompression` в обработчике, переданном вызывающим кодом, уже декодировал тело, обработчик также удаляет заголовок `Content-Encoding`. Поэтому драйвер получает данные в исходном виде и не декодирует их снова.

**Для необработанных результатов кодек не запрашивается.** `ExecuteRawResultAsync` (как и общедоступные `PostStreamAsync` / `InsertRawStreamAsync`) передают вам тело ответа без изменений. Поэтому, если вы сами не укажете кодек, они не запросят сжатие: драйвер не декодирует такое тело, и запрошенный кодек незаметно превратил бы экспортируемые данные в сжатый файл. Правило простое и не зависит от настройки `HttpClient`: **тело ответа поступает в точности таким, каким его отправил сервер, а сервер отправляет данные без сжатия, если вы не запросили кодек.** Чтобы экспортировать сжатые данные, укажите кодек для всего клиента или отдельного запроса.

Явно заданный `AcceptEncoding` (на любом из этих уровней) действует и для запросов с необработанным результатом. Если результат нужно декодировать, используйте `ClickHouseRawResult.ReadDecompressedStreamAsync()`. Методы `ReadAsStreamAsync`, `ReadAsByteArrayAsync`, `ReadAsStringAsync` и `CopyToAsync` всегда возвращают байты в том виде, в каком они поступили.

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT JSONEachRow",
    options: new QueryOptions { AcceptEncoding = "lz4" });

Console.WriteLine(result.ContentEncoding); // "lz4"

await using var body = await result.ReadDecompressedStreamAsync();
using var bodyReader = new StreamReader(body);
var json = await bodyReader.ReadToEndAsync();
```

Как и в примере выше, полностью прочитайте возвращённый поток, прежде чем он выйдет из области видимости. Если ответ *сжат*, возвращается декодер, созданный с `leaveOpen`: его освобождение не затрагивает ответ. Если ответ **не** сжат, возвращается непосредственно поток HTTP-содержимого, и его освобождение закрывает тело ответа. В обоих случаях ответом владеет `ClickHouseRawResult` — после освобождения потока не вызывайте другие методы чтения этого объекта. `ClickHouseRawResult` необходимо освобождать в любом случае, и одного этого достаточно: вместе с ним освобождаются ответ и созданный здесь декодер (декодеры используют буферы из пула). Поэтому конструкция `await using` выше необязательна, но её можно оставить. При повторных последовательных вызовах возвращается один и тот же поток; этот тип не предназначен для параллельного использования.

Рабочий пример см. в [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/Select/Select_007_ResponseCompression.cs).

<h4 id="insert-compression">
  Сжатие запросов на вставку
</h4>

По умолчанию для вставки используется кодек Zstd: начальное значение `InsertOptions.Compressor` — `ZstdCompressor.Default`,
то есть zstd с уровнем сжатия 3. Чтобы изменить кодек, укажите другой алгоритм сжатия. Чтобы отправлять
тело запроса без сжатия, укажите `null`.

```csharp theme={null}
var options = new InsertOptions { Compressor = GZipCompressor.Default };  // Content-Encoding: gzip
await client.InsertBinaryAsync("events", columns, rows, options);
```

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

| Компрессор | `Content-Encoding` | Конструктор | `Default` |
| - | - | - | - |
| `ZstdCompressor` | `zstd` | `(int level = 3, int bufferSize = 262144)` | уровень 3 |
| `Lz4Compressor` | `lz4` | `(Lz4Level level = Lz4Level.Fast, int bufferSize = 262144)` | `Lz4Level.Fast` |
| `GZipCompressor` | `gzip` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |
| `BrotliCompressor` | `br` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |

```csharp theme={null}
var options = new InsertOptions { Compressor = new ZstdCompressor(level: 1) };
```

<Note>
  *Используйте общие инстансы компрессоров.* Каждый `Default` — отдельный общий инстанс. Все четыре компрессора
  можно безопасно использовать одновременно в нескольких потоках. Именно так и происходит, когда
  `InsertOptions.MaxDegreeOfParallelism` больше 1: при вставке один и тот же компрессор используется для каждого
  батча. Ни один из компрессоров не реализует `IDisposable`. Создайте собственный инстанс один раз и используйте его повторно —
  так же, как используется `Default`.
</Note>

<h5 id="custom-compressor">
  Пользовательский кодек
</h5>

`IClickHouseCompressor` — публичный интерфейс, реализация которого должна содержать всего два члена:

```csharp theme={null}
public sealed class MyCompressor : IClickHouseCompressor
{
    public string ContentEncoding => "my-codec";

    public Stream Compress(Stream destination, bool leaveOpen) => /* a compressing write stream */;
}
```

Сервер должен поддерживать указанный вами `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`.

***

<h3 id="tuning-compression">
  Настройка сжатия
</h3>

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

<h4 id="the-one-number-that-decides-it">
  Ключевая цифра
</h4>

Сжатие имеет смысл, пока кодек работает быстрее, чем сеть передаёт данные.

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

Таким образом, если результат большой и одновременно обрабатывается только один запрос, сжатие перестаёт окупаться при скорости передачи около 100 МБ/с. Скорость одного HTTPS-потока
в пределах одного облачного региона нередко выше, а при передаче через общедоступный интернет, VPN или между регионами — обычно ниже.

При вставке сжатие остаётся полезным и на более быстрых соединениях: клиент выполняет его на отдельном ядре и обычно сжимает данные быстрее, чем сервер — ответы.

<h4 id="rough-guide-by-deployment">
  Ориентировочные рекомендации в зависимости от размещения
</h4>

| Где работает клиент | Типичная пропускная способность | Чтение | Вставка |
| - | - | - | - |
| Тот же хост / петлевой интерфейс | > 500 MB/s | `identity` | `lz4` — самый быстрый вариант, либо без сжатия |
| Тот же регион, то же облако | \~100–500 MB/s | `identity` или `lz4` | `zstd:1` |
| Разные регионы, то же облако | \~10–100 MB/s | `zstd` | `zstd:3` |
| Интернет / VPN / другое облако | \< 25 MB/s | `zstd` | `zstd:3` |
| Тарифицируемый или сильно ограниченный канал | \< 5 MB/s | `zstd` | `zstd:5`+ или `br` |

Таблица не учитывает три фактора:

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

При чтении распараллеливание возможно только между несколькими запросами.

<h4 id="choosing-a-codec">
  Выбор кодека
</h4>

| Кодек | Степень сжатия | Когда использовать | На что обратить внимание |
| - | - | - | - |
| `lz4` | самая низкая | Быстрые каналы связи, когда ресурсы CPU ограничены сильнее, чем пропускная способность. Декодировать данные с ним значительно дешевле, чем с другими кодеками; на небольших результатах он также быстрее всех. Выбирайте его, если не хотите использовать `zstd` по умолчанию. | У него **нет энтропийного кодирования**, поэтому данные с неравномерным распределением, но без повторов (например, длинные последовательности чисел в текстовом виде) он сжимает значительно хуже остальных. Повышение `http_zlib_compression_level` тоже сильнее всего сказывается именно на нём: переход с уровня 1 на 3 увеличивает затраты CPU примерно в 2,7 раза, сокращая объём данных лишь на \~29%. |
| `zstd` | высокая | Универсальный выбор для передачи данных по сети. Лучшее соотношение степени сжатия и затрат CPU в практически значимом диапазоне. На уровне 3 он превосходит `lz4` по объёму данных, затратам CPU на сервере и общему времени выполнения. | Декодирование **дороже**, чем у `lz4`: по нашим измерениям, в 1,6 раза на уровне 3, хотя на уровне 1 показатели сопоставимы. При этом драйвер декодирует данные в вызывающем потоке. При `http_zlib_compression_level=1` затраты CPU на сервере у него немного *выше*, чем у `lz4`. |
| `gzip` | средняя | Совместимость — его поддерживают практически все прокси и шлюзы. | По нашим измерениям, уступает и `lz4`, и `zstd` по всем показателям: объём данных больше, чем у `zstd`, кодирование требует в несколько раз больше ресурсов CPU, а декодирование — в 5–9 раз больше. Выбирайте его ради совместимости, а не производительности. |
| `br` | самая высокая на низких уровнях | Пропускная способность действительно ограничена, и ради сокращения объёма передаваемых данных вы готовы увеличить нагрузку на CPU. | На высоких уровнях затраты резко возрастают: при `http_zlib_compression_level=6` затраты CPU на сервере, по нашим измерениям, в 3–4 раза выше, чем у `zstd`. По умолчанию он не указывается, поскольку имеет приоритет над всеми резервными вариантами в списке по умолчанию. |

<h4 id="levels">
  Уровни
</h4>

Сжатием ответов управляет одна настройка сервера — `http_zlib_compression_level`. Она действует для *всех* кодеков HTTP, а не только для zlib. По умолчанию её значение — 3.

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

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions
    {
        AcceptEncoding = "zstd",
        CustomSettings = new Dictionary<string, object> { ["http_zlib_compression_level"] = 1 },
    });
```

<h4 id="measuring-your-own-crossover">
  Определение порога эффективности сжатия
</h4>

Самый быстрый способ подобрать кодек и уровень сжатия — измерить время выполнения одного и того же запроса с несколькими кодеками и сравнить результаты.

```csharp theme={null}
foreach (var codec in new[] { "identity", "lz4", "zstd" })
{
    var sw = Stopwatch.StartNew();
    using var reader = await client.ExecuteReaderAsync(
        "SELECT ... FROM big_table",
        options: new QueryOptions { AcceptEncoding = codec });
    while (await reader.ReadAsync()) { }
    Console.WriteLine($"{codec,-9} {sw.ElapsedMilliseconds} ms");
}
```

Чтобы увидеть ту же картину со стороны сервера, считайте `ProfileEvents` из `system.query_log` — задайте
`QueryOptions.QueryId`, чтобы найти нужную строку:

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

Если вы проводите бенчмарк самостоятельно, учтите: простой `LIMIT n` без `ORDER BY` при каждом запуске возвращает *разные строки*, поэтому каждый раз сжимаются разные данные, а сравнение коэффициентов сжатия теряет смысл. Используйте для сравнения
один и тот же результирующий набор.

***

<h3 id="raw-stream-insert">
  Вставка данных из необработанного потока
</h3>

Используйте `InsertRawStreamAsync`, чтобы вставлять данные напрямую из файловых потоков или потоков в памяти. Поддерживаются CSV, JSON, Parquet и другие [форматы ClickHouse](/ru/reference/formats/index).

**Вставка данных из CSV-файла:**

```csharp theme={null}
using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: File.OpenRead("data.csv"),
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);
```

<Warning>
  *Драйвер берёт на себя управление жизненным циклом потока.* `InsertRawStreamAsync` и `PostStreamAsync` освобождают
  переданный им поток после завершения запроса — как успешного, так и неуспешного. Не освобождайте поток
  самостоятельно и не используйте его повторно. Именно поэтому в примере выше
  `FileStream` не заключён в блок `using`.

  Если вы добавите `using`, он вызовет `Dispose` уже после того, как драйвер освободит поток. Для `FileStream` и
  `MemoryStream` повторный вызов безвреден. Но если `Dispose` возвращает буфер в пул
  или уменьшает счётчик ссылок, ресурс будет освобождён дважды.

  Драйвер принимает на себя управление потоком только после проверки аргументов: если вызов генерирует `ArgumentException` или
  `ArgumentNullException` из-за отсутствия таблицы, потока или формата, поток по-прежнему остаётся в вашем распоряжении.
</Warning>

<Note>
  Параметры управления ингестией данных описаны в [документации по настройкам форматов](/ru/reference/settings/formats).
</Note>

***

<h3 id="more-examples">
  Дополнительные примеры
</h3>

Другие примеры практического использования см. в [каталоге примеров](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) репозитория GitHub.

<h2 id="ado-net">
  ADO.NET
</h2>

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

<h3 id="ado-net-datasource">
  Управление жизненным циклом с помощью ClickHouseDataSource
</h3>

**Всегда создавайте соединения через `ClickHouseDataSource`**, чтобы обеспечить корректное управление их жизненным циклом и использование пула соединений. DataSource содержит один экземпляр `ClickHouseClient`, пул HTTP-соединений которого используется всеми соединениями.

```csharp theme={null}
using ClickHouse.Driver.ADO;

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

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

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

Для внедрения зависимостей:

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

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

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

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

<Warning>
  **Не создавайте `ClickHouseConnection` напрямую** в продакшн-коде. При каждом таком создании инициализируются новый HTTP-клиент и пул соединений, что под нагрузкой может привести к исчерпанию доступных сокетов:

  ```csharp theme={null}
  // НЕ ДЕЛАЙТЕ ТАК — каждый раз создаётся новый пул соединений
  using var conn = new ClickHouseConnection("Host=localhost");
  await conn.OpenAsync();
  ```

  Вместо этого всегда используйте `ClickHouseDataSource` или один общий экземпляр `ClickHouseClient`.
</Warning>

***

<h3 id="ado-net-command">
  Использование ClickHouseCommand
</h3>

Создавайте команды через соединение, чтобы выполнять SQL-запросы:

```csharp theme={null}
await using var connection = await dataSource.OpenConnectionAsync();

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

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

Методы выполнения команд:

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

***

<h3 id="ado-net-reader">
  Использование ClickHouseDataReader
</h3>

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

```csharp theme={null}
await using var reader = await command.ExecuteReaderAsync();

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

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

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

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

<h4 id="ado-net-reader-enum-ordinal">
  Чтение числового значения enum
</h4>

Столбец `Enum8` или `Enum16` возвращается в виде метки: `GetFieldType` возвращает `string`, а
`GetString`, `GetValue` и `GetFieldValue<string>` — саму метку. Числовые аксессоры
при обращении к такому столбцу генерируют `InvalidCastException`, поскольку его значение хранится как строка.

Чтобы получить число, соответствующее метке, используйте `TryGetEnumOrdinal`:

```csharp theme={null}
if (reader.TryGetEnumOrdinal(ordinal, out int value))
    Console.WriteLine(value);   // e.g. 1 for 'Active' in Enum8('Active' = 1)
```

Для столбца типа `Enum8`/`Enum16`, а также для столбца типа `Nullable(Enum...)`,
если ячейка не содержит NULL, метод возвращает `true` и записывает порядковый номер в `value`. Если ячейка содержит NULL или
столбец не относится к перечислимому типу, метод возвращает `false` и записывает в `value` значение `0`. Порядковый номер — это
знаковое значение из двоичного представления данных при передаче, поэтому оно может быть отрицательным, а порядковый номер `Enum16` может не помещаться в
один байт.

<h2 id="best-practices">
  Рекомендации
</h2>

<h3 id="best-practices-connection-lifetime">
  Время жизни подключения и пулы соединений
</h3>

`ClickHouse.Driver` использует `System.Net.Http.HttpClient`. `HttpClient` поддерживает отдельный пул соединений для каждой конечной точки. Поэтому:

* Сеансы базы данных передаются через HTTP-соединения, которыми управляет пул соединений.
* Пул автоматически обновляет HTTP-соединения.
* Соединения могут оставаться открытыми после освобождения объектов `ClickHouseClient` или `ClickHouseConnection`.

**Рекомендуемые подходы:**

| Сценарий | Рекомендуемый подход |
| - | - |
| Общие случаи | Используйте единый экземпляр `ClickHouseClient` (singleton) |
| ADO.NET / ORM | Используйте `ClickHouseDataSource` (создаёт соединения, использующие общий пул) |
| Среды с внедрением зависимостей | Зарегистрируйте `ClickHouseClient` или `ClickHouseDataSource` как singleton с помощью `IHttpClientFactory` |

<Warning>
  Если вы используете собственный `HttpClient` или `HttpClientFactory`, убедитесь, что значение `PooledConnectionIdleTimeout` меньше значения `keep_alive_timeout` на сервере. Это позволит избежать ошибок из-за полузакрытых соединений. Для развёртываний в Cloud значение `keep_alive_timeout` по умолчанию составляет 10 секунд.
</Warning>

<Warning>
  Не создавайте несколько экземпляров `ClickHouseClient` или отдельные экземпляры `ClickHouseConnection` без общего `HttpClient`: каждый из них создаёт собственный пул соединений.
</Warning>

***

<h3 id="best-practice-datetime">
  Работа с DateTime
</h3>

1. **По возможности используйте UTC.** Храните временные метки в столбцах `DateTime('UTC')` и используйте `DateTimeKind.Utc` в коде. Это устраняет неоднозначность часовых поясов.

2. **Используйте `DateTimeOffset` для явного учёта часового пояса.** Этот тип всегда обозначает конкретный момент времени и содержит смещение относительно UTC.

3. **Указывайте часовой пояс в подсказках типов SQL.** Если вы передаёте значения DateTime с `Unspecified` в качестве параметров для столбцов с часовым поясом, отличным от UTC, укажите часовой пояс в SQL:
   ```csharp theme={null}
   var parameters = new ClickHouseParameterCollection();
   parameters.AddParameter("dt", myDateTime);

   await client.ExecuteNonQueryAsync(
       "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
       parameters
   );
   ```

***

<h3 id="async-inserts">
  Асинхронные вставки
</h3>

[Асинхронные вставки](/ru/concepts/features/operations/insert/asyncinserts) перекладывают группировку данных в пакеты с клиента на сервер. Клиенту больше не нужно группировать данные: сервер буферизует поступающие данные и сбрасывает их в хранилище при достижении настраиваемых пороговых значений. Это полезно при высокой степени параллелизма, например для задач обсервабилити, когда множество агентов отправляют небольшие порции данных.

Включите асинхронные вставки через `CustomSettings` или строку подключения:

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

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

**Два режима** (определяются параметром `wait_for_async_insert`):

| Режим | Поведение | Когда использовать |
| - | - | - |
| `wait_for_async_insert=1` | Операция вставки завершается после сброса данных на диск. Ошибки возвращаются клиенту. | **Рекомендуется** для большинства рабочих нагрузок |
| `wait_for_async_insert=0` | Операция вставки завершается, как только данные попадают в буфер. Сохранение данных не гарантируется. | Только если потеря данных допустима |

<Warning>
  При `wait_for_async_insert=0` ошибки проявляются только во время сброса данных на диск, поэтому их нельзя связать с исходной операцией вставки. Кроме того, клиент не замедляет отправку данных при перегрузке сервера, что может усугубить её.
</Warning>

**Основные настройки:**

| Настройка | Описание |
| - | - |
| `async_insert_max_data_size` | Сброс данных на диск при достижении буфером указанного размера (в байтах) |
| `async_insert_busy_timeout_ms` | Сброс данных на диск по истечении указанного времени (в миллисекундах) |
| `async_insert_max_query_number` | Сброс данных на диск после накопления указанного числа запросов |

***

<h3 id="best-practices-sessions">
  Сеансы
</h3>

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

* Временные таблицы (`CREATE TEMPORARY TABLE`)
* Сохранение контекста запроса при выполнении нескольких команд
* Настройки на уровне сеанса (`SET max_threads = 4`)

При включённых сеансах запросы обрабатываются последовательно, чтобы исключить одновременное использование одного сеанса. Это создаёт дополнительные накладные расходы для рабочих нагрузок, которым не требуется сохранять состояние сеанса.

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

using var client = new ClickHouseClient(settings);

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

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

**Использование ADO.NET (для совместимости с ORM):**

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

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

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

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

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

***

<h2 id="supported-data-types">
  Поддерживаемые типы данных
</h2>

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

<h3 id="clickhouse-native-type-map-reading">
  Сопоставление типов при чтении из ClickHouse
</h3>

<h4 id="type-map-reading-integer">
  Целочисленные типы
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Int8 | `sbyte` |
| UInt8 | `byte` |
| Int16 | `short` |
| UInt16 | `ushort` |
| Int32 | `int` |
| UInt32 | `uint` |
| Int64 | `long` |
| UInt64 | `ulong` |
| Int128 | `BigInteger` |
| UInt128 | `BigInteger` |
| Int256 | `BigInteger` |
| UInt256 | `BigInteger` |

***

<h4 id="type-map-reading-floating-points">
  Типы с плавающей запятой
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Float32 | `float` |
| Float64 | `double` |
| BFloat16 | `float` |

***

<h4 id="type-map-reading-decimal">
  Десятичные типы
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Decimal(P, S) | `decimal` / `ClickHouseDecimal` |
| Decimal32(S) | `decimal` / `ClickHouseDecimal` |
| Decimal64(S) | `decimal` / `ClickHouseDecimal` |
| Decimal128(S) | `decimal` / `ClickHouseDecimal` |
| Decimal256(S) | `decimal` / `ClickHouseDecimal` |

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

***

<h4 id="type-map-reading-boolean">
  Булевый тип
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Bool | `bool` |

***

<h4 id="type-map-reading-strings">
  Строковые типы
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| String | `string` |
| FixedString(N) | `string` |

<Note>
  По умолчанию значения столбцов типов `String` и `FixedString(N)` возвращаются как `string`. Чтобы получать их как `byte[]`, укажите `ReadStringsAsByteArrays=true` в строке подключения. Это полезно для хранения двоичных данных, которые могут не быть корректным текстом в кодировке UTF-8.

  Настройка действует и для строк внутри других типов: `Array(String)` считывается как `byte[][]`,
  а `Map(String, String)` — как `Dictionary<byte[], byte[]>`, в том числе для ключей. Исключение составляет
  JSON-столбец: строковые значения на конечных уровнях его структуры всегда считываются как текст; см. раздел [Тип JSON](#type-map-reading-json).
</Note>

***

<h4 id="type-map-reading-datetime">
  Типы даты и времени
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Date | `DateTime` |
| Date32 | `DateTime` |
| DateTime | `DateTime` |
| DateTime32 | `DateTime` |
| DateTime64 | `DateTime` |
| Time | `TimeSpan` |
| Time64 | `TimeSpan` |

ClickHouse хранит значения `DateTime` и `DateTime64` в виде Unix-временных меток (числа секунд или долей секунды с начала эпохи Unix). Данные всегда хранятся в UTC, но у столбца может быть задан часовой пояс, который влияет на отображение и интерпретацию значений.

При чтении значений `DateTime` свойство `DateTime.Kind` устанавливается в зависимости от часового пояса столбца:

| Определение столбца | Возвращаемое значение DateTime.Kind | Примечания |
| - | - | - |
| `DateTime('UTC')` | `Utc` | Явно заданный часовой пояс UTC |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | Применено смещение |
| `DateTime` | `Unspecified` | Время сохранено без изменений |

Для столбцов с часовым поясом, отличным от UTC, возвращаемое значение `DateTime` отражает время в указанном часовом поясе. Чтобы получить значение `DateTimeOffset` с соответствующим смещением, используйте `ClickHouseDataReader.GetDateTimeOffset()`:

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

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

Для столбцов **без** явно указанного часового пояса (то есть с типом `DateTime`, а не `DateTime('Europe/Amsterdam')`) драйвер возвращает значение `DateTime` с `Kind=Unspecified`. Так сохраняется исходное локальное время без каких-либо предположений о часовом поясе.

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

1. Укажите часовой пояс в определении столбца: `DateTime('UTC')` или `DateTime('Europe/Amsterdam')`
2. Задайте часовой пояс самостоятельно после чтения.

***

<h4 id="type-map-reading-json">
  Тип JSON
</h4>

| Тип ClickHouse | Тип .NET | Примечания |
| - | - | - |
| Json | `JsonObject` | По умолчанию (`JsonReadMode=Binary`) |
| Json | `string` | При `JsonReadMode=String` |

Тип возвращаемого значения для JSON-столбцов зависит от настройки `JsonReadMode`:

* **`Binary` (по умолчанию)**: Возвращает `System.Text.Json.Nodes.JsonObject`. Позволяет работать со структурой JSON-данных, но некоторые типы ClickHouse (например, IP-адреса, UUID и десятичные числа высокой точности) внутри JSON преобразуются в строковое представление.

* **`String`**: Возвращает JSON без обработки в виде `string`. Сохраняет JSON в точности в том виде, в каком он получен из ClickHouse. Это удобно, если нужно передать JSON дальше без разбора или самостоятельно выполнить десериализацию.

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

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

`None` — третий режим. Он считывает данные так же, как `Binary`, но не передаёт настройку сервера вместе с
запросом. Используйте его, если для соединения запрещено задавать эту настройку.

<h5 id="type-map-reading-json-nulls">
  Типизированные пути и значения null
</h5>

Путь, объявленный в типе столбца, — это **типизированный путь**; любой другой путь в документе —
**динамический путь**. Разница между ними проявляется, когда значение равно null.

Типизированный путь всегда присутствует в `JsonObject`. Если он объявлен с типом `Nullable(T)` или `Dynamic`, ему
соответствует значение null в JSON как в случае, когда сохранённое значение равно null, так и в случае, когда такого
пути нет в документе. Различить эти случаи невозможно:

```csharp theme={null}
// Column type JSON(x Nullable(Int64))
// stored '{"x":null}'  ->  {"x":null}
// stored '{}'          ->  {"x":null}
```

Если путь объявлен с типом, не допускающим NULL, то при отсутствии пути используется значение этого типа по умолчанию: `JSON(x String)`
даёт `{"x":""}`, а `JSON(x Int64)` — `{"x":0}`.

Динамический путь со значением null полностью исключается из объекта, поэтому `ContainsKey` возвращает
для него false. При чтении `{"x":null}` из обычного JSON-столбца получается `{}`.

Вложенные типизированные пути создают родительские узлы, поэтому `JSON(a.b Nullable(Int64))` даёт `{"a":{"b":null}}`
даже для пустого документа.

<Note>
  Именно так объект отображает сам сервер, поэтому режимы `Binary` и `String` теперь дают одинаковый результат. До версии 1.4.0
  типизированный путь со значением null удалялся из `JsonObject`, из-за чего `{"x":null}` при чтении
  превращался в `{}`. Для вложенного пути, например `JSON(a.b Nullable(Int64))`, исчезало всё поддерево `a`.
</Note>

<h5 id="type-map-reading-json-strings">
  Строки внутри JSON-столбца
</h5>

Строковые значения в конечных узлах `JSON`-столбца всегда возвращаются как текст, независимо от значения
`ReadStringsAsByteArrays`: у `JsonValue` нет представления в виде массива байтов, поэтому `byte[]`
отображался бы в кодировке base64. Это относится к `String`, `FixedString`, типам, обёрнутым в
`LowCardinality`, `Nullable` или `SimpleAggregateFunction`, а также к строкам внутри `Array` и `Map`,
включая ключи карт.

<Note>
  Если считыватель JSON не может определить тип массива байтов, тот всё равно отображается в кодировке base64:
  путь типа `Variant` или `Dynamic` содержит значение, тип которого определяется отдельно для каждой строки, поэтому строка
  в `Variant(Array(UInt8), String)` возвращается в кодировке base64. Это справедливо при обоих значениях настройки.

  Если тип ключа карты JSON не совпадает в точности с `String` — например, это `Map(LowCardinality(String), String)` —
  возникает `NotSupportedException`.
</Note>

<h5 id="overlapping-paths">
  Пересекающиеся пути
</h5>

ClickHouse допускает объявление столбца, в котором один путь служит и значением, и родительским путём для другого,
например `JSON(a Int64, a.b Int64)`. Оба пути присутствуют в каждой строке, поэтому сервер
формирует строку с повторяющимся ключом: `{"a":0,"a":{"b":7}}`. `JsonObject` не может содержать два значения
для одного ключа, поэтому `JsonReadMode.Binary` генерирует `SerializationException` с указанием обоих путей. То
же происходит, если значение имеет тип `Map`, как в `JSON(a Map(String, Int64))`, а в считываемой строке
также есть динамический путь `a.b`.

Это относится только к строкам, в которых оба пути содержат значения. Если в одном из них нет значения — null,
пустой объект или поддерево, все значения которого равны null, — сохраняется значение другого пути
независимо от того, какой из них сервер отправляет первым. Поэтому при пересечении путей с типами `Nullable` в каждой строке значение берётся только из одного пути, и чтение проходит без
ошибки: `JSON(a Nullable(Int64), a.b Nullable(Int64))` даёт `{"a":5}` и `{"a":{"b":7}}`, как
и ожидается.

Чтобы получить JSON-текст сервера без изменений, включая повторяющийся
ключ, считывайте такой столбец в режиме `JsonReadMode.String`.

Установите `AllowDuplicateJsonKeys`, чтобы считывать столбец как `JsonObject` без исключения. В этом случае
драйвер сохраняет последнее из двух значений в строке и отбрасывает другое, поэтому
часть данных теряется: `JSON(a Int64, a.b Int64)` со значением `{"a.b":7}` считывается как `{"a":0}`. Если путь
содержит значение, а его родительский путь — скалярное значение или массив, исключение всё равно возникает: поддерево нельзя
поместить ни в скалярное значение, ни в массив.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost")
{
    AllowDuplicateJsonKeys = true
};

// Or via connection string
// "Host=localhost;AllowDuplicateJsonKeys=true"
```

***

<h4 id="type-map-reading-map">
  Тип Map
</h4>

| Тип ClickHouse | Тип .NET | Примечания |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | По умолчанию (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | При `MapReadMode=KeyValuePairs` |

Тип `Map(K, V)` в ClickHouse физически хранится как `Array(Tuple(K, V))` и может содержать несколько пар с одинаковым ключом. `Dictionary` этого не допускает, поэтому в режиме по умолчанию для каждого ключа сохраняется только последнее значение, а предыдущие пары отбрасываются. Настройка `MapReadMode` определяет, в каком виде возвращаются данные:

* **`Dictionary` (по умолчанию)**: возвращает `Dictionary<K, V>`.

* **`KeyValuePairs`**: возвращает `List<KeyValuePair<K, V>>` в порядке отправки пар сервером. При этом сохраняются все пары, в том числе с повторяющимися ключами.

```csharp theme={null}
// Configure key-value-pair mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    MapReadMode = MapReadMode.KeyValuePairs
};

// Or via connection string
// "Host=localhost;MapReadMode=KeyValuePairs"
```

Режим определяет, каким типом .NET представлен столбец `Map`. Он также влияет на `GetFieldValue<T>`, типы схемы, возвращаемые драйвером, и сопоставление свойств POCO. Режим действует на любом уровне дерева типов столбца, где встречается Map, в том числе в `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` и `Dynamic`.

В любом режиме при записи принимаются оба представления — см. раздел [«Запись значений Map»](#type-map-writing-other).

***

<h4 id="type-map-reading-other">
  Другие типы
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| UUID | `Guid` |
| IPv4 | `IPAddress` |
| IPv6 | `IPAddress` |
| Nothing | `DBNull` |
| Dynamic | См. примечание |
| Array(T) | `T[]` (вложенный `Array(Array(T))` считывается как ступенчатый массив `T[][]`; чтобы получить данные в виде прямоугольного многомерного массива CLR, используйте `reader.GetFieldValue<T[,]>(ordinal)`) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>` или `List<KeyValuePair<K, V>>` при `MapReadMode=KeyValuePairs` — см. [тип Map](#type-map-reading-map) |
| Nullable(T) | `T?` |
| Enum8 | `string` |
| Enum16 | `string` |
| LowCardinality(T) | Тот же тип, что и T |
| SimpleAggregateFunction | Тот же тип, что и базовый тип |
| Nested(...) | `Tuple[]` |
| Variant(T1, T2, ...) | См. примечание |
| QBit(T, dimension) | `T[]` |

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

***

<h4 id="type-map-reading-geometry">
  Геометрические типы
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Point | `Tuple<double, double>` |
| Ring | `Tuple<double, double>[]` |
| LineString | `Tuple<double, double>[]` |
| Polygon | `Ring[]` |
| MultiLineString | `LineString[]` |
| MultiPolygon | `Polygon[]` |
| Geometry | См. примечание |

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

***

<h3 id="clickhouse-native-type-map-writing">
  Соответствие типов при записи в ClickHouse
</h3>

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

<h4 id="type-map-writing-integer">
  Целочисленные типы
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Int8 | `sbyte`, любой тип, совместимый с `Convert.ToSByte()` | |
| UInt8 | `byte`, любой тип, совместимый с `Convert.ToByte()` | |
| Int16 | `short`, любой тип, совместимый с `Convert.ToInt16()` | |
| UInt16 | `ushort`, любой тип, совместимый с `Convert.ToUInt16()` | |
| Int32 | `int`, любой тип, совместимый с `Convert.ToInt32()` | |
| UInt32 | `uint`, любой тип, совместимый с `Convert.ToUInt32()` | |
| Int64 | `long`, любой тип, совместимый с `Convert.ToInt64()` | |
| UInt64 | `ulong`, любой тип, совместимый с `Convert.ToUInt64()` | |
| Int128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |
| UInt128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |
| Int256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |
| UInt256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |

***

<h4 id="type-map-writing-floating-point">
  Типы с плавающей запятой
</h4>

| Тип ClickHouse | Допустимый тип .NET | Примечания |
| - | - | - |
| Float32 | `float` или любой тип, совместимый с `Convert.ToSingle()` | |
| Float64 | `double` или любой тип, совместимый с `Convert.ToDouble()` | |
| BFloat16 | `float` или любой тип, совместимый с `Convert.ToSingle()` | Значение усекается до 16-битного формата brain float |

***

<h4 id="type-map-writing-boolean">
  Булевый тип
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Bool | `bool` | |

***

<h4 id="type-map-writing-strings">
  Строковые типы
</h4>

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

***

<h4 id="type-map-writing-datetime">
  Типы даты и времени
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Date | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Преобразуются в количество дней с начала эпохи Unix в виде UInt16; поддерживаемый диапазон `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Преобразуются в количество дней с начала эпохи Unix в виде Int32; поддерживаемый диапазон `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Подробнее см. ниже; поддерживаемый диапазон `[1970-01-01, 2106-02-07 06:28:15]` UTC |
| DateTime32 | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | То же, что и DateTime |
| DateTime64 | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Точность определяется параметром масштаба |
| Time | `TimeSpan`, `TimeOnly`, `int` | Значение ограничивается пределами ±999:59:59; int интерпретируется как число секунд |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | Строка разбирается в формате `[-]HHH:MM:SS[.fraction]`; значение ограничивается пределами ±999:59:59.999999999 |

<Note>
  **Значения вне допустимого диапазона**

  При записи в бинарном формате значения `Date`, `Date32`, `DateTime` и `DateTime32` вне поддерживаемого диапазона приводят к исключению `ArgumentOutOfRangeException` при вызове `Write`. В сообщении указываются тип столбца и поддерживаемый диапазон. Ранее такие значения могли незаметно усекаться при преобразовании в 32-битное целое число, а затем переинтерпретироваться сервером. В результате получались корректные по формату, но неверные временные метки.
</Note>

При записи значений драйвер учитывает `DateTime.Kind`:

| DateTime.Kind | Параметры HTTP | Пакетная запись |
| - | - | - |
| Utc | Точный момент времени сохраняется | Точный момент времени сохраняется |
| Local | Точный момент времени сохраняется | Точный момент времени сохраняется |
| Unspecified | Интерпретируется как время в часовом поясе типа параметра без пересчёта (по умолчанию UTC) | Интерпретируется как время в часовом поясе столбца без пересчёта |

Значения `DateTimeOffset` всегда сохраняют точный момент времени.

**Пример: DateTime в UTC (точный момент времени сохраняется)**

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

**Пример: дата и время без указания часового пояса (местное время)**

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

**Рекомендация:** чтобы работа с датой и временем была максимально простой и предсказуемой, всегда используйте `DateTimeKind.Utc` или `DateTimeOffset`. Тогда код будет работать одинаково независимо от часового пояса сервера, клиента или столбца.

<h4 id="datetime-http-param-vs-bulkcopy">
  Параметры HTTP и пакетная загрузка
</h4>

При записи значений DateTime с типом `Unspecified` привязка параметров HTTP и пакетная загрузка отличаются в одном важном отношении:

**Пакетная загрузка** учитывает часовой пояс целевого столбца и правильно интерпретирует в нём значения `Unspecified`.

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

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

// INCORRECT: Without timezone hint, interpreted as UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// String value "2024-01-15 14:30:00" interpreted as UTC, not Amsterdam time!
```

| `DateTime.Kind` | Целевой столбец | HTTP-параметр (с указанием часового пояса) | HTTP-параметр (без указания часового пояса) | Пакетная загрузка |
| - | - | - | - | - |
| `Utc` | UTC | Точный момент времени сохраняется | Точный момент времени сохраняется | Точный момент времени сохраняется |
| `Utc` | Europe/Amsterdam | Точный момент времени сохраняется | Точный момент времени сохраняется | Точный момент времени сохраняется |
| `Local` | Любой | Точный момент времени сохраняется | Точный момент времени сохраняется | Точный момент времени сохраняется |
| `Unspecified` | UTC | Интерпретируется как время в UTC | Интерпретируется как время в UTC | Интерпретируется как время в UTC |
| `Unspecified` | Europe/Amsterdam | Интерпретируется как время в часовом поясе Амстердама | **Интерпретируется как время в UTC** | Интерпретируется как время в часовом поясе Амстердама |

***

<h4 id="type-map-writing-decimal">
  Десятичные типы
</h4>

| Тип ClickHouse | Допустимый тип .NET | Примечания |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Вызывает исключение `OverflowException` при превышении допустимой точности |
| Decimal32 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность — 9 |
| Decimal64 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность — 18 |
| Decimal128 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность — 38 |
| Decimal256 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность — 76 |

***

<h4 id="type-map-writing-json">
  Тип JSON
</h4>

| Тип ClickHouse | Допустимый тип .NET | Примечания |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, любой объект | Поведение зависит от настройки `JsonWriteMode` |

Поведение при записи JSON определяется настройкой `JsonWriteMode`:

| Тип входных данных | `JsonWriteMode.String` (по умолчанию) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | Передаётся без изменений | Генерируется `ArgumentException` |
| `JsonObject` | Сериализуется с помощью `ToJsonString()` | Генерируется `ArgumentException` |
| `JsonNode` | Сериализуется с помощью `ToJsonString()` | Генерируется `ArgumentException` |
| Зарегистрированный POCO | Сериализуется с помощью `JsonSerializer.Serialize()` | Двоичное кодирование с подсказками типов; поддерживаются пользовательские атрибуты путей |
| Незарегистрированный POCO / анонимный объект | Сериализуется с помощью `JsonSerializer.Serialize()` | Генерируется `ClickHouseJsonSerializationException` |

* **`String` (по умолчанию)**: Принимает `string`, `JsonObject`, `JsonNode` или любой объект. Все входные данные сериализуются с помощью `System.Text.Json.JsonSerializer` и передаются как JSON-строки для разбора на стороне сервера. Это наиболее гибкий режим: он не требует регистрации типов.

* **`Binary`**: Принимает только зарегистрированные типы POCO. Данные преобразуются на стороне клиента в двоичный JSON-формат ClickHouse с полной поддержкой подсказок типов. Перед использованием необходимо вызвать `connection.RegisterJsonSerializationType<T>()`. При записи значений `string` или `JsonNode` в этом режиме генерируется `ArgumentException`.

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

// Binary mode requires explicit opt-in and type registration
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

<h5 id="json-typed-columns">
  Типизированные JSON-столбцы
</h5>

Если для JSON-столбца заданы подсказки типов (например, `JSON(id UInt64, price Decimal128(2))`), драйвер использует их при сериализации, чтобы точно сохранить типы значений. Это позволяет избежать потери точности для таких типов, как `UInt64`, `Decimal`, `UUID` и `DateTime64`, при сериализации в обычный JSON.

<h5 id="json-poco-serialization">
  Сериализация POCO
</h5>

Объекты POCO можно записывать в JSON-столбцы двумя способами в зависимости от значения `JsonWriteMode`:

**Строковый режим (по умолчанию)**: объекты POCO сериализуются с помощью `System.Text.Json.JsonSerializer`. Регистрировать типы не нужно. Это самый простой способ, который подходит и для анонимных объектов.

**Двоичный режим**: объекты POCO сериализуются в двоичный JSON-формат драйвера с полной поддержкой подсказок типов. Перед использованием типы необходимо зарегистрировать с помощью `connection.RegisterJsonSerializationType<T>()`. В этом режиме атрибуты позволяют сопоставлять свойства с произвольными путями:

* **`[ClickHouseJsonPath("path")]`**: сопоставляет свойство с заданным JSON-путём. Это полезно для вложенных структур или когда имя свойства не совпадает с нужным ключом JSON. **Работает только в двоичном режиме.**

* **`[ClickHouseJsonIgnore]`**: исключает свойство из сериализации. **Работает только в двоичном режиме.**

```sql theme={null}
CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
```

```csharp theme={null}
using ClickHouse.Driver.Json;

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

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

    public DateTime Timestamp { get; set; }

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

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

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

При сопоставлении имён свойств с подсказками типов столбца учитывается регистр. Свойство `UserId` будет сопоставлено только с подсказкой `UserId`, но не с `userid`. Это соответствует поведению ClickHouse: пути `userName` и `UserName` могут существовать как разные поля.

**Ограничения (только для режима Binary mode):**

* Перед сериализацией типы POCO необходимо зарегистрировать для соединения с помощью `connection.RegisterJsonSerializationType<T>()`. Попытка сериализовать незарегистрированный тип приводит к исключению `ClickHouseJsonSerializationException`.
* Для корректной сериализации свойств, содержащих словари, массивы или списки, в определении столбца необходимо указать подсказки типов. Если подсказок нет, используйте режим String mode.
* Значения null в свойствах POCO записываются только в том случае, если для соответствующего пути в определении столбца указана подсказка типа `Nullable(T)`. ClickHouse не допускает типы `Nullable` внутри динамических JSON-путей, поэтому свойства со значением null без подсказок пропускаются.
* Атрибуты `ClickHouseJsonPath` и `ClickHouseJsonIgnore` игнорируются в режиме String mode (они работают только в режиме Binary mode).

***

<h4 id="type-map-writing-other">
  Другие типы
</h4>

| Тип ClickHouse | Допустимый тип .NET | Примечания |
| - | - | - |
| UUID | `Guid`, `string` | Строка разбирается как Guid |
| IPv4 | `IPAddress`, `string` | Требуется адрес IPv4; строка разбирается с помощью `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Требуется адрес IPv6; строка разбирается с помощью `IPAddress.Parse()` |
| Nothing | Любой | Ничего не записывает (операция не имеет эффекта) |
| Dynamic | — | **Не поддерживается** (генерируется `NotImplementedException`) |
| Array(T) | `IList`, `null` | При значении null записывается пустой массив. Для вложенных типов (`Array(Array(T))` и глубже) допускаются как ступенчатые массивы (`T[][]`, `List<List<T>>`), так и прямоугольные многомерные массивы CLR (`T[,]`, `T[,,]`, …); число измерений массива CLR должно соответствовать глубине вложенности в ClickHouse. |
| Tuple(T1, T2, ...) | `ITuple`, `IList` | Число элементов должно соответствовать числу элементов Tuple. Если элементов больше 7, см. [особенность ValueTuple](#valuetuple-caveat). |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | Последовательность пар (например, `List<KeyValuePair<K, V>>`, полученный при `MapReadMode=KeyValuePairs`) допускается при любом режиме чтения и может содержать повторяющиеся ключи. Это относится к бинарным вставкам и параметрам запросов |
| Nullable(T) | `null`, `DBNull` или типы, допустимые для T | Перед значением записывается байт, указывающий, является ли оно null |
| Enum8 | `string`, `sbyte`, числовые типы | Строка ищется в словаре перечисления |
| Enum16 | `string`, `short`, числовые типы | Строка ищется в словаре перечисления |
| LowCardinality(T) | Типы, допустимые для T | Обработка передаётся базовому типу |
| SimpleAggregateFunction | Типы, допустимые для базового типа | Обработка передаётся базовому типу |
| Nested(...) | `IList` из Tuple | Число элементов должно соответствовать числу полей |
| Variant(T1, T2, ...) | Значение, соответствующее одному из типов T1, T2, ... | Если значение не соответствует ни одному типу, генерируется `ArgumentException` |
| QBit(T, dim) | `IList` | Обработка передаётся Array; размерность используется только как метаданные |

***

<h4 id="type-map-writing-geometry">
  Геометрические типы
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Point | `System.Drawing.Point`, `ITuple`, `IList` (2 элемента) | |
| Ring | `IList` объектов Point | |
| LineString | `IList` объектов Point | |
| Polygon | `IList` объектов Ring | |
| MultiLineString | `IList` объектов LineString | |
| MultiPolygon | `IList` объектов Polygon | |
| Geometry | Любой из перечисленных выше геометрических типов | Variant для всех геометрических типов |

***

<h4 id="type-map-writing-not-supported">
  Не поддерживается при записи
</h4>

| Тип ClickHouse | Примечания |
| - | - |
| Dynamic | Генерирует исключение `NotImplementedException` |
| AggregateFunction | Генерирует исключение `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  Работа с типом Nested
</h3>

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

```sql theme={null}
CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
```

```csharp theme={null}
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

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

<h2 id="logging-and-diagnostics">
  Логирование и диагностика
</h2>

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

<h3 id="logging-quick-start">
  Быстрый старт
</h3>

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

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

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

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-appsettings-config">
  Использование appsettings.json
</h4>

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

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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

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

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

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-inmemory-config">
  Использование конфигурации в оперативной памяти
</h4>

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

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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

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

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

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

using var client = new ClickHouseClient(settings);
```

<h3 id="logging-categories">
  Категории и источники сообщений
</h3>

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

| Категория | Источник | Что регистрируется |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Жизненный цикл соединения, выбор фабрики HTTP-клиентов, открытие и закрытие соединения, управление сеансами. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Начало и завершение выполнения запросов, время выполнения, идентификаторы запросов, статистика сервера и сведения об ошибках. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Низкоуровневые запросы с потоковой передачей по HTTP, флаги сжатия, коды состояния ответов и ошибки передачи данных. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | Бинарная вставка данных, запросы и другие операции |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | Низкоуровневая трассировка сети, только при включённом режиме отладки |

<h4 id="logging-config-example">
  Пример: диагностика проблем с соединением
</h4>

```json theme={null}
{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}
```

В журнал будут записываться сведения о следующем:

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

<h3 id="logging-debugmode">
  Режим отладки: трассировка сети и диагностика
</h3>

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

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

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

<h2 id="opentelemetry">
  OpenTelemetry
</h2>

Драйвер имеет встроенную поддержку распределённой трассировки OpenTelemetry через API .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). Если трассировка включена, драйвер создаёт спаны операций с базой данных. Их можно экспортировать в системы обсервабилити, например Jaeger или сам ClickHouse (через [OpenTelemetry Collector](/ru/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

<h3 id="opentelemetry-enabling">
  Включение трассировки
</h3>

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

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

Для консольных приложений, тестирования или настройки вручную:

```csharp theme={null}
using OpenTelemetry;
using OpenTelemetry.Trace;

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

<h3 id="opentelemetry-attributes">
  Атрибуты спанов
</h3>

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

| Атрибут | Описание |
| - | - |
| `db.system` | Всегда `"clickhouse"` |
| `db.name` | Имя базы данных |
| `db.user` | Имя пользователя |
| `db.statement` | SQL-запрос (если включён сбор) |
| `db.clickhouse.read_rows` | Количество строк, прочитанных при выполнении запроса |
| `db.clickhouse.read_bytes` | Количество байт, прочитанных при выполнении запроса |
| `db.clickhouse.written_rows` | Количество строк, записанных при выполнении запроса |
| `db.clickhouse.written_bytes` | Количество байт, записанных при выполнении запроса |
| `db.clickhouse.elapsed_ns` | Время выполнения на стороне сервера в наносекундах |

<h3 id="opentelemetry-configuration">
  Параметры конфигурации
</h3>

Настройте поведение трассировки с помощью `ClickHouseDiagnosticsOptions`:

```csharp theme={null}
using ClickHouse.Driver.Diagnostic;

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

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

<Warning>
  При включении `IncludeSqlInActivityTags` конфиденциальные данные могут попасть в трассировку. Используйте эту настройку с осторожностью в продакшне.
</Warning>

<h2 id="tls-configuration">
  Настройка TLS
</h2>

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

<h3 id="custom-certificate-validation">
  Пользовательская проверка сертификата
</h3>

Если для продакшна требуется особая логика проверки сертификата, передайте собственный `HttpClient` с настроенным обработчиком `ServerCertificateCustomValidationCallback`:

```csharp theme={null}
using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // No AutomaticDecompression needed: the driver decodes compressed responses itself.
    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Example: Accept a specific certificate thumbprint
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

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

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

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

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

using var client = new ClickHouseClient(settings);
```

<Note>
  Что важно учитывать при использовании собственного HttpClient

  * **Автоматическая распаковка**: не включайте `AutomaticDecompression`. Драйвер сам распаковывает сжатые ответы, поэтому эта функция не нужна. Более того, при её включении обработчик во время отправки запроса *также добавляет* в исходящий заголовок `Accept-Encoding` все алгоритмы из своей маски. Это расширяет список, указанный драйвером, и ClickHouse может ответить, используя кодек, который вы не запрашивали. См. [Распаковка ответов](#response-decompression).
  * **Тайм-аут простоя**: задайте для `PooledConnectionIdleTimeout` значение меньше серверного `keep_alive_timeout` (10 секунд для ClickHouse Cloud), чтобы избежать ошибок из-за полуоткрытых соединений.
</Note>

<h2 id="performance-tuning">
  Настройка производительности
</h2>

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

<h3 id="perf-at-a-glance">
  Кратко о настройке производительности
</h3>

\| Если вы | Рекомендуется |
\|---|---|---|
\| Считываете строки в объекты POCO | Используйте [`QueryAsync<T>`](#perf-read-path) вместо `MapTo<T>` |
\| Выполняете вставку больших объёмов данных | Увеличьте [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Запускаете консольное приложение или воркер, часто выполняющий вставку данных | Включите [Server GC](#perf-gc) |
\| Считываете большие объёмы данных по сети | Не отключайте сжатие ответа (оно включено по умолчанию) |
\| Вставляете данные через быстрое соединение | Попробуйте [`InsertOptions.Compressor = null`](#perf-compression) |
\| Многократно вставляете данные в одну и ту же таблицу | Используйте [`UseSchemaCache` или `ColumnTypes`](#skip-schema-query) |
\| Считываете очень большие объёмы данных | Увеличьте [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  Чтение: выберите способ материализации
</h3>

Строку результата можно получить разными способами, и затраты на них различаются. При некоторых способах значения упаковываются в объекты, что увеличивает число выделений памяти и снижает производительность.

| Способ чтения | Упаковывает каждое значение | Примечания |
| - | - | - |
| `QueryAsync<T>` | **Нет** | Считывает данные из потока напрямую в свойства объекта. Быстрый путь. |
| Типизированные аксессоры считывателя (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **Нет** | Считывает значения из типизированного хранилища без упаковки. |
| `MapTo<T>` | Да | Сначала материализует строку, а затем копирует из неё значения. |
| `GetValue` и `GetValues` | Да | Возвращают `object`, поэтому при обращении к значениям их приходится упаковывать. |

При чтении 1,000,000 строк и 105 столбцов из набора данных *hits*:

| API | Выделено памяти |
| - | -: |
| `QueryAsync<T>` | **1,372 MB** |
| `MapTo<T>` | 3,133 MB |

```csharp theme={null}
// Fast path: register the type once, then stream rows directly into it.
client.RegisterPocoType<HitRow>();

await foreach (var row in client.QueryAsync<HitRow>("SELECT * FROM hits"))
    Process(row);
```

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

***

<h3 id="perf-insert-batching">
  Вставка: размер батча и степень параллелизма
</h3>

Размер батча сильнее всего влияет на пропускную способность вставки. По умолчанию `InsertOptions.BatchSize` составляет
100 000 строк.

**Используйте большие батчи.** При вставке 1 000 000 строк увеличение размера батча с 10 000 до 100 000 строк
дало следующие результаты:

| Вставка | 10 000 строк/батч | 100 000 строк/батч | |
| - | -: | -: | -: |
| POCO | 15 308 мс | 7 853 мс | −49% |
| `object[]` | 17 027 мс | 10 671 мс | −37% |

Если вы не можете управлять размером батча (например, когда множество источников независимо друг от друга отправляют небольшие порции строк), используйте [асинхронные вставки](#async-inserts): сервер сам объединит строки в батчи.

**Параллельная отправка.** По умолчанию `InsertOptions.MaxDegreeOfParallelism` равен `1`. Увеличьте это значение, чтобы отправлять
батчи одновременно. Это особенно полезно при включённом сжатии: каждый батч будет сжиматься
в отдельном потоке. Сеансы несовместимы с параллельными вставками: либо отключите сеансы, либо оставьте
`MaxDegreeOfParallelism = 1`.

**Отключите предварительный запрос для определения схемы.** Каждый вызов `InsertBinaryAsync` сначала отправляет запрос `SELECT ... WHERE 1=0`,
чтобы определить типы столбцов. Как избежать этого дополнительного обращения к серверу с помощью `ColumnTypes` или `UseSchemaCache`,
см. в разделе [Отключение предварительного запроса для определения схемы](#skip-schema-query).

<Note>
  Вставка без упаковки значений применяется к формату `RowBinary`, используемому по умолчанию. При использовании `RowBinaryWithDefaults`
  необходимо проверять каждое значение на наличие маркера `DBDefault`, поэтому вставка выполняется более медленным способом.
</Note>

***

<h3 id="perf-compression">
  Сжатие: результат зависит от направления передачи
</h3>

Сжатие сокращает объём передаваемых данных за счёт дополнительных затрат CPU. Выгодно ли оно, зависит от направления
передачи, пропускной способности соединения с сервером ClickHouse, эффективности выбранного алгоритма сжатия для ваших данных и того, платите ли вы за каждый переданный байт.

**Чтение:** оставьте сжатие включённым, если сервер не работает на том же компьютере. По умолчанию оно включено. По сравнению с передачей без сжатия `zstd` на уровне 1
показал следующие результаты:

| Расположение клиента относительно сервера | Эффект сжатия |
| - | - |
| Тот же хост (loopback) | Затраты выше на 8% |
| Тот же облачный регион | **Экономия 16%** |
| Другой регион | **Экономия 33%** |

**Вставка:** прежде чем включать сжатие, проведите измерения. Выигрыш может оказаться слишком небольшим, чтобы оправдать его использование. Учитывайте также, что распаковка создаёт дополнительную нагрузку на сервер: для Zstd и LZ4 она невелика, но для других алгоритмов (например, Brotli) может быть высокой.

Чтобы отключить сжатие при вставке:

```csharp theme={null}
var options = new InsertOptions { Compressor = null };
await client.InsertBinaryAsync("my_table", columns, rows, options);
```

О выборе кодека, уровнях сжатия и определении точки, после которой сжатие становится выгодным в ваших условиях, см. в разделе
[«Настройка сжатия»](#tuning-compression).

***

<h3 id="perf-buffers">
  Буферы
</h3>

`ReadBufferSize` задаёт размер буфера для чтения HTTP-ответов. По умолчанию он составляет 64 КиБ.

Драйвер берёт буфер из общего пула и возвращает его при освобождении считывателя, поэтому
выделять память под буфер для каждого запроса не требуется. Увеличьте размер буфера, чтобы сократить число его повторных заполнений при обработке больших
результатов. Драйвер использует отдельный буфер для каждого открытого считывателя, поэтому потребление памяти
растёт как с размером буфера, так и с числом одновременно работающих считывателей.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost") { ReadBufferSize = 256 * 1024 };
```

<Warning>
  *Всегда освобождайте считыватели.* При освобождении считывателя его буфер возвращается в пул, а HTTP-соединение освобождается.
  Если оставить считыватель без освобождения, буфер не вернётся в пул, а HTTP-соединение может остаться
  недоступным; обычная сборка мусора не заменяет освобождение считывателя.
</Warning>

***

<h3 id="perf-gc">
  Среда выполнения и сборка мусора
</h3>

**Включите Server GC для приложений, которые часто выполняют вставку данных.** При одном и том же коде и одинаковом объёме
выделенной памяти вставка данных с Workstation GC выполнялась до 97 % медленнее, чем с Server GC.

```xml theme={null}
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

В проектах ASP.NET Core этот параметр уже задан. В консольных приложениях, службах-воркерах и большинстве контейнерных
образов — нет.

Причина — размер бюджета для поколения 0. В режиме Workstation GC он невелик, поэтому
кратковременно используемые буферы, создаваемые при вставке, не успевают освободиться в поколении 0. Вместо этого они переходят в поколение 1,
из-за чего чаще продвигаются в следующие поколения и значительно увеличивают объём работы сборщика мусора в поколении 2. В одном из сценариев вставки
на каждую 1 000 операций приходилось 4 000 сборок мусора в поколении 2 при Server GC и 73 000 при
Workstation GC.

<Note>
  Server GC предназначен для повышения пропускной способности, а не для снижения задержек. В тех же измерениях при Server GC суммарное
  время пауз сократилось более чем вдвое, но отдельные паузы были длиннее
  (95-й процентиль — 114,6 мс против 61,9 мс). Если для вашего сервиса важна задержка самых медленных запросов, измерьте
  оба режима, прежде чем сделать выбор.
</Note>

***

<h3 id="perf-latency">
  Задержка: повторное использование соединений
</h3>

Установление нового TCP-соединения и TLS-рукопожатие занимают значительное время.
Повторное использование соединений существенно снижает задержку при выполнении запросов.

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

Полный набор рекомендуемых подходов приведён в разделе
[Время жизни подключения и управление пулом соединений](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  Измерьте производительность самостоятельно
</h3>

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

Чтобы оценить, какую часть работы выполняет сервер, задайте `QueryOptions.QueryId` и получите значения счётчиков:

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

***

<h2 id="orm-support">
  Поддержка ORM
</h2>

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

```csharp theme={null}
// Register DataSource as singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

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

<h3 id="orm-support-dapper">
  Dapper
</h3>

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

Используйте `ClickHouseDataSource` для корректного управления временем жизни подключений:

```csharp theme={null}
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Register as singleton in DI

using var connection = dataSource.CreateConnection();
```

<h4 id="dapper-parameter-passing">
  Способы передачи параметров
</h4>

Поддерживаются все стандартные способы передачи параметров в Dapper:

**Анонимные объекты:**

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

**Классы POCO:**

```csharp theme={null}
class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

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

**Словарь:**

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

**`DynamicParameters` (на основе словаря или анонимного объекта):**

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

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

<h4 id="dapper-pocos">
  Получение результатов запросов в виде POCO
</h4>

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

```csharp theme={null}
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

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

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

<h4 id="dapper-clickhouse-param-syntax">
  Нативный синтаксис параметров ClickHouse
</h4>

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

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

<h4 id="dapper-where-in">
  WHERE IN
</h4>

**Штатный механизм развёртывания IN в Dapper работает:**

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

Dapper переписывает условие как `WHERE id IN (@Ids1, @Ids2, @Ids3)`, а драйвер преобразует каждый параметр, полученный при развёртывании.

**Функция ClickHouse `has()` также работает с параметром типа Array:**

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

<h4 id="dapper-type-handlers">
  Пользовательские обработчики типов
</h4>

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

```csharp theme={null}
// ClickHouseDecimal (for Decimal64/128/256 columns)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

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

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

Пример реализации пользовательского обработчика типов см. в [примере с Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/ORM/ORM_001_Dapper.cs).

<h4 id="dapper-contrib">
  Dapper.Contrib
</h4>

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

```csharp theme={null}
[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

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

Имена свойств должны точно совпадать с именами столбцов ClickHouse (с учётом регистра).

<h4 id="dapper-limitations">
  Ограничения
</h4>

| Что | Статус | Подробности |
| - | - | - |
| Tuple в **результате** | Работает | Необходимо зарегистрировать `SqlMapper.TypeHandler<ITuple>` |
| Tuple в качестве **параметра** | Не поддерживается | Dapper не может сериализовать `ITuple`/`Tuple<>` в значение `DbParameter` |
| Вложенные типы в качестве параметра | Не поддерживаются | По той же причине: Dapper не принимает сложные типы в качестве значений параметров |
| Гео-типы в качестве параметра | Не поддерживаются | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | Не поддерживается | Генерирует SQL-синтаксис, специфичный для SQL Server |
| Тип `Nothing` | Не поддерживается | Не имеет осмысленного представления в .NET |

<h3 id="orm-support-linq2db">
  Linq2db
</h3>

Этот драйвер совместим с [linq2db](https://github.com/linq2db/linq2db) — легковесной ORM и LINQ-провайдером для .NET. Подробную документацию см. на сайте проекта.

**Пример использования:**

Создайте `DataConnection` с помощью провайдера ClickHouse:

```csharp theme={null}
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

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

await using var db = new DataConnection(options);
```

Сопоставление с таблицами можно настроить с помощью атрибутов или программного API конфигурации. Если имена классов и свойств в точности совпадают с именами таблиц и столбцов, дополнительная настройка не требуется:

```csharp theme={null}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
```

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

```csharp theme={null}
await using var db = new DataConnection(options);

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

**Пакетная загрузка:**

Для эффективной массовой вставки данных используйте `BulkCopyAsync`.

```csharp theme={null}
await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

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

await table.BulkCopyAsync(options, products);
```

<h3 id="orm-support-ef-core">
  Entity Framework Core
</h3>

Официальный провайдер Entity Framework Core для ClickHouse. Он позволяет сопоставлять классы C# с таблицами ClickHouse, выполнять запросы с помощью LINQ и вставлять данные через `SaveChanges` — всё с использованием привычных механизмов EF Core.

* **NuGet**: [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **Исходный код**: [GitHub](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore)

<Note>
  Провайдер активно развивается. Текущая версия поддерживает запросы LINQ (в том числе JOIN, подзапросы и операции над множествами), `INSERT` через `SaveChanges` / `BulkInsertAsync`, миграции с полной поддержкой DDL (CREATE / ALTER / DROP), а также настройку движков таблиц ClickHouse. `UPDATE` / `DELETE` не поддерживаются.
</Note>

<h4 id="ef-core-installation">
  Установка
</h4>

```bash theme={null}
dotnet add package ClickHouse.EntityFrameworkCore
```

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

<h4 id="ef-core-quick-start">
  Быстрый старт
</h4>

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

```csharp theme={null}
using Microsoft.EntityFrameworkCore;

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

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

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

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

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

<h4 id="ef-core-types">
  Поддерживаемые типы
</h4>

| Категория | Типы ClickHouse | Типы CLR |
| - | - | - |
| **Целые числа** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **Большие целые числа** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Числа с плавающей точкой** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Десятичные числа** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` или `ClickHouseDecimal` |
| **Логические значения** | `Bool` | `bool` |
| **Строки** | `String`, `FixedString(N)` | `string` |
| **Перечисления** | `Enum8(...)`, `Enum16(...)` | `string` или `enum` в C# |
| **Дата и время** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Время** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **Сетевые адреса** | `IPv4`, `IPv6` | `IPAddress` |
| **Массивы** | `Array(T)` | `T[]`, `List<T>`, `IList<T>`, `ICollection<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IEnumerable<T>` |
| **Словари** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuple** | `Tuple(T1, ...)` | `Tuple<...>` или `ValueTuple<...>` |
| **Варианты** | `Variant(T1, T2, ...)` | `object` |
| **Динамические типы** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` или `string` |
| **Географические типы** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` и массивы таких значений; `object` для Geometry |
| **Типы-обёртки** | `Nullable(T)`, `LowCardinality(T)` | Обёртки снимаются автоматически |

Если вам нужна полная точность столбцов `Decimal128`/`Decimal256`, используйте `ClickHouseDecimal` (из `ClickHouse.Driver.Numerics`) вместо `decimal`: тип .NET `decimal` поддерживает не более 28–29 значащих цифр.

<h4 id="ef-core-linq">
  Поддерживаемые операции LINQ
</h4>

**Запросы:** `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](#ef-core-join-nulls) ниже).

**Подзапросы:** коррелированные `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.

<h5 id="ef-core-join-nulls">
  Обработка NULL при LEFT JOIN
</h5>

Провайдер автоматически добавляет `set_join_use_nulls=1` при любом способе подключения, чтобы поведение JOIN соответствовало ожиданиям Entity Framework.

Если сервер ClickHouse или профиль запрещает изменять эту настройку (например, профиль с `readonly=1`), отключите её автоматическое добавление:

```csharp theme={null}
optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());
```

Если включён отказ от этой функции, LEFT JOIN возвращает значения по умолчанию для столбцов ClickHouse, поэтому проверка наличия связанных объектов в EF по значениям NULL больше не работает как ожидается. Вместо `== null` явно сравнивайте значения с `0` / `""`.

<h4 id="ef-core-insert">
  Вставка данных
</h4>

`SaveChanges` использует встроенный API драйвера `InsertBinaryAsync`: данные кодируются в формате RowBinary, а тело запроса сжимается. Это гораздо эффективнее параметризованного SQL:

```csharp theme={null}
await using var ctx = new AnalyticsContext();

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

await ctx.SaveChangesAsync();
```

После сохранения сущности переходят из состояния `Added` в `Unchanged`, как и при работе с любым другим провайдером EF Core.

**Размер батча** можно настроить (по умолчанию — 1000):

```csharp theme={null}
optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));
```

<h4 id="ef-core-bulk-insert">
  Массовая вставка
</h4>

При загрузке больших объёмов данных с высокой пропускной способностью используйте `BulkInsertAsync` вместо `SaveChanges`. Этот метод расширения для `DbContext` полностью обходит механизмы отслеживания изменений, сопоставления идентичностей и управления состоянием в EF Core: он напрямую вызывает метод драйвера `InsertBinaryAsync`, используя кодирование RowBinary и сжатое тело запроса.

Поэтому он подходит для загрузки больших наборов данных, если после вставки не требуется отслеживать сущности:

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

long rowsInserted = await ctx.BulkInsertAsync(events);
```

На вход можно передать любой `IEnumerable<T>` — метод обрабатывает объекты по мере поступления, не загружая их все в память. Возвращаемое значение — количество вставленных строк. После вставки объекты **не** прикрепляются к `DbContext`, поэтому перехода из состояния `Added` в `Unchanged` не происходит.

<h4 id="ef-core-enums">
  Перечисления
</h4>

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

```csharp theme={null}
public enum Status { Active, Inactive, Pending }

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

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

<h4 id="ef-core-value-converters">
  Преобразование пользовательских типов
</h4>

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

**Преобразование для отдельного свойства:**

```csharp theme={null}
public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

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

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

**Класс преобразователя для повторного использования:**

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

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

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

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

<h4 id="ef-core-column-types">
  Аннотации типов столбцов
</h4>

Для скалярных типов, таких как `string`, `int`, `DateTime` и т. д., провайдер автоматически определяет тип ClickHouse. Для параметризованных типов и обёрток тип ClickHouse нужно указать явно.

**С помощью атрибутов аннотаций данных:**

```csharp theme={null}
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

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

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

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

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

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

**Использование fluent API в `OnModelCreating`:**

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

Поддерживаются вложенные обёртки, такие как `Array(Nullable(Int32))` и `LowCardinality(Nullable(String))`: провайдер автоматически снимает обёртки `Nullable` и `LowCardinality` на каждом уровне вложенности.

<h4 id="ef-core-variant-dynamic">
  Столбцы типов Variant и Dynamic
</h4>

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

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

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

При чтении значение автоматически десериализуется в тип .NET, соответствующий сохранённому дискриминатору (например, `string`, `ulong`, `ulong[]`).

<h4 id="ef-core-json">
  JSON-столбцы
</h4>

Провайдер поддерживает столбцы ClickHouse типа `Json`: по умолчанию они сопоставляются с `System.Text.Json.Nodes.JsonNode`, а при использовании автоматического `ValueConverter` — со `string`:

```csharp theme={null}
using System.Text.Json.Nodes;

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

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

Читать и записывать JSON можно как с помощью `SaveChanges`, так и с помощью `BulkInsertAsync`:

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

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

Если вы предпочитаете хранить JSON как строки без обработки, настройте для свойства тип `string`, а для столбца — `Json`. Провайдер автоматически применит `ValueConverter`:

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // raw JSON string
}

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

<Note>
  * **Отсутствие преобразования JSON-путей** — выражение `entity.Data["name"]` в LINQ не преобразуется в SQL-синтаксис ClickHouse `data.name`. Фильтруйте данные по столбцам, не содержащим JSON, а JSON обрабатывайте в памяти.
  * **Семантика NULL** — для значений NULL JSON-тип ClickHouse возвращает `{}` (пустой объект), а не SQL NULL.
  * **Точность целых чисел** — ClickHouse хранит все целые числа в JSON как `Int64`. При чтении через `JsonNode` используйте `GetValue<long>()` вместо `GetValue<int>()`.
</Note>

<h4 id="ef-core-engines">
  Движки таблиц
</h4>

Настраивайте движки таблиц ClickHouse и относящиеся к ним секции через fluent API `ToTable(name, t => ...)`. Если движок не задан, провайдер использует `MergeTree`, а секция `ORDER BY` определяется по первичному ключу сущности.

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

Поддерживаемые семейства движков:

| Движок | Метод Fluent API | Примечания |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Используется по умолчанию, если движок не задан |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` или `HasReplacingMergeTreeEngine<T>(e => e.Version)` | Столбцы `Version` и `IsDeleted` необязательны |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` или `HasSummingMergeTreeEngine<T>(e => new { … })` | Столбцы для суммирования указывать необязательно |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` или `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | Столбец `Sign` должен иметь тип `Int8` |
| `VersionedCollapsingMergeTree` | `HasVersionedCollapsingMergeTreeEngine("Sign", "Version")` или `<T>(e => e.Sign, e => e.Version)` | — |
| `GraphiteMergeTree` | `HasGraphiteMergeTreeEngine("config_section")` | — |
| `Log`, `TinyLog`, `StripeLog`, `Memory` | `HasLogEngine()`, `HasTinyLogEngine()`, `HasStripeLogEngine()`, `HasMemoryEngine()` | Без ORDER BY / PARTITION BY |

**Секции движка:** `WithOrderBy`, `WithPartitionBy`, `WithPrimaryKey`, `WithSampleBy`, `WithTtl`, `WithSettings`. Все они применяются к построителю движка, который возвращает `HasXxxEngine()`.

**Возможности на уровне столбца:** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — все поддерживаются при миграциях.

**Индексы пропуска данных** — через `HasIndex(...).HasSkippingIndexType(...)`:

```csharp theme={null}
modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

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

Обычные индексы (не индексы пропуска данных) игнорируются без предупреждения, поскольку в ClickHouse нет их аналогов. При создании уникальных индексов возникает исключение, поскольку ClickHouse не контролирует уникальность значений.

<h4 id="ef-core-migrations">
  Миграции
</h4>

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

```bash theme={null}
dotnet ef migrations add InitialCreate
dotnet ef database update
```

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

| Операция | Что генерируется |
| - | - |
| `CREATE TABLE` | Указание движка, ORDER BY, PARTITION BY, SETTINGS, кодеки столбцов, TTL, комментарии и значения по умолчанию |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Изменение типа, добавление и удаление аннотаций (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | Только индексы пропуска данных |
| `CREATE DATABASE` / `DROP DATABASE` | Через `EnsureCreated` / `EnsureDeleted` и миграции |

<h4 id="ef-core-limitations">
  Ограничения миграций
</h4>

| Возможность | Причина |
| - | - |
| Внешние ключи | ClickHouse не контролирует соблюдение ограничений внешнего ключа. Миграции отклоняют `AddForeignKey`, а валидатор модели выдаёт предупреждение при её построении. |
| Ограничения уникальности / уникальные индексы | ClickHouse не контролирует уникальность. При миграции уникальные индексы вызывают исключение. |
| Значения, генерируемые сервером (автоинкремент / `IDENTITY`) | В ClickHouse нет аналога. |
| Столбцы `Nested(…)` | Пока не поддерживается сопоставление с типом CLR. |
| Зависимые сущности в формате JSON (`.ToJson()`) | Структурное сопоставление JSON для зависимых сущностей пока не реализовано. Вместо этого используйте `JsonNode` / `string` в столбце `Json` (см. [JSON-столбцы](#ef-core-json)). |

Помимо миграций, провайдер пока не поддерживает:

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

<h2 id="limitations">
  Ограничения
</h2>

<h3 id="valuetuple-caveat">
  Tuple с 8 и более элементами и вложенным Tuple в конце
</h3>

Типы C# `ValueTuple` с более чем 7 элементами используют вложенную структуру, создаваемую компилятором: восьмой аргумент обобщённого типа (`TRest`) сам представляет собой `ValueTuple` с оставшимися элементами. Например, `(int, int, int, int, int, int, int, string, string)` компилируется в `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Из-за этого возникает неоднозначность, если столбец ClickHouse имеет тип Tuple с 8 элементами, последний из которых тоже является Tuple, например `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. Драйвер не может различить:

* **Плоский Tuple с 9 элементами** (вложенность TRest, создаваемая компилятором)
* **Tuple с 8 элементами**, последний из которых — вложенный `Tuple(String, String)`

В обоих случаях получается один и тот же тип .NET: `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Драйвер считает восьмой аргумент TRest (то есть разворачивает его), поэтому Tuple с 8 элементами, последний из которых — вложенный Tuple, будет сериализован неверно.

Это касается как `System.Tuple`, так и `ValueTuple`: оба типа используют вложенность TRest при количестве элементов более 7. Tuple с 7 или менее элементами, а также Tuple, у которых последний элемент сам не является Tuple, не затронуты.

**Обходное решение:** Поместите внутренний Tuple в ещё один уровень вложенности, чтобы драйвер мог отличить его от вложенности TRest:

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

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

***

<h3 id="aggregatefunction-columns">
  Столбцы типа AggregateFunction
</h3>

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

Чтобы вставить данные:

```sql theme={null}
INSERT INTO t VALUES (uniqState(1));
```

Для выборки:

```sql theme={null}
SELECT uniqMerge(c) FROM t;
```

***
