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

> 用于连接 ClickHouse 的官方 C# 客户端。

# ClickHouse C# client

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

用于连接 ClickHouse 的官方 C# 客户端。
该客户端的源代码可在 [GitHub 仓库](https://github.com/ClickHouse/clickhouse-cs) 中获取。
最初由 [Oleg V. Kozlyuk](https://github.com/DarkWanderer) 开发。

该库提供两个主要 API：

* **`ClickHouseClient`** (推荐) ：一个高级、线程安全的客户端，适合以单例方式使用。为查询和批量插入提供简洁的异步 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>

该客户端官方支持最近 3 个发行版，以及最近两个 LTS 发行版。

<h2 id="installation">
  安装
</h2>

通过 NuGet 安装该包：

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

或者使用 NuGet 包管理器：

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

<h2 id="quick-start">
  快速入门
</h2>

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

// 创建客户端（通常作为单例使用）
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// 执行查询
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

<h2 id="configuration">
  配置
</h2>

有两种方式可用于配置与 ClickHouse 的连接：

* \*\*连接字符串：\*\*由分号分隔的键/值对，用于指定主机、身份验证凭据和其他连接选项。
* \*\*`ClickHouseClientSettings` object：\*\*强类型配置对象，可从配置文件中加载，也可在代码中设置。

下面列出了所有设置、它们的默认值及其作用。

<h3 id="connection-settings">
  连接设置
</h3>

| 属性 | 类型 | 默认值 | 连接字符串键 | 描述 |
| - | - | - | - | - |
| Host | `string` | `"localhost"` | `Host` | ClickHouse 服务器的主机名或 IP 地址 |
| Port | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | 端口号；默认值取决于协议 |
| Username | `string` | `"default"` | `Username` | 身份验证用户名 |
| Password | `string` | `""` | `Password` | 身份验证密码 |
| Database | `string` | `""` | `Database` | 默认数据库；留空时使用服务器或用户的默认值 |
| Protocol | `string` | `"http"` | `Protocol` | 连接协议：`"http"` 或 `"https"` |
| Path | `string` | `null` | `Path` | 用于反向代理场景的 URL 路径 (例如 `/clickhouse`) |
| Timeout | `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 网络 trace (要求 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_` 前缀。

  有关可用设置的完整列表，请参见[此处](/zh/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`；还会在 URL 上强制设置 `enable_http_compression=1`。参见 [按查询传输压缩](#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` | 应用于插入请求体的 codec (`Content-Encoding`) 。设为 `null` 则不压缩发送。参见 [插入压缩](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | `INSERT INTO ... FORMAT ...` statement 的发送位置：`Body` (位于行数据之前) 或 `Url` (作为 `query` URL 参数) 。参见 [插入查询位置](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | 列名 → ClickHouse 类型字符串。设置后会跳过 schema 探测查询。 |
| UseSchemaCache | `bool` | `false` | 在客户端生命周期内，按 (数据库、表) 缓存完整的表 schema。 |

`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">
  跳过 schema 探测查询
</h4>

默认情况下，`InsertBinaryAsync` 会在每次 insert 之前发送一个 `SELECT ... WHERE 1=0` 查询，以探测列类型。对于高吞吐量场景，你可以通过以下两种方式消除这部分开销：

**选项 1：显式提供列类型**

当你在编译时就已知表的 schema 时，可通过 `ColumnTypes` 直接传入。这样就完全不会发送 schema 查询：

```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：缓存 schema**

当你反复向同一个表插入数据时，可设置 `UseSchemaCache = true`，这样只需查询一次 schema，后续在同一个 `ClickHouseClient` 实例上插入时即可复用：

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

// 第一次调用从服务器拉取 schema
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// 第二次调用复用已缓存的 schema，无需额外往返
await client.InsertBinaryAsync("my_table", columns, batch2, options);
```

<Note>
  * `ColumnTypes` 的优先级高于 `UseSchemaCache`。如果两者都已设置，则使用显式指定的类型。
  * schema 缓存无法检测 `ALTER TABLE` 带来的变更。如果你修改了表的 schema，请创建新的 `ClickHouseClient`，或避免对该表使用 `UseSchemaCache`。
  * 缓存的作用域仅限于 `ClickHouseClient` 实例，并以 (database，table) 为键。同一张表的不同列子集会共享同一个缓存的 schema。
</Note>

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

`ClickHouseClient` 是与 ClickHouse 交互时推荐使用的 API。它是线程安全的，采用单例模式设计，并在内部管理 HTTP 连接池。

<h3 id="creating-a-client">
  创建客户端
</h3>

使用连接字符串或 `ClickHouseClientSettings` object 创建 `ClickHouseClient`。可用选项请参阅[配置](#configuration)部分。

你的 ClickHouse Cloud 服务的详细信息可在 ClickHouse Cloud 控制台中查看。

选择一个服务并点击 **Connect**：

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=d059c1bbcc7317ff8df85b20189e65f4" 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-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=487b14816a8a8711d46ae022d82d74ef" 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` 设计为可长期使用，并可在整个应用程序中共享。只需创建一次 (通常作为单例) ，并在所有数据库操作中重复使用。该客户端会在内部管理 HTTP 连接池。
</Note>

***

<h3 id="executing-queries">
  执行查询
</h3>

对于不返回结果的语句，使用 `ExecuteNonQueryAsync`：

```csharp theme={null}
// 创建一张表
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// 删除一张表
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
```

使用 `ExecuteScalarAsync` 获取单个值：

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

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"服务器版本: {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 too long" 错误。

```csharp theme={null}
// 将数据准备为 IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

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

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

对于较大的数据集，可通过 `InsertOptions` 配置批处理和并行度：

```csharp theme={null}
var options = new InsertOptions
{
    BatchSize = 100_000,           // 每个批次的行数（默认值：100,000）
    MaxDegreeOfParallelism = 4     // 批次并行上传（默认值：1）
};
```

<Note>
  * 客户端会在插入前通过 `SELECT * FROM <table> WHERE 1=0` 自动拉取表结构。提供的值必须与目标列的类型匹配。若要跳过此查询，请使用 [`InsertOptions.ColumnTypes` 或 `InsertOptions.UseSchemaCache`](#skip-schema-query)。
  * 当 `MaxDegreeOfParallelism > 1` 时，批次会并行上传。会话与并行插入不兼容；请禁用会话，或将 `MaxDegreeOfParallelism` 设为 `1`。
  * 如果你希望服务器为未提供的列应用 DEFAULT 值，请在 `InsertOptions.Format` 中使用 `RowBinaryFormat.RowBinaryWithDefaults`。
</Note>

<h4 id="poco-insert">
  POCO 插入
</h4>

无需构造 `object[]` 数组，可直接插入强类型的 POCO 对象。只需注册一次该类型，然后传入 `IEnumerable<T>`：

```csharp theme={null}
// 定义一个与表列匹配的 POCO
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// 注册类型（每个客户端生命周期只需注册一次）
client.RegisterBinaryInsertType<SensorReading>();

// 直接插入——列名从属性名推导而来
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

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

默认情况下，所有公开可读属性都会通过严格区分大小写的名称匹配映射到列。你可以使用特性来自定义映射：

```csharp theme={null}
public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // 映射到不同名称的列
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // 显式指定 ClickHouse 类型
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // 排除在插入操作之外
    public string InternalTag { get; set; }
}
```

| 特性 | 用途 |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | 覆盖目标列名 |
| `[ClickHouseColumn(Type = "...")]` | 显式声明 ClickHouse 类型 |
| `[ClickHouseNotMapped]` | 将该属性排除在插入之外 |

当**所有**映射属性都显式指定了 `Type` 时，会完全跳过 schema 探测查询。只有部分属性显式指定类型时，驱动程序会回退为对完整列集执行 schema 探测查询。

`InsertBinaryAsync<T>` 支持与 `object[]` 重载相同的 `InsertOptions` (批处理、并行度、schema 缓存) 。

<Note>
  与 `object[]` 重载不同，`InsertBinaryAsync<T>` 不接受显式列列表。列由已注册类型的映射属性决定。要控制插入哪些列，可使用 `[ClickHouseNotMapped]` 排除属性，或使用 `[ClickHouseColumn(Name = "...")]` 为属性重命名。

  如果在 `InsertOptions` 中设置了 `ColumnTypes`，它们会覆盖 POCO 特性。
</Note>

<h4 id="poco-insert-schema-evolution">
  schema 演进
</h4>

即使在类型注册完成后向目标表新增列，POCO 插入也能无缝运行。由于 驱动 只会插入由 POCO 映射的列，任何带有 `DEFAULT` (或其他默认表达式) 的新列都会由 server 自动补齐。无需修改代码，也无需重新注册。

<h4 id="insert-query-placement">
  插入查询的位置
</h4>

二进制插入会将 `INSERT INTO ... FORMAT ...` 语句写在请求体的第一行，位于数据行之前。请求体默认经过压缩，因此仅检查 URL 的路由和日志无法看到该语句。将 `InsertOptions.QueryPlacement` 设置为 `InsertQueryPlacement.Url`，即可改为通过 `query` URL 参数发送该语句，请求体中则只保留数据行：

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

当 proxy、load balancer 或 gateway 需要基于 `query` 参数进行路由或检查时，或者当你希望语句出现在访问日志和可观测性工具中时，可以使用该模式。该模式需显式启用，因为语句会计入 URL 长度。实际生效的限制取决于 .NET runtime、中间层和 server 三者中最严格的那个。在 .NET 6 到 .NET 9 上，`System.Uri` 将完整编码后的请求 URI 限制为 65,519 个字符；超出该限制时，驱动会抛出 `InvalidOperationException`，提示你改用 `InsertQueryPlacement.Body`。ClickHouse 的 `http_max_uri_size` 默认为 1 MiB，而中间层可能设置更低的限制。在 请求体 模式下，语句和行不受此 URL 长度限制；但其他请求选项仍可能出现在 URL 中。

该设置与 `Compressor` 相互独立：两种模式下请求体的编码方式相同。

***

<h3 id="reading-data">
  读取数据
</h3>

使用 `ExecuteReaderAsync` 执行 SELECT 查询。返回的 `ClickHouseDataReader` 可通过 `GetInt64()`、`GetString()` 和 `GetFieldValue<T>()` 等方法，以强类型方式访问结果列。

调用 `Read()` 以移动到下一行。没有更多行时，它会返回 `false`。可以按索引 (从 0 开始) 或列名访问列。

```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>()` 会同时设置 `insert` 和读取映射，并在一开始就验证两者。`RegisterBinaryInsertType<T>()` 保持不变，出于向后兼容性的考虑，仍然仅用于 `insert`。

已注册的类型必须满足以下条件：

* 具有一个公开的无参构造函数。
* 至少有一个公开属性，并且该属性具有公开的、非 `init` 的 setter。支持 `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`，或在 .NET 8 及更高版本上使用原生的 `System.Int128`/`System.UInt128` |
| `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(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` (在
[`JsonReadMode=String`](#type-map-reading-json) 下则为 `string`) ，`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}");
}
```

当你必须自行驱动读取器循环时 (例如将原始列访问与 POCO 物化混合使用) ，才使用 `MapTo<T>`。它通过读取器的装箱值来读取行，因此不支持上述可选属性类型，且分配的内存比 `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">
  ADO 风格的 `@name` 占位符
</h4>

驱动程序也接受 `@name` 占位符，Dapper 等 ORM 会生成这种形式。这只是客户端侧的一种便利：在发送请求之前，每个占位符都会被改写为
`{name:ResolvedType}`，因此服务器永远不会看到 `@`。类型的选择方式参见
[类型解析](#parameter-type-mapping)。条件允许时，请优先使用显式的
`{name:Type}` 形式。

若 `@name` 没有匹配的参数，则原样保留，交由服务器拒绝。匹配区分大小写，因此 `@ID` 不会绑定名为 `id` 的参数。

<Note>
  若要关闭这一改写行为，请在首次使用驱动程序之前设置 `ClickHouse.Driver.DisableReplacingParameters` AppContext 开关。此时仅停止文本改写，参数仍会照常发送，因此使用原生 `{name:Type}` 语法编写的查询仍可正常工作。
</Note>

<h4 id="identifier-parameters">
  标识符参数
</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);
```

该值会原样发送，server 会将其作为不加引号的 SQL 标识符替换，并使用自身的反引号引用和转义规则。包含特殊字符 (包括反引号) 的标识符也能安全地往返传输。

***

<h3 id="query-id">
  查询 ID
</h3>

每个查询都会被分配一个唯一的 `query_id`，可用于从 `system.query_log` 表中查询数据，或取消长时间运行的查询。你可以通过 `QueryOptions` 指定自定义的查询 ID：

```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`) ，驱动程序会根据 .NET 值类型自动推断 ClickHouse 类型。例如，`int` 会映射为 `Int32`。

<Warning>
  **推断的 DateTime 参数的行为**

  对于 SQL 中没有 `{name:Type}` 提示且未设置 `ClickHouseType` 的 `@` 风格参数，表示瞬时时间的值会被推断为 `DateTime('UTC')`，而不是不带时区的 `DateTime`。`Kind` 为 `Utc` 或 `Local` 的 `DateTime`，以及所有 `DateTimeOffset` 值，都会作为 `DateTime('UTC')` 发送，从而在任何服务器时区下都能保留其对应的时间点。

  显式提示 (`{name:DateTime}`) 的优先级高于自动推断，并且是构建查询的推荐方式。
</Warning>

如需覆盖这些默认映射，请在 `ClickHouseClientSettings` 上设置 `ParameterTypeResolver`。例如，如果你希望所有 `DateTime` 参数都使用具有毫秒精度的 `DateTime64(3)`，或者希望所有 Decimal 参数都使用特定的标度，而不必为每个参数单独设置 `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. 查询中通过 `{name:Type}` 语法指定的 SQL 类型提示
3. `IParameterTypeResolver` (来自 `QueryOptions.ParameterTypeResolver`，若未设置则回退到 `ClickHouseClientSettings.ParameterTypeResolver`)
4. 内置类型推断 (`TypeConverter.ToClickHouseType`)

该解析器也适用于 ADO.NET 的 `ClickHouseConnection` 路径——由客户端创建的连接会继承这些设置。

***

<h3 id="parameter-value-formatting">
  自定义参数值格式化
</h3>

`IParameterFormatter` 是一个 hook，用于决定参数值如何序列化。当内置格式化 (例如日期时间精度、小数区域设置、字符串转义、数值表示形式) 不符合您的 schema 或下游工具的预期时，请使用它。

在 `ClickHouseClientSettings` 中设置 `ParameterFormatter`，即可为所有参数化查询启用格式化器。该格式化器会接收值、已解析的 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)` 映射会分别格式化 `Array(Int32)` 中的每个 `Int32` 元素。

**复合上下文中的单引号包裹：**

对于嵌入在复合字面量中的类字符串 ClickHouse 类型 (`String`、`FixedString`、`Enum8`、`Enum16`、`IPv4`、`IPv6`、`UUID`) ，驱动程序会用单引号包裹格式化器的输出，但不会对其内容进行转义。如果你返回的字符串中包含未转义的单引号或反斜杠，复合字面量就会格式错误，服务器会拒绝该查询。

顶层字符串参数 (未嵌入复合值中) 会按原样使用，不会额外包裹，因此在这种情况下不需要转义。

**格式化器优先级：**

1. `IParameterFormatter` (来自 `QueryOptions.ParameterFormatter`，若未设置则回退到 `ClickHouseClientSettings.ParameterFormatter`) 。如果它返回非 null，则使用该值。
2. `HttpParameterFormatter` 中内置的类型专用格式化。

格式化器不会用于 `null` 或 `DBNull` 值；这些值始终会被序列化为 ClickHouse 的 null 标记 (`\N`) 。

***

<h3 id="read-value-conversion">
  自定义读取值转换
</h3>

`IReadValueConverter` 允许你在反序列化后对数据读取器返回的值进行转换，而无需更改其 CLR 类型。典型用途包括：为不带时区的 `DateTime` 列设置 `DateTime.Kind = Utc`，修剪或规范化字符串，或在 JSON 列到达应用代码之前对其进行后处理。

在 `ClickHouseClientSettings` 中设置 `ReadValueConverter`，即可为所有读取操作启用转换器。该转换器会通过装箱 (`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);
```

未通过 `For<T>` 注册其运行时 CLR 类型的值会原样返回。分派基于精确的 CLR 类型，因此请注册读取器实际产生的类型 (例如，对于 `JsonReadMode.Binary` 中的 JSON 列，使用 `For<JsonObject>`) 。

**用于高级场景的自定义 `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` 同样会绕过它——参见
[读取枚举的序号](#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`。所有选项请参阅[格式文档](/zh/reference/formats/index)。

***

<h3 id="per-query-accept-encoding">
  按查询设置传输压缩
</h3>

默认情况下，当 `Compression=true` (connection-string 的默认值) 时，client 会协商使用 `zstd, lz4, gzip, deflate`，并自行透明地解码 stream。

对于原始导出 (例如 Parquet、Arrow、Native) ，你可能希望协商使用其他 编解码器 (例如 `zstd` 或 `lz4`) ，以便在不更改整个 connection 设置的情况下，用 CPU 换取 带宽。`QueryOptions.AcceptEncoding` 和 `ClickHouseCommand.AcceptEncoding` 可为单个请求设置 HTTP `Accept-Encoding` 请求头，替换原先附加的默认值，并强制在 URL 上设置 `enable_http_compression=1` (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` 绝不会在你不知情的情况下被剥离，原始响应 body 会原封不动地按 server 发送的样子交到你手中。

<Warning>
  如果你自行提供 `HttpClient`，同样要关闭 `AutomaticDecompression`。它不只是一个响应侧的设置：在发送请求时，handler 会**把其掩码中所有未出现在待发送 `Accept-Encoding` 里的算法统统补上**。因此，带有 `GZip | Deflate` 的 handler 会在线上传输中把显式设置的 `AcceptEncoding = "lz4"` 变成 `lz4, gzip, deflate`，把显式设置的 `"identity"` 变成 `identity, gzip, deflate`；而由于 ClickHouse 是按自身固定的 编解码器 优先级来解析该请求头 (忽略顺序和 q 值) ，它可能返回一个你从未请求过的 编解码器，随后 handler 又将其解码并剥离，让你根本无从察觉。不启用该掩码，才能保证实际发出的内容正是你所选择的。
</Warning>

<Warning>
  如果 `AcceptEncoding` 请求了 驱动程序 无法解码的 编解码器 (`snappy`) ，则只有 `ExecuteRawResultAsync` 是安全的。`ExecuteReaderAsync`、`ExecuteScalarAsync` 和 `ExecuteNonQueryAsync` 会抛出 `NotSupportedException` 并指明该 编解码器 (此前它们会把 compressed bytes 当作结果 format 来解析，从而产生无效数据) 。
</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`——在 client 级别：

```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
```

或者在 connection string 中设置，适用于从不直接使用 `ClickHouseClientSettings` 的 ORM 用户：

```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` 如此声明，无论请求时如何设置，body 都会被解码：该头缺失或为 `identity` 时原样透传，受支持的 codec 会被解码，其他任何值都会引发一个指明该值的 error。不存在重复解码的风险——如果 caller 提供的 handler 的 `AutomaticDecompression` 已经解码了 body，它同时也会移除 `Content-Encoding`，因此 驱动程序 看到的是 plaintext，不会再做处理。

**原始结果不声明任何 编解码器。** `ExecuteRawResultAsync` (以及公开的 `PostStreamAsync` / `InsertRawStreamAsync`) 会原样把 body 交给你，因此除非你自己指定 编解码器，否则它们根本不会请求任何压缩方式——驱动程序 中没有任何环节会解码这样的 body，若在此处提供 编解码器，就会在无声无息中把一次导出变成一个压缩文件。所以规则很简单，且与 `HttpClient` 的配置方式无关：**原样返回的 body 与 server 发送时完全一致，而除非你主动请求 编解码器，否则 server 发送的是 plaintext。** 主动请求 编解码器 (无论是客户端全局级别还是单条 查询 级别) ，正是有意导出 compressed bytes 的方式。

显式设置的 `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();
```

如上所示，请在返回的 stream 离开作用域之前将其读取完毕。当响应*确实*经过压缩时，你拿到的是以 `leaveOpen` 方式创建的 decoder，因此释放它不会影响响应；当响应**未**经过压缩时，你拿到的是 HTTP 内容流本身，释放它会终止 body。无论哪种情况，`ClickHouseRawResult` 都持有该响应——stream 被释放后，请勿再调用它的其他读取成员。释放 `ClickHouseRawResult` 始终是必需的，且仅此一步即已足够：它会同时释放响应以及此处引入的 decoder (decoder 会持有池化的 buffer) 。因此上面的 `await using` 是可选的，保留也无妨。多次顺序调用返回的是同一个 stream；该类型不支持并发使用。

可运行示例请参见 [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Select/Select_007_ResponseCompression.cs)。

<h4 id="insert-compression">
  插入 (请求) 压缩
</h4>

Zstd 是插入操作的默认 编解码器：`InsertOptions.Compressor` 的初始值为 `ZstdCompressor.Default`，即级别 3 的 zstd。将其设置为其他压缩器可更换 编解码器，设置为 `null` 则以未压缩的形式发送 body。

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

驱动程序 中内置了四种 编解码器。每种都提供一个 `Default` instance，以及一个接受压缩级别和 write buffer 大小的构造函数：

| 压缩器 | `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 时便是如此，因为一次 insert 会为每个批次使用一个压缩器。
  它们都没有实现 `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` 以解码响应体；当响应体损坏或格式错误时，应从其返回的 stream 中引发 `InvalidDataException`。

`InsertOptions.Compressor` 仅作用于二进制插入。驱动程序 的其他请求体遵循不同的压缩规则，且都不会经过它：

* **所有 SQL 文本请求** (`ExecuteReaderAsync`、`ExecuteScalarAsync`、`ExecuteNonQueryAsync`、`QueryAsync<T>`、`ExecuteRawResultAsync` 以及 ADO.NET layer) 只要 `UseCompression` 为 `true` (即默认情况) ，都会以 `Content-Encoding: gzip` 发送其语句。此处的 编解码器 不可配置：`AcceptEncoding` 只影响响应，因此要么用 gzip，要么不压缩。设置 `Compression=false` 则以明文发送语句。语句体积很小，通常无需在意这一点——但在 proxy 或抓包中查看请求时，了解这一点会有帮助。
* **multipart 请求体**——即参数以 form data 形式发送的查询 (`UseFormDataParameters=true`) ——始终以未压缩方式发送，无论 `UseCompression` 如何设置。
* **原始上传** (`InsertRawStreamAsync`、`PostStreamAsync`) 使用各自调用级别的标志，既不参考 `UseCompression`，也不参考 `InsertOptions.Compressor`：设置了该标志就使用 gzip，否则不压缩。请注意，`InsertRawStreamAsync` 的 `useCompression` 参数默认为 `true`，因此除非显式传入 `false`，原始上传都会经过 gzip 压缩——即使客户端上设置了 `Compression=false` 也是如此。

***

<h3 id="tuning-compression">
  调优压缩
</h3>

压缩本质上是用 CPU 换取传输字节数。是否划算，几乎完全取决于链路速度与 编解码器 运行速度之间的相对快慢。不存在一种适用于所有场景的设置。

<h4 id="the-one-number-that-decides-it">
  决定性的那个数字
</h4>

只要 编解码器 比网络更快，压缩就值得开启。

在读取路径上，这个 threshold 比大多数人预想的要低，因为 ClickHouse 是在输出 buffer 中以单线程方式压缩
HTTP 响应的。在一台 16 vCPU 的 ClickHouse Cloud 服务上实测 (`hits`、RowBinary、级别 3) ，server 产出压缩数据的速度大约为 100-200MB/s。

因此，对于较大的结果集，并假设同一时刻只处理一个 查询，当带宽超过约 100 MB/s 时，压缩就不再划算。同一云区域内的单条 HTTPS stream
通常能超过这个速度，而任何跨越 public internet、VPN 或区域边界的链路一般都达不到。

插入路径则在更高的链路速度下仍能从压缩中获益，因为客户端会在自己独占的 CPU 核心上完成压缩，通常比 server 端的响应压缩更快。

<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>

| Codec | Ratio | 适用场景 | 注意事项 |
| - | - | - | - |
| `lz4` | 最低 | 高速链路；CPU 比带宽更稀缺。解码成本远低于其他方案，在小结果集上也最快——因此当你不想用默认的 zstd 时，就选它。 | 它**没有熵编码器**，因此在数据分布偏斜但重复度不高 (例如长串数字文本) 的场景下，压缩率明显落后于其他编解码器。它也是提高 `http_zlib_compression_level` 后代价最大的编解码器：级别 1 → 3 会带来约 2.7 倍的 CPU 开销，字节数却只减少约 29%。 |
| `zstd` | 高 | 只要涉及真实网络，它就是通用之选。在关键区间内单位 CPU 的压缩率最佳，级别 3 时字节数、服务器 CPU *和*墙钟时间均优于 `lz4`。 | **解码**开销高于 `lz4`——我们测得级别 3 时为 1.6 倍，不过级别 1 时两者相当——而且 driver 是在你的调用线程上解码的。尤其在 `http_zlib_compression_level=1` 时，它消耗的服务器 CPU 略*高于* `lz4`。 |
| `gzip` | 中等 | 互操作性——代理和 gateway 普遍都能识别。 | 在我们的测量中，它在各个维度上都被 `lz4` 和 `zstd` 全面压制：体积大于 `zstd`，编码 CPU 却高出数倍，解码更是高出 5–9 倍。选它是为了兼容性，而非性能。 |
| `br` | 低级别下最高 | 带宽确实是真正的瓶颈，且你愿意为此付出 CPU。 | 在较高级别下急剧劣化——在 `http_zlib_compression_level=6` 时，我们测得其服务器 CPU 是 `zstd` 的 3–4 倍。默认不会通告它，因为它的优先级会高于默认列表中的所有 fallback 标记。 |

<h4 id="levels">
  级别
</h4>

响应压缩由单个服务器设置 `http_zlib_compression_level` 控制，它对*所有* HTTP 编解码器 生效，而不仅仅是 zlib。默认值为 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");
}
```

若要从 服务器 侧观察同一情况，可从 `system.query_log` 中读回 `ProfileEvents` —— 设置
`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';
```

如果你自己做基准测试，这里有一个陷阱：不带 `ORDER BY` 的裸 `LIMIT n` *每次运行返回的行都不一样*，因此每次重复压缩的数据都不同，得出的比率也就成了噪声。请针对固定的结果集进行比较。

***

<h3 id="raw-stream-insert">
  原始流插入
</h3>

使用 `InsertRawStreamAsync` 可直接从文件流或内存流插入数据，支持 CSV、JSON、Parquet 等任意[受支持的 ClickHouse 格式](/zh/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>
  *driver 会接管该 stream 的所有权。* `InsertRawStreamAsync` 和 `PostStreamAsync` 会在请求结束后释放你传入的
  stream，无论请求成功还是失败。请勿自行释放，也不要在之后重复使用——这正是上面的示例没有把
  `FileStream` 放在 `using` 中的原因。

  你自己写的 `using` 会在 driver 释放该 stream 之后才执行。对于 `FileStream` 或
  `MemoryStream`，这第二次释放是无害的；但如果某个 stream 的 `Dispose` 会归还池化的
  buffer 或递减引用计数，就会导致资源被释放两次。

  所有权只有在 argument 被接受之后才会转移：如果调用因缺少 table、stream 或 format 而 throw `ArgumentException` 或
  `ArgumentNullException`，则该 stream 仍归你所有。
</Warning>

<Note>
  有关控制数据摄取行为的选项，请参阅 [format 设置文档](/zh/reference/settings/formats)。
</Note>

***

<h3 id="more-examples">
  更多示例
</h3>

如需更多实用用法示例，请参阅 GitHub 仓库中的 [examples 目录](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples)。

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

该库通过 `ClickHouseConnection`、`ClickHouseCommand` 和 `ClickHouseDataReader` 提供完整的 ADO.NET 支持。ORM 集成 (Dapper、Linq2db) 以及需要标准 .NET 数据库抽象时，都必须使用此 API。

<h3 id="ado-net-datasource">
  使用 ClickHouseDataSource 管理生命周期
</h3>

**始终通过 `ClickHouseDataSource` 创建连接**，以确保正确管理生命周期并使用连接池。DataSource 在内部维护一个 `ClickHouseClient`，所有连接共享其 HTTP 连接池。

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

// 只创建一次 DataSource（在 DI 中注册为单例）
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// 按需创建轻量连接
await using var connection = await dataSource.OpenConnectionAsync();

// 使用此连接
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
```

使用依赖注入时：

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

// 在你的服务中
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

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

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // 使用连接...
    }
}
```

<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();

// 使用 SQL 创建命令
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// 执行并读取结果
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}
```

命令方法：

* `ExecuteNonQueryAsync()` - 用于 INSERT、UPDATE、DELETE 和 DDL 语句
* `ExecuteScalarAsync()` - 返回第一行第一列的值
* `ExecuteReaderAsync()` - 返回一个 `ClickHouseDataReader`，用于遍历结果

***

<h3 id="ado-net-reader">
  使用 ClickHouseDataReader
</h3>

`ClickHouseDataReader` 提供对查询结果的类型安全访问：

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

while (reader.Read())
{
    // 按列索引访问
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // 按列名访问
    var email = reader.GetString("email");

    // 通用访问方式
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // 检查 null 值
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}
```

<h4 id="ado-net-reader-enum-ordinal">
  读取枚举的序号
</h4>

`Enum8` 或 `Enum16` 列会以其 label 的形式 materialize：`GetFieldType` 返回 `string`，`GetString`、`GetValue` 和 `GetFieldValue<string>` 也都会返回该 label。在枚举列上调用数值类型的 accessor 会抛出 `InvalidCastException`，因为其中存放的值是字符串。

使用 `TryGetEnumOrdinal` 即可获取 label 对应的数字：

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

对于 `Enum8`/`Enum16` 列，以及单元不为 NULL 的 `Nullable(Enum...)` 列，它返回 `true` 并设置 `value`；对于 NULL 单元或任何非枚举列，它返回 `false`，并将 `value` 置为 `0`。该序号是来自 wire 的有符号值，因此可能为负数，且 `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` |
| ADO.NET / ORMs | 使用 `ClickHouseDataSource` (创建共享同一连接池的连接) |
| DI 环境 | 结合 `IHttpClientFactory`，将 `ClickHouseClient` 或 `ClickHouseDataSource` 注册为单例 |

<Warning>
  使用自定义 `HttpClient` 或 `HttpClientFactory` 时，请确保将 `PooledConnectionIdleTimeout` 设置为小于服务器 `keep_alive_timeout` 的值，以避免因连接半关闭而导致错误。Cloud 部署的默认 `keep_alive_timeout` 为 10 秒。
</Warning>

<Warning>
  避免在未共享 `HttpClient` 的情况下创建多个 `ClickHouseClient` 或独立的 `ClickHouseConnection` 实例。每个实例都会创建自己的连接池。
</Warning>

***

<h3 id="best-practice-datetime">
  DateTime 处理
</h3>

1. **尽可能使用 UTC。** 将时间戳存储为 `DateTime('UTC')` 列，并在代码中使用 `DateTimeKind.Utc`。这样可以避免时区歧义。

2. **使用 `DateTimeOffset` 进行明确的时区处理。** 它始终表示某个确定的时间点，并包含偏移信息。

3. **在 SQL 类型提示中指定时区。** 当参数中使用 `Unspecified` 的 DateTime 值，且目标列不是 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>

[异步插入](/zh/concepts/features/operations/insert/asyncinserts) 将批处理的责任从客户端转移到服务器。服务器不再要求客户端进行批处理，而是缓冲传入的数据，并根据可配置的阈值将其刷写到存储中。这对于高并发场景非常有用，例如在可观测性工作负载中，大量 agent 会发送小型载荷。

可通过 `CustomSettings` 或连接字符串启用异步插入：

```csharp theme={null}
// 使用 CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // 推荐：等待 flush 确认

// 或通过 连接字符串
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"
```

**两种模式** (由 `wait_for_async_insert` 控制) ：

| Mode | Behavior | Use case |
| - | - | - |
| `wait_for_async_insert=1` | 插入会在数据写入磁盘后返回。错误也会返回给客户端。 | **推荐**用于大多数工作负载 |
| `wait_for_async_insert=0` | 数据进入缓冲区后，插入会立即返回。不保证数据一定会被持久化。 | 仅适用于可接受数据丢失的场景 |

<Warning>
  使用 `wait_for_async_insert=0` 时，错误只会在刷新期间暴露出来，且无法追溯到原始插入。客户端也不会提供背压，存在服务器过载的风险。
</Warning>

**关键设置：**

| Setting | Description |
| - | - |
| `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 数据类型。下表展示了从数据库读取数据时，ClickHouse 类型与原生 .NET 类型之间的映射。

<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">
  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>
  Decimal 类型的转换由 UseCustomDecimals 设置控制。
</Note>

***

<h4 id="type-map-reading-boolean">
  布尔类型
</h4>

| ClickHouse 类型 | .NET 类型 |
| - | - |
| Bool | `bool` |

***

<h4 id="type-map-reading-strings">
  String 类型
</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](#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 时间戳 (即自纪元以来的秒或亚秒单位) 。虽然存储始终采用 UTC，但列可以关联一个时区，这会影响值的显示和解析方式。

读取 `DateTime` 值时，`DateTime.Kind` 属性会根据列的时区进行设置：

| 列定义 | 返回的 DateTime.Kind | 说明 |
| - | - | - |
| `DateTime('UTC')` | `Utc` | 显式指定 UTC 时区 |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | 已应用时区偏移 |
| `DateTime` | `Unspecified` | 挂钟时间按原样保留 |

对于非 UTC 列，返回的 `DateTime` 表示该时区中的挂钟时间。使用 `ClickHouseDataReader.GetDateTimeOffset()` 可获取带有该时区正确偏移量的 `DateTimeOffset`：

```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')`) ，驱动程序 会返回一个 `Kind=Unspecified` 的 `DateTime`。这样可以原样保留存储的挂钟时间，而不对时区作任何假定。

如果你需要让没有显式时区的列具备时区感知行为，可以：

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、较大精度的 Decimal) 会在 JSON 结构中转换为字符串表示形式。

* **`String`**：以 `string` 形式返回原始 JSON。保留 ClickHouse 中 JSON 的精确表示形式，这在你需要不经解析直接传递 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}
```

若声明为非 Nullable 类型，缺失的路径将取该类型的默认值——`JSON(x String)`
得到 `{"x":""}`，`JSON(x Int64)` 得到 `{"x":0}`。

值为 null 的动态路径会被整个从对象中移除，因此 `ContainsKey` 对其返回
false。从普通 `JSON` 列中读取 `{"x":null}` 得到的是 `{}`。

嵌套的类型化路径会自动构建其父级路径，因此即使是空文档，`JSON(a.b Nullable(Int64))` 也会产生 `{"a":{"b":null}}`。

<Note>
  这是 server 自身渲染的结果，因此 `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>

无论 `ReadStringsAsByteArrays` 设置为何值，`JSON` 列中的字符串叶子节点始终以文本形式返回——`JsonValue` 没有字节数组形式，否则 `byte[]` 会被渲染为 base64。这一点适用于 `String`、`FixedString`，以及被 `LowCardinality`、`Nullable` 或 `SimpleAggregateFunction` 包装的上述类型，同样适用于 `Array` 和 `Map` 中的字符串 (包括 map 的键) 。

<Note>
  JSON reader 无法识别其类型的字节数组仍会渲染为 base64：`Variant` 或 `Dynamic` 类型的 typed path 所持有的值，其类型只能逐行确定，因此 `Variant(Array(UInt8), String)` 下的字符串会以 base64 编码形式返回。两种设置下的行为都是如此。

  如果 JSON map 的键类型不是严格的 `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` 时同样如此，例如从同时带有动态 `a.b` 的行中读取 `JSON(a Map(String, Int64))`。

这仅适用于该行中两侧都有值的情况。若某一侧没有任何内容——为 null、空对象，或子树中的值全为 null——则会让位于有数据的一侧，无论服务端先发送的是这两个路径中的哪一个。因此，用 `Nullable` 类型声明的重叠在每行中只会填充其中一侧，读取时不会报错：`JSON(a Nullable(Int64), a.b Nullable(Int64))` 会如预期得到 `{"a":5}` 和 `{"a":{"b":7}}`。

若使用 `JsonReadMode.String` 读取此类列，可原样获取服务端返回的 JSON 文本，其中包含重复的键。

设置 `AllowDuplicateJsonKeys` 后，该列仍会被读取为 `JsonObject`，而不会抛出异常。此时驱动会保留该行中最后出现的那个值并丢弃另一个，因此结果是有损的：内容为 `{"a.b":7}` 的 `JSON(a Int64, a.b Int64)` 会被读取为 `{"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 type
</h4>

| ClickHouse 类型 | .NET 类型 | Notes |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | 默认 (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | 当 `MapReadMode=KeyValuePairs` 时 |

ClickHouse 的 `Map(K, V)` 在物理上就是 `Array(Tuple(K, V))`，可以包含多个键相同的条目，而 `Dictionary` 做不到这一点。因此在默认模式下，重复的键只保留最后一个值，此前的键值对会被丢弃。`MapReadMode` 设置用于选择采用哪种表示形式：

* **`Dictionary` (默认) **：返回 `Dictionary<K, V>`。

* **`KeyValuePairs`**：按 server 发送键值对的顺序返回 `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"
```

该 mode 决定 `Map` 列的框架类型，因此它同样影响 `GetFieldValue<T>`、驱动程序报告的 schema 类型以及 POCO property 映射。只要 map 出现在某个列的类型树中，该设置就会生效 —— 包括 `Array(Map(...))`、`Map(K, Map(...))`、`Tuple(..., Map(...))` 以及 `Dynamic`。

在写入路径上，两种 mode 下都接受这两种表示形式 —— 参见 [写入 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[][]`；使用 `reader.GetFieldValue<T[,]>(ordinal)` 将矩形数据具体化为多维 CLR 数组) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>`，当 `MapReadMode=KeyValuePairs` 时为 `List<KeyValuePair<K, V>>` — 参见 [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>
  The 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 类型。下表列出了每种 ClickHouse 列类型可接受的 .NET 类型。

<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 位 bfloat 格式 |

***

<h4 id="type-map-writing-boolean">
  布尔类型
</h4>

| ClickHouse 类型 | 可接受的 .NET 类型 | 说明 |
| - | - | - |
| Bool | `bool` | |

***

<h4 id="type-map-writing-strings">
  String 类型
</h4>

| ClickHouse 类型 | 可接受的 .NET 类型 | 说明 |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | 二进制类型会直接写入；流可以支持寻道，也可以不支持寻道 |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | String 会按 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 类型 | 精度取决于 scale 参数 |
| 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` 值会在 `Write` 时抛出 `ArgumentOutOfRangeException`，并指出列类型和支持范围。此前，超出范围的值可能会先经由 32 位整数被静默截断，再由服务器重新解释，从而产生看似真实但实际错误的时间戳。
</Note>

驱动程序在写入值时会遵循 `DateTime.Kind`：

| DateTime.Kind | HTTP 参数 | 批量写入 |
| - | - | - |
| Utc | 保持同一时刻 | 保持同一时刻 |
| Local | 保持同一时刻 | 保持同一时刻 |
| Unspecified | 按参数类型时区中的挂钟时间处理 (默认为 UTC) | 按列时区中的挂钟时间处理 |

`DateTimeOffset` 值始终保留精确时刻。

**示例：UTC DateTime (保留精确时刻)**

```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
```

**示例：未指定 DateTime (挂钟时间) **

```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
```

\*\*建议：\*\*为获得最简单且最可预测的行为，所有 DateTime 操作都使用 `DateTimeKind.Utc` 或 `DateTimeOffset`。这样可以确保你的代码始终保持一致，不受服务器时区、客户端时区或列时区的影响。

<h4 id="datetime-http-param-vs-bulkcopy">
  HTTP 参数与批量复制
</h4>

在写入 `Unspecified` DateTime 值时，HTTP 参数绑定和批量复制之间有一个重要区别：

**批量复制** 知道目标列的时区，因此会按该时区正确解释 `Unspecified` 值。

**HTTP 参数** 不会自动获知列的时区。你必须在 SQL 类型提示中显式指定它：

```csharp theme={null}
// 正确：在 SQL 类型提示中指定时区 - 类型会自动提取
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// 错误：未指定时区提示，将被解释为 UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// 字符串值 "2024-01-15 14:30:00" 被解释为 UTC，而非阿姆斯特丹时间！
```

| `DateTime.Kind` | 目标列 | HTTP 参数 (带 tz 提示) | HTTP 参数 (无 tz 提示) | 批量复制 |
| - | - | - | - | - |
| `Utc` | UTC | 保持同一时刻 | 保持同一时刻 | 保持同一时刻 |
| `Utc` | Europe/Amsterdam | 保持同一时刻 | 保持同一时刻 | 保持同一时刻 |
| `Local` | 任意 | 保持同一时刻 | 保持同一时刻 | 保持同一时刻 |
| `Unspecified` | UTC | 按 UTC 处理 | 按 UTC 处理 | 按 UTC 处理 |
| `Unspecified` | Europe/Amsterdam | 按阿姆斯特丹时间处理 | **按 UTC 处理** | 按阿姆斯特丹时间处理 |

***

<h4 id="type-map-writing-decimal">
  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 类型。数据会在客户端转换为 ClickHouse 的二进制 JSON 格式，并完整支持类型提示。使用前需要调用 `connection.RegisterJsonSerializationType<T>()`。在此模式下写入 `string` 或 `JsonNode` 值会抛出 `ArgumentException`。

```csharp theme={null}
// 默认 String 模式适用于任何输入
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary 模式需要显式启用并注册类型
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

<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>

根据 `JsonWriteMode`，可通过两种方式将 POCO 写入 JSON 列：

**String 模式 (默认) **：POCO 通过 `System.Text.Json.JsonSerializer` 进行序列化。无需注册类型。这是最简单的方法，也适用于匿名对象。

**Binary 模式**：POCO 使用驱动的二进制 JSON 格式进行序列化，并完整支持 类型提示。使用前必须通过 `connection.RegisterJsonSerializationType<T>()` 注册类型。此模式还支持通过特性自定义 path 映射：

* **`[ClickHouseJsonPath("path")]`**：将属性映射到自定义 JSON path。适用于嵌套结构，或属性名与所需的 JSON 键不一致时。**仅在 Binary 模式下有效。**

* **`[ClickHouseJsonIgnore]`**：序列化时排除此属性。**仅在 Binary 模式下有效。**

```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; }  // 不会被序列化
}

// 对于 Binary 模式：注册类型并启用 Binary 模式
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// 插入 POCO - 通过自定义路径属性序列化为具有嵌套结构的 JSON
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// 生成的 JSON：{"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
```

属性名称与列类型提示的匹配是区分大小写的。属性 `UserId` 只会匹配定义为 `UserId` 的提示，不会匹配 `userid`。这与 ClickHouse 的行为一致：它允许 `userName` 和 `UserName` 作为两个不同的字段并存。

**限制 (仅 Binary 模式) ：**

* 在序列化之前，必须通过 `connection.RegisterJsonSerializationType<T>()` 在 connection 上注册 POCO 类型。尝试序列化未注册的类型会抛出 `ClickHouseJsonSerializationException`。
* 字典以及数组/列表属性需要在列定义中提供类型提示，才能正确序列化。没有提示时，请改用 String 模式。
* 只有当该 path 在列定义中具有 `Nullable(T)` 类型提示时，POCO 属性中的 NULL 值才会被写入。ClickHouse 不允许在动态 JSON path 中使用 `Nullable` 类型，因此未提供提示的 null 属性会被跳过。
* 在 String 模式下，`ClickHouseJsonPath` 和 `ClickHouseJsonIgnore` 特性会被忽略 (它们仅在 Binary 模式下生效) 。

***

<h4 id="type-map-writing-other">
  其他类型
</h4>

| ClickHouse 类型 | 可接受的 .NET 类型 | 说明 |
| - | - | - |
| UUID | `Guid`, `string` | `string` 会被解析为 Guid |
| IPv4 | `IPAddress`, `string` | 必须是 IPv4；`string` 通过 `IPAddress.Parse()` 解析 |
| IPv6 | `IPAddress`, `string` | 必须是 IPv6；`string` 通过 `IPAddress.Parse()` 解析 |
| Nothing | Any | 不写入任何内容 (空操作) |
| 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>>` | 在任一读取模式下都接受键值对序列 (例如由 `MapReadMode=KeyValuePairs` 生成的 `List<KeyValuePair<K, V>>`) ，并且允许键重复。适用于二进制插入以及 query parameters |
| Nullable(T) | `null`, `DBNull`, or T 可接受的类型 | 会在值之前写入 null 标志字节 |
| Enum8 | `string`, `sbyte`, 数值类型 | `string` 会在枚举字典中查找 |
| Enum16 | `string`, `short`, 数值类型 | `string` 会在枚举字典中查找 |
| LowCardinality(T) | T 可接受的类型 | 委托给底层类型处理 |
| SimpleAggregateFunction | 底层类型可接受的类型 | 委托给底层类型处理 |
| Nested(...) | tuple 的 `IList` | 元素数量必须与字段数量一致 |
| Variant(T1, T2, ...) | 匹配 T1、T2、... 之一的值 | 如果没有匹配的类型，则抛出 `ArgumentException` |
| QBit(T, dim) | `IList` | 委托给 Array；dimension 仅作为元数据 |

***

<h4 id="type-map-writing-geometry">
  几何类型
</h4>

| ClickHouse 类型 | 可接受的 .NET 类型 | 说明 |
| - | - | - |
| Point | `System.Drawing.Point`、`ITuple`、`IList` (2 个元素) | |
| Ring | 由 Point 组成的 `IList` | |
| LineString | 由 Point 组成的 `IList` | |
| Polygon | 由 Ring 组成的 `IList` | |
| MultiLineString | 由 LineString 组成的 `IList` | |
| MultiPolygon | 由 Polygon 组成的 `IList` | |
| Geometry | 上述任意几何类型 | 所有几何类型的 Variant |

***

<h4 id="type-map-writing-not-supported">
  不支持写入的类型
</h4>

| ClickHouse 类型 | 说明 |
| - | - |
| Dynamic | 会抛出 `NotImplementedException` |
| AggregateFunction | 会抛出 `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  嵌套类型处理
</h3>

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` | 查询执行开始/完成、耗时、查询 ID、服务器统计信息以及错误详情。 |
| `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 handler 配置 (SocketsHttpHandler 或 HttpClientHandler)
* 连接池设置 (MaxConnectionsPerServer、PooledConnectionLifetime 等)
* 超时设置 (ConnectTimeout、Expect100ContinueTimeout 等)
* SSL/TLS 配置
* 连接打开/关闭事件
* 会话 ID 跟踪

<h3 id="logging-debugmode">
  调试模式：网络跟踪与诊断
</h3>

为帮助诊断网络问题，驱动库提供了一个辅助工具，可启用对 .NET 网络内部机制的底层跟踪。要启用该功能，必须传入一个级别设为 Trace 的 LoggerFactory，并将 EnableDebugMode 设置为 true (或者通过 `ClickHouse.Driver.Diagnostic.TraceHelper` 类手动启用) 。事件会记录到 `ClickHouse.Driver.NetTrace` 类别中。警告：这会生成极其详细的日志，并影响性能。不建议在生产环境中启用调试模式。

```csharp theme={null}
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // 必须设置为 Trace 级别才能查看网络事件
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // 启用底层网络追踪
};
```

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

该驱动程序内置了对通过 .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing) API 实现的 OpenTelemetry 分布式链路追踪的支持。启用后，驱动程序会为数据库操作生成 span，并可将其导出到 Jaeger 或 ClickHouse 自身等可观测性后端 (通过 [OpenTelemetry Collector](/zh/guides/use-cases/observability/build-your-own/integrating-opentelemetry)) 。

<h3 id="opentelemetry-enabling">
  启用链路追踪
</h3>

在 ASP.NET Core 应用中，将 ClickHouse 驱动的 `ActivitySource` 添加到 OpenTelemetry 配置中：

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // 订阅 ClickHouse 驱动的 span
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // 或使用 AddJaegerExporter() 等
```

对于控制台应用程序、测试或手动配置：

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

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

<h3 id="opentelemetry-attributes">
  Span 属性
</h3>

每个 span 都包含标准的 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;

// 在 spans 中包含 SQL 语句（出于安全考虑，默认为 false）
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// 截断过长的 SQL 语句（默认值：1000 个字符）
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
```

<Warning>
  启用 `IncludeSqlInActivityTags` 可能会在链路追踪中泄露敏感数据。在 production 环境中使用时请务必谨慎。
</Warning>

<h2 id="tls-configuration">
  TLS 配置
</h2>

通过 HTTPS 连接 ClickHouse 时，您可以通过多种方式配置 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 可能使用您并未请求的 codec 进行响应。参见 [响应解压缩](#response-decompression)。
  * **空闲超时**：将 `PooledConnectionIdleTimeout` 设置为小于服务器的 `keep_alive_timeout` (ClickHouse Cloud 中为 10 秒) ，以避免半开连接导致的连接错误。
</Note>

<h2 id="performance-tuning">
  性能调优
</h2>

本节介绍如何使用该 client 获得最佳性能，以及可以调整哪些选项，使 client 在你的特定用例中发挥更高的性能。

<h3 id="perf-at-a-glance">
  速览
</h3>

\| 若你需要 | 请这样做 |
\|---|---|---|
\| 将行读取为 POCO | 使用 [`QueryAsync<T>`](#perf-read-path)，而非 `MapTo<T>` |
\| 执行大批量插入 | 调大 [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| 运行以插入为主的控制台或 worker 应用 | 启用 [服务器 GC](#perf-gc) |
\| 通过网络读取大结果集 | 保持响应压缩开启 (默认值)  |
\| 在高速链路上执行插入 | 尝试 [`InsertOptions.Compressor = null`](#perf-compression) |
\| 多次向同一张表插入 | 使用 [`UseSchemaCache` 或 `ColumnTypes`](#skip-schema-query) |
\| 读取超大结果集 | 调大 [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  读取：选择物化路径
</h3>

从结果中取出一行有三种方式，开销各不相同。其中某些路径会对结果进行装箱，导致内存分配增加、性能下降。

| 读取方式 | 是否对每个值装箱 | 说明 |
| - | - | - |
| `QueryAsync<T>` | **否** | 直接从 stream 读取到属性中，即快速路径。 |
| 类型化 reader 访问器 (`GetInt32`、`GetInt64`、`GetDouble`、`GetGuid`、`GetDateTime`、`GetFieldValue<T>`) | **否** | 从类型化值存储中进行无装箱读取。 |
| `MapTo<T>` | 是 | 先物化整行，再从中复制出值。 |
| `GetValue` 和 `GetValues` | 是 | 它们返回 `object`，因此取值时必须装箱。 |

对 *hits* 数据集的 105 个列读取 1,000,000 行时：

| 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` 读取的代码 (包括 Dapper 返回的 `dynamic` 结果) 会对每个值进行装箱。如果某个 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 ms | 7,853 ms | −49% |
| `object[]` | 17,027 ms | 10,671 ms | −37% |

如果无法控制批次大小 (例如有许多小型 producer 各自独立发送行) ，请使用[异步插入](#async-inserts)，把攒批交给服务器完成。

**并行上传。** `InsertOptions.MaxDegreeOfParallelism` 默认为 `1`。将其调大即可同时发送多个批次。启用压缩时收益最为明显，因为每个批次会在各自的线程上压缩。session 无法与并行插入同时使用：要么关闭 session，要么保持
`MaxDegreeOfParallelism = 1`。

**去掉 schema 探测。** 每次调用 `InsertBinaryAsync` 都会先发送一个 `SELECT ... WHERE 1=0` 查询，
以确定列类型。参见[跳过 schema 探测查询](#skip-schema-query)，通过 `ColumnTypes` 或 `UseSchemaCache` 省去这次往返。

<Note>
  免装箱的插入路径仅适用于默认的 `RowBinary` 格式。`RowBinaryWithDefaults` 必须
  逐个检查值以查找 `DBDefault` marker，因此仍会走较慢的路径。
</Note>

***

<h3 id="perf-compression">
  压缩：两个方向的结论并不一致
</h3>

压缩本质上是用 CPU 换字节数。这笔交换是否划算，取决于传输的方向、你与 ClickHouse server 之间连接的带宽、数据与所选压缩算法的契合程度，以及是否需要为传输的每个字节付费。

**读取：** 保持压缩开启，除非 server 与客户端运行在同一台机器上。这也是默认行为。与不压缩相比，级别为 1 的 `zstd` 表现如下：

| 客户端到 server | 压缩的效果 |
| - | - |
| 同一主机 (回环) | 多耗 8% |
| 同一云区域 | **节省 16%** |
| 相隔一个区域 | **节省 33%** |

**插入：** 先测量，再决定是否压缩。节省的开销未必足以抵消开启压缩的代价。此外还需注意，解压会给 server 带来额外负载；对于 Zstd 和 LZ4 来说这一负载不大，但对其他算法 (例如 Brotli) 可能相当高。

关闭插入压缩：

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

关于 codec 选择、压缩级别，以及如何找到适合自己场景的交叉点，请参阅[压缩调优](#tuning-compression)。

***

<h3 id="perf-buffers">
  缓冲区
</h3>

`ReadBufferSize` 用于设置读取 HTTP 响应的缓冲区大小，默认值为 64 KiB。

驱动会从共享池中租用该缓冲区，并在释放 reader 时归还，因此不会为每个查询单独分配一次内存。增大该值可以减少处理大结果集时缓冲区重新填充的次数。驱动会为每个同时处于打开状态的 reader 各持有一个缓冲区，因此内存占用会随缓冲区大小以及并发 reader 数量的增加而上升。

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

<Warning>
  *务必释放 reader。* 释放 reader 时，它会把占用的池化 buffer 归还到 pool，并释放其 HTTP connection。
  若直接丢弃 reader，buffer 不会归还到 pool，还可能导致该 HTTP connection 一直处于不可用状态；
  普通的垃圾回收并不能替代显式释放。
</Warning>

***

<h3 id="perf-gc">
  运行时与 GC
</h3>

**对于写入密集型应用，请启用 Server GC。** 在代码相同、分配字节数相同的情况下，Workstation GC 的插入性能最多比 Server GC 慢 97%。

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

ASP.NET Core 项目已经默认设置了这一项，而控制台应用、worker service 以及大多数容器镜像则没有。

原因在于第 0 代的预算大小。Workstation GC 使用的预算较小，因此 insert 产生的短生命周期缓冲区来不及在第 0 代被回收，而是被晋升到第 1 代，晋升量随之上升，进而带来多得多的第 2 代回收开销。在某个 insert 场景中，每 1,000 次操作触发的第 2 代回收次数，Server GC 下为 4,000 次，Workstation GC 下则高达 73,000 次。

<Note>
  Server GC 是一项吞吐量设置，而非延迟设置。在同一组测量中，Server GC 的总暂停时间不到前者的一半，但单次暂停更长 (第 95 百分位为 114.6 毫秒，对比 61.9 毫秒) 。如果你的服务对尾部延迟敏感，请先对两种模式分别测量，再做选择。
</Note>

***

<h3 id="perf-latency">
  延迟：复用连接
</h3>

建立新的 TCP 连接并完成 TLS 握手会耗费大量时间。
复用连接可以显著降低查询延迟。

* 不要为每个请求都创建一个客户端。每个拥有自身 `HttpClient` 的新客户端都会创建新的
  连接池，并再次付出握手开销。在应用程序的整个生命周期中应始终使用同一个 `ClickHouseClient`。它是线程安全的，专为
  单例使用而设计。
* 对于 ADO.NET 和 ORM，请使用 `ClickHouseDataSource`，让所有连接共享同一个连接池。

完整的实践模式请参阅
[连接生命周期与连接池](#best-practices-connection-lifetime)。

***

<h3 id="perf-measuring">
  自行测量
</h3>

在很多情况下，性能取决于数据的形态、与 server 之间链路的速度、你是否愿意用客户端 CPU 换取服务端 CPU (或反过来) 、硬件限制等因素。
因此建议你结合自己的数据和环境自行测量性能。

若要查看 server 端承担的工作量，请设置 `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 需要使用 ADO.NET API (`ClickHouseConnection`) 。为妥善管理连接生命周期，请通过 `ClickHouseDataSource` 创建连接：

```csharp theme={null}
// 以单例方式注册 DataSource
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// 创建供 ORM 使用的连接
await using var connection = await dataSource.OpenConnectionAsync();
// 将连接传递给 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); // 在 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 });
// 或：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; }
}

// 从表中查询
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// 从字面量中查询
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();
```

<h4 id="dapper-clickhouse-param-syntax">
  ClickHouse 原生参数语法
</h4>

当需要显式控制类型时，可直接在 SQL 中使用 ClickHouse 的 `{param:Type}` 语法，并通过 `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>

**Dapper 原生支持 IN 展开：**

```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（适用于 Decimal64/128/256 列）
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger（适用于 Int128/Int256/UInt128/UInt256 列）
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress（适用于 IPv4/IPv6 列）
SqlMapper.AddTypeHandler(new IpAddressHandler());
```

有关类型处理程序实现的示例，请参见 [Dapper 示例](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/ORM/ORM_001_Dapper.cs)。

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

`GetAll<T>()` 和 `Get<T>(id)` 可以正常工作。`Insert<T>()` 不支持——它会生成 SQL Server 语法 (`SCOPE_IDENTITY`、`[]`) 。建议改用 `ClickHouseClient` 原生的 `InsertBinaryAsync` 方法。

```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 会拒绝将复杂类型用作参数值 |
| Geo 类型作为参数 | 不支持 | Point、Ring、Polygon、LineString、MultiLineString、MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | 不支持 | 会生成 SQL Server 专用语法 |
| `Nothing` 类型 | 不支持 | 没有对应的 .NET 有效表示 |

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

此驱动与 [linq2db](https://github.com/linq2db/linq2db) 兼容；后者是适用于 .NET 的轻量级 ORM 和 LINQ 提供商。详细文档请参见项目网站。

**示例用法：**

使用 ClickHouse 提供商创建 `DataConnection`：

```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);
```

表映射可以通过特性或 Fluent 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>

ClickHouse 官方的 Entity Framework Core 提供商。可将 C# 类映射到 ClickHouse 表，使用 LINQ 进行查询，并通过 `SaveChanges` 插入数据——全部采用熟悉的 EF Core 模式。

* **NuGet**: [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **Source**: [GitHub](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore)

<Note>
  该提供商仍在积极开发中。当前版本支持 LINQ 查询 (包括 JOIN、子查询和集合运算) 、通过 `SaveChanges` / `BulkInsertAsync` 执行 `INSERT`、支持完整 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");
}

// 查询
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** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` 或 `ClickHouseDecimal` |
| **Bool** | `Bool` | `bool` |
| **String** | `String`, `FixedString(N)` | `string` |
| **枚举** | `Enum8(...)`, `Enum16(...)` | `string` 或 C# `enum` |
| **日期/时间** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Time** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **Network** | `IPv4`, `IPv6` | `IPAddress` |
| **数组** | `Array(T)` | `T[]`, `List<T>`, `IList<T>`, `ICollection<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IEnumerable<T>` |
| **Map** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuple** | `Tuple(T1, ...)` | `Tuple<...>` 或 `ValueTuple<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **动态** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` 或 `string` |
| **地理空间** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` 及其数组；`Geometry` 使用 `object` |
| **包装类型** | `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` (在 `.GroupBy()` 之后调用 `.Where()`) 、在单个投影中使用多个聚合，以及按聚合结果执行 `OrderBy`。

**JOIN：** `Join` (INNER) 、`GroupJoin`/`SelectMany` 模式 (LEFT 和 CROSS) 。对于不匹配的行，LEFT JOIN 会返回实际的 `null` (参见下方的 [LEFT JOIN null 语义](#ef-core-join-nulls)) 。

**子查询：** 关联 `Contains` / `IN`、`Any` / `EXISTS`、`All`，以及投影中的标量子查询。

**集合操作：** `Concat` (→ `UNION ALL`) 、`Union` (→ `UNION DISTINCT`) 、`Intersect`、`Except`。

**内联本地集合：** 针对内存中集合 (`int[]`、`List<T>` 等) 的联接和 `Contains` 会被转换为一系列 UNION。

**字符串方法：** `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`/`TrimStart`/`TrimEnd`, `ToLower`, `ToUpper`, `Length`, `IsNullOrEmpty`, `Concat` (以及 `+` 运算符) 。

**数学函数：** 标准 `Math` 和 `MathF` 方法会被转换为对应的 ClickHouse 函数 —— 包括算术、对数、三角和实用函数。

<h5 id="ef-core-join-nulls">
  LEFT JOIN 的 NULL 语义
</h5>

该提供程序会自动在每条连接路径中注入 `set_join_use_nulls=1`，以使 JOIN 行为符合 Entity Framework 的预期。

如果你的 ClickHouse 服务器或 profile 禁止更改此设置 (例如 `readonly=1` profile) ，可通过以下方式禁用：

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

启用 opt-out 后，LEFT JOIN 会返回 ClickHouse 列的默认值，EF 基于 null 的导航属性检测将不再按预期工作。请显式与 `0` / `""` 比较，不要使用 `== null`。

<h4 id="ef-core-insert">
  插入数据
</h4>

`SaveChanges` 使用驱动程序提供的原生 `InsertBinaryAsync` API——采用 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` 属性或 C# `enum` 类型。使用 C# 枚举时，提供商会自动在枚举值及其字符串表示形式之间进行转换：

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

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

// 使用枚举值查询
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();
```

<h4 id="ef-core-value-converters">
  自定义类型转换
</h4>

EF Core 的 `ValueConverter` 系统允许你将自定义类型映射到提供商已支持的类型。提供商不会直接看到你的自定义类型——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; }
}

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

// 应用于单个属性：
.HasConversion<MoneyConverter>()

// 或通过约定应用到某一类型的所有属性：
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}
```

<h4 id="ef-core-column-types">
  列类型注解
</h4>

对于 `string`、`int`、`DateTime` 等标量类型，提供商会自动推断出 ClickHouse 类型。对于参数化类型和包装类型，则需要显式指定 ClickHouse 类型。

**使用数据注解 (attribute) ：**

```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; }
}
```

**在 `OnModelCreating` 中使用 Fluent API：**

```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` 列在 .NET 中会映射为 `object`。由于 `object` 过于宽泛，无法自动推断类型，因此必须通过 `.HasColumnType()` 显式声明存储类型：

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

// 在 OnModelCreating 中：
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// 或者：
entity.Property(e => e.Payload).HasColumnType("Dynamic");
```

读取时，该值会根据存储的判别器自动反序列化为相应的 .NET 类型 (例如 `string`、`ulong`、`ulong[]`) 。

<h4 id="ef-core-json">
  JSON 列
</h4>

该提供程序支持 ClickHouse 的 `Json` 列类型，可映射到 `System.Text.Json.Nodes.JsonNode` (主要) 或 `string` (通过自动 `ValueConverter`) ：

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

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

// 在 OnModelCreating 中：
entity.Property(e => e.Data).HasColumnType("Json");
```

JSON 的读取和写入既可通过 `SaveChanges`，也可通过 `BulkInsertAsync` 完成：

```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; }  // 原始 JSON 字符串
}

entity.Property(e => e.Data).HasColumnType("Json");
```

<Note>
  * **不支持 JSON 路径转换** — LINQ 中的 `entity.Data["name"]` 不会转换为 ClickHouse 的 `data.name` SQL 语法。请对非 JSON 列进行过滤，并在内存中检查 JSON 内容。
  * **NULL 语义** — 对于 NULL 值，ClickHouse 的 JSON 类型返回的是 `{}` (空对象) ，而不是 SQL NULL。
  * **整数精度** — ClickHouse JSON 会将所有整数存储为 `Int64`。通过 `JsonNode` 读取时，应使用 `GetValue<long>()`，而不是 `GetValue<int>()`。
</Note>

<h4 id="ef-core-engines">
  表引擎
</h4>

通过 `ToTable(name, t => ...)` 流式 API 配置 ClickHouse 表引擎及引擎特定子句。若未配置引擎，提供商默认使用 `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"));
});
```

支持的引擎系列：

| Engine | 流式方法 | 说明 |
| - | - | - |
| `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);

// 带参数的索引（如 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
```

支持的操作：

| Operation | Emits |
| - | - |
| `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 映射。请改为在 `Json` 列上使用 `JsonNode` / `string` (参见 [JSON 列](#ef-core-json)) 。 |

除迁移外，该提供商目前还不支持：

* **`UPDATE` / `DELETE`**
* **事务**：`BeginTransaction` 是空操作。ClickHouse 不支持 ACID 事务。
* **JSON 路径查询转换**：LINQ 中的 `entity.Data["key"]` 不会转换为 ClickHouse 的 `data.key` SQL 语法。请对非 JSON 列进行过滤，并在内存中检查 JSON。

<h2 id="limitations">
  局限性
</h2>

<h3 id="valuetuple-caveat">
  具有 8 个以上元素且最后一位为嵌套 Tuple 的元组
</h3>

元素超过 7 个的 C# `ValueTuple` 类型会采用编译器生成的嵌套方案：第 8 个泛型参数 (`TRest`) 本身也是一个 `ValueTuple`，用于承载其余元素。例如，`(int, int, int, int, int, int, int, string, string)` 会编译为 `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`。

这会在 ClickHouse 列为一个 8 元素元组且最后一个元素本身也是元组时产生歧义——例如，`Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`。驱动程序无法区分以下两种情况：

* **扁平的 9 元素元组** (编译器生成的 TRest 嵌套)
* **8 元素元组**，其中最后一个元素是嵌套的 `Tuple(String, String)`

这两种情况都会生成相同的 .NET 类型：`ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`。

驱动程序会将第 8 个参数视为 TRest (即将其展平) ，这意味着“8 元素且最后一位为嵌套元组”的情况会被错误地序列化。

这同时会影响 `System.Tuple` 和 `ValueTuple`，因为两者在元素数量 >7 时都会使用 TRest 嵌套。元素不超过 7 个的元组，或最后一个元素本身不是元组的元组，则不受影响。

**解决方法：** 在内部元组外再包一层，这样驱动程序就能将其与 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;
```

***
