Skip to main content
用于连接 ClickHouse 的官方 C# 客户端。 该客户端的源代码可在 GitHub 仓库 中获取。 最初由 Oleg V. Kozlyuk 开发。 该库提供两个主要 API:
  • ClickHouseClient (推荐) :一个高级、线程安全的客户端,适合以单例方式使用。为查询和批量插入提供简洁的异步 API。最适合大多数应用程序。
  • ADO.NET (ClickHouseDataSource、ClickHouseConnection、ClickHouseCommand) :标准的 .NET 数据库抽象。ORM 集成 (Dapper、Linq2db) 以及需要 ADO.NET 兼容性时必须使用。ClickHouseBulkCopy 是一个辅助类,用于通过 ADO.NET 连接高效插入数据。ClickHouseBulkCopy 已弃用,并将在未来的版本中移除;请改用 ClickHouseClient.InsertBinaryAsync。
这两种 API 共享同一个底层 HTTP 连接池,并且可以在同一个应用程序中同时使用。

迁移指南

  1. 将 .csproj 文件中的包名更新为 ClickHouse.Driver,并使用 NuGet 上的最新版本。
  2. 将代码库中所有对 ClickHouse.Client 的引用替换为 ClickHouse.Driver。

支持的 .NET 版本

ClickHouse.Driver 支持以下 .NET 版本:
  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

支持的 ClickHouse 版本

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

安装

通过 NuGet 安装该包:
或者使用 NuGet 包管理器:

快速入门

配置

有两种方式可用于配置与 ClickHouse 的连接:
  • **连接字符串:**由分号分隔的键/值对,用于指定主机、身份验证凭据和其他连接选项。
  • **ClickHouseClientSettings object:**强类型配置对象,可从配置文件中加载,也可在代码中设置。
下面列出了所有设置、它们的默认值及其作用。

连接设置

数据格式与序列化

会话管理

UseSession 标志会启用服务器会话持久化,从而可以使用 SET 语句和临时表。会话在 60 秒无活动后会被重置 (默认超时) 。可通过 ClickHouse 语句或服务器配置中的会话设置来延长会话生命周期。ClickHouseConnection 类通常支持并行操作 (多个线程可并发运行查询) 。但启用 UseSession 标志后,任意时刻每个连接只允许有一个活动查询 (这是服务器端限制) 。

安全

HTTP 客户端配置

日志与调试

自定义设置与角色

使用连接字符串设置自定义设置时,请使用 set_ 前缀,例如 set_max_threads=4。使用 ClickHouseClientSettings 对象时,不要使用 set_ 前缀。有关可用设置的完整列表,请参见此处。

连接字符串示例

基本连接

使用自定义 ClickHouse 设置


QueryOptions

QueryOptions 允许你按查询覆盖客户端级别的设置。所有属性均为可选,只有在指定时才会覆盖客户端默认值。 示例:

InsertOptions

InsertOptions 在 QueryOptions 的基础上增加了通过 InsertBinaryAsync 执行批量插入操作所需的特定设置。 QueryOptions 的所有属性也可用于 InsertOptions。 示例:

跳过 schema 探测查询

默认情况下,InsertBinaryAsync 会在每次 insert 之前发送一个 SELECT ... WHERE 1=0 查询,以探测列类型。对于高吞吐量场景,你可以通过以下两种方式消除这部分开销: 选项 1:显式提供列类型 当你在编译时就已知表的 schema 时,可通过 ColumnTypes 直接传入。这样就完全不会发送 schema 查询:
选项 2:缓存 schema 当你反复向同一个表插入数据时,可设置 UseSchemaCache = true,这样只需查询一次 schema,后续在同一个 ClickHouseClient 实例上插入时即可复用:
  • ColumnTypes 的优先级高于 UseSchemaCache。如果两者都已设置,则使用显式指定的类型。
  • schema 缓存无法检测 ALTER TABLE 带来的变更。如果你修改了表的 schema,请创建新的 ClickHouseClient,或避免对该表使用 UseSchemaCache。
  • 缓存的作用域仅限于 ClickHouseClient 实例,并以 (database,table) 为键。同一张表的不同列子集会共享同一个缓存的 schema。

ClickHouseClient

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

创建客户端

使用连接字符串或 ClickHouseClientSettings object 创建 ClickHouseClient。可用选项请参阅配置部分。 你的 ClickHouse Cloud 服务的详细信息可在 ClickHouse Cloud 控制台中查看。 选择一个服务并点击 Connect: 选择 C#。连接详细信息会显示在下方。 如果你使用的是自管理 ClickHouse,连接详细信息由你的 ClickHouse 管理员提供。 使用连接字符串:
或者使用 ClickHouseClientSettings:
对于依赖注入的场景,请使用 IHttpClientFactory:
ClickHouseClient 设计为可长期使用,并可在整个应用程序中共享。只需创建一次 (通常作为单例) ,并在所有数据库操作中重复使用。该客户端会在内部管理 HTTP 连接池。

执行查询

对于不返回结果的语句,使用 ExecuteNonQueryAsync:
使用 ExecuteScalarAsync 获取单个值:

插入数据

参数化插入

使用 ExecuteNonQueryAsync 通过参数化查询插入数据。必须在 SQL 中使用 {name:Type} 语法来指定参数类型:

批量插入

使用 InsertBinaryAsync 可高效插入大量行。它使用 ClickHouse 原生的行二进制格式以流式方式传输数据,支持并行批次上传,并可避免参数化查询可能导致的 “URL too long” 错误。
对于较大的数据集,可通过 InsertOptions 配置批处理和并行度:
  • 客户端会在插入前通过 SELECT * FROM <table> WHERE 1=0 自动拉取表结构。提供的值必须与目标列的类型匹配。若要跳过此查询,请使用 InsertOptions.ColumnTypes 或 InsertOptions.UseSchemaCache。
  • 当 MaxDegreeOfParallelism > 1 时,批次会并行上传。会话与并行插入不兼容;请禁用会话,或将 MaxDegreeOfParallelism 设为 1。
  • 如果你希望服务器为未提供的列应用 DEFAULT 值,请在 InsertOptions.Format 中使用 RowBinaryFormat.RowBinaryWithDefaults。

POCO 插入

无需构造 object[] 数组,可直接插入强类型的 POCO 对象。只需注册一次该类型,然后传入 IEnumerable<T>:
默认情况下,所有公开可读属性都会通过严格区分大小写的名称匹配映射到列。你可以使用特性来自定义映射:
当所有映射属性都显式指定了 Type 时,会完全跳过 schema 探测查询。只有部分属性显式指定类型时,驱动程序会回退为对完整列集执行 schema 探测查询。 InsertBinaryAsync<T> 支持与 object[] 重载相同的 InsertOptions (批处理、并行度、schema 缓存) 。
与 object[] 重载不同,InsertBinaryAsync<T> 不接受显式列列表。列由已注册类型的映射属性决定。要控制插入哪些列,可使用 [ClickHouseNotMapped] 排除属性,或使用 [ClickHouseColumn(Name = "...")] 为属性重命名。如果在 InsertOptions 中设置了 ColumnTypes,它们会覆盖 POCO 特性。

schema 演进

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

插入查询的位置

二进制插入会将 INSERT INTO ... FORMAT ... 语句写在请求体的第一行,位于数据行之前。请求体默认经过压缩,因此仅检查 URL 的路由和日志无法看到该语句。将 InsertOptions.QueryPlacement 设置为 InsertQueryPlacement.Url,即可改为通过 query URL 参数发送该语句,请求体中则只保留数据行:
当 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 相互独立:两种模式下请求体的编码方式相同。

读取数据

使用 ExecuteReaderAsync 执行 SELECT 查询。返回的 ClickHouseDataReader 可通过 GetInt64()、GetString() 和 GetFieldValue<T>() 等方法,以强类型方式访问结果列。 调用 Read() 以移动到下一行。没有更多行时,它会返回 false。可以按索引 (从 0 开始) 或列名访问列。

POCO 读取

无需按索引或名称读取列,你可以将查询结果直接流式写入自定义类。只需向客户端注册一次该类型,然后使用 QueryAsync<T>:
RegisterPocoType<T>() 会同时设置 insert 和读取映射,并在一开始就验证两者。RegisterBinaryInsertType<T>() 保持不变,出于向后兼容性的考虑,仍然仅用于 insert。 已注册的类型必须满足以下条件:
  • 具有一个公开的无参构造函数。
  • 至少有一个公开属性,并且该属性具有公开的、非 init 的 setter。支持 required 属性。
列匹配区分大小写。缺失的结果列会使属性保持默认值;多出的结果列会被忽略。 驱动不会对值进行放宽或收窄转换。除下文列出的替代表示形式外, 列的框架类型必须可赋值给属性类型,不匹配时会抛出 InvalidOperationException。因此,object 类型的属性可以接受任意列。 QueryAsync<T> 会将下列各类列直接读取到与之匹配的属性中: 无论该列是否为 Nullable(...),上述每一行同样接受其属性类型的可空形式 (long?、DateOnly? 等) 。 在 Nullable(T) 列上使用不可空的值类型属性可以通过注册,但读取到 NULL 时会抛出异常。 LowCardinality(T)、SimpleAggregateFunction(f, T) 和 Object(T) 这类包装类型的映射方式与 T 完全相同。 复合列同样受支持,其映射采用读取类型参考中给出的框架类型:Array(T) 映射为 T[],Tuple(...) 映射为 System.Tuple<...>,Nested(...) 映射为 Tuple<...>[],JSON 映射为 JsonObject (在 JsonReadMode=String 下则为 string) ,Variant/Dynamic 映射为 object。 Map(K, V) 列属于特例:List<KeyValuePair<K, V>> 或 KeyValuePair<K, V>[] 属性会走免装箱路径读取,并且在两种 MapReadMode 下都会保留线上传输顺序以及重复出现的键。Dictionary<K, V> 属性仅在默认模式下可用。键和值的类型必须完全匹配,因此 Map(String, Nullable(Int32)) 需要 KeyValuePair<string, int?>。 当某一列可对应多种属性类型时 (DateTime 列可为 DateTime、 DateTimeOffset 或 DateOnly,String 列可为 string 或 byte[]) ,最终采用哪种表示形式由声明的属性类型决定。这些可选的表示形式 仅存在于 POCO 路径,因此 QueryAsync<T> 支持它们,而 MapTo<T> 不支持。 手动迭代读取器时,可使用 ClickHouseDataReader.MapTo<T>() 将当前行物化为已注册的 POCO,且不会将读取器向前推进:
当你必须自行驱动读取器循环时 (例如将原始列访问与 POCO 物化混合使用) ,才使用 MapTo<T>。它通过读取器的装箱值来读取行,因此不支持上述可选属性类型,且分配的内存比 QueryAsync<T> 更多。如果只需要获取行,请优先使用 QueryAsync<T>;具体数据参见 选择物化路径。 客户端级别或按查询设置的读取值转换器对两条路径都生效,且不会禁用免装箱读取。驱动会根据读取该列的方式,在对应的重载上完成转换:免装箱列使用带类型的 ConvertValue<T>,复合列则使用装箱的 ConvertValue。请确保这两个重载的实现保持一致,否则同一列在不同路径上会得到不同的结果。 配置了 LoggerFactory 后,RegisterPocoType<T>() 和 RegisterBinaryInsertType<T>() 会输出一条 Debug 级别的日志 (类别为 ClickHouse.Driver.Client) ,列出哪些属性映射到了哪些列,以及哪些属性被跳过和跳过原因。请参阅日志和诊断。

SQL 参数

在 ClickHouse 中,SQL 查询中的查询参数标准格式为 {parameter_name:DataType}。 示例:
SQL ‘绑定’参数通过 HTTP URI 查询参数传递,因此如果使用过多,可能会导致出现 “URL 过长” 异常。为避免这一限制,在批量插入数据时请使用 InsertBinaryAsync。

ADO 风格的 @name 占位符

驱动程序也接受 @name 占位符,Dapper 等 ORM 会生成这种形式。这只是客户端侧的一种便利:在发送请求之前,每个占位符都会被改写为 {name:ResolvedType},因此服务器永远不会看到 @。类型的选择方式参见 类型解析。条件允许时,请优先使用显式的 {name:Type} 形式。 若 @name 没有匹配的参数,则原样保留,交由服务器拒绝。匹配区分大小写,因此 @ID 不会绑定名为 id 的参数。
若要关闭这一改写行为,请在首次使用驱动程序之前设置 ClickHouse.Driver.DisableReplacingParameters AppContext 开关。此时仅停止文本改写,参数仍会照常发送,因此使用原生 {name:Type} 语法编写的查询仍可正常工作。

标识符参数

Identifier 参数类型允许你安全地绑定数据库、表或列名,而不是使用带引号的字符串字面量。可在 SQL 中通过 {name:Identifier} 语法使用,或通过设置 ClickHouseDbParameter.ClickHouseType = "Identifier" 来使用:
该值会原样发送,server 会将其作为不加引号的 SQL 标识符替换,并使用自身的反引号引用和转义规则。包含特殊字符 (包括反引号) 的标识符也能安全地往返传输。

查询 ID

每个查询都会被分配一个唯一的 query_id,可用于从 system.query_log 表中查询数据,或取消长时间运行的查询。你可以通过 QueryOptions 指定自定义的查询 ID:
如果要指定自定义 QueryId,请确保每次调用使用的值都是唯一的。随机生成的 GUID 是不错的选择。

自定义参数类型映射

使用 @ 风格的参数时 (例如 WHERE id = @id) ,驱动程序会根据 .NET 值类型自动推断 ClickHouse 类型。例如,int 会映射为 Int32。
推断的 DateTime 参数的行为对于 SQL 中没有 {name:Type} 提示且未设置 ClickHouseType 的 @ 风格参数,表示瞬时时间的值会被推断为 DateTime('UTC'),而不是不带时区的 DateTime。Kind 为 Utc 或 Local 的 DateTime,以及所有 DateTimeOffset 值,都会作为 DateTime('UTC') 发送,从而在任何服务器时区下都能保留其对应的时间点。显式提示 ({name:DateTime}) 的优先级高于自动推断,并且是构建查询的推荐方式。
如需覆盖这些默认映射,请在 ClickHouseClientSettings 上设置 ParameterTypeResolver。例如,如果你希望所有 DateTime 参数都使用具有毫秒精度的 DateTime64(3),或者希望所有 Decimal 参数都使用特定的标度,而不必为每个参数单独设置 ClickHouseType,这会很有用。 使用 DictionaryParameterTypeResolver 进行简单的类型映射:
用于高级场景的自定义 IParameterTypeResolver: 对于按值或按名称进行解析的场景,请直接实现 IParameterTypeResolver 接口。返回 null 以回退到默认推断:
你也可以通过 QueryOptions.ParameterTypeResolver 为单个查询设置解析器。设置后,它会优先于客户端级别的解析器。 类型解析优先级: 解析器只是这一优先级事件链中的一环。按优先级从高到低依次为:
  1. 在参数上显式设置的 ClickHouseType
  2. 查询中通过 {name:Type} 语法指定的 SQL 类型提示
  3. IParameterTypeResolver (来自 QueryOptions.ParameterTypeResolver,若未设置则回退到 ClickHouseClientSettings.ParameterTypeResolver)
  4. 内置类型推断 (TypeConverter.ToClickHouseType)
该解析器也适用于 ADO.NET 的 ClickHouseConnection 路径——由客户端创建的连接会继承这些设置。

自定义参数值格式化

IParameterFormatter 是一个 hook,用于决定参数值如何序列化。当内置格式化 (例如日期时间精度、小数区域设置、字符串转义、数值表示形式) 不符合您的 schema 或下游工具的预期时,请使用它。 在 ClickHouseClientSettings 中设置 ParameterFormatter,即可为所有参数化查询启用格式化器。该格式化器会接收值、已解析的 ClickHouse 类型名称以及参数名,并返回发送到服务器的字符串表示形式。返回 null 则会回退到默认格式化器。 使用 DictionaryParameterFormatter 进行简单的按 CLR 类型格式化:
面向高级场景的自定义 IParameterFormatter:
你也可以通过 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) 。

自定义读取值转换

IReadValueConverter 允许你在反序列化后对数据读取器返回的值进行转换,而无需更改其 CLR 类型。典型用途包括:为不带时区的 DateTime 列设置 DateTime.Kind = Utc,修剪或规范化字符串,或在 JSON 列到达应用代码之前对其进行后处理。 在 ClickHouseClientSettings 中设置 ReadValueConverter,即可为所有读取操作启用转换器。该转换器会通过装箱 (GetValue) 和泛型 (GetFieldValue<T>) 两种路径,对每一行的每一列各调用一次。未设置转换器时,没有任何额外开销——读取器会直接返回值。 使用 DictionaryReadValueConverter 进行简单的按 CLR 类型转换:
未通过 For<T> 注册其运行时 CLR 类型的值会原样返回。分派基于精确的 CLR 类型,因此请注册读取器实际产生的类型 (例如,对于 JsonReadMode.Binary 中的 JSON 列,使用 For<JsonObject>) 。 用于高级场景的自定义 IReadValueConverter: 如果你需要根据 ClickHouse 侧的类型字符串进行分派 (例如,区分 DateTime 和 DateTime('UTC')——两者在 CLR 中都会显示为相同的类型) ,请直接实现 IReadValueConverter:
转换器必须保留运行时 CLR 类型;列元数据 (GetFieldType、GetSchemaTable) 不会通过它改写,且必须与实际返回的内容保持一致。 你也可以通过 QueryOptions.ReadValueConverter 为每个查询设置转换器;设置后,它的优先级高于客户端级别的转换器。 分派边界: 转换器对每一列只会以整个反序列化后的单元格值调用一次,不会递归处理复合容器。对于 Array(Int32) 列,传入的值是 int[];对于 Tuple(Int32, String),则是 ITuple。 哪个重载会被执行: 两个重载的行为必须保持一致,因为驱动调用哪一个取决于调用方读取该列的方式:
  • ConvertValue<T> —— 类型化访问器 GetByte、GetSByte、GetInt16/32/64、 GetUInt16/32/64、GetFloat、GetDouble、GetGuid、GetDateTime、GetIPAddress、 GetBigInteger 和 GetFieldValue<T>,以及 POCO 读取路径上的每一个免装箱列。
  • ConvertValue (装箱) —— GetValue、GetValues、索引器、GetChar、GetTuple,以及 GetBoolean、GetDecimal 和 GetString 中的强制转换路径。
IsDBNull 完全不会运行转换器:它直接读取 null 标志,因此转换器永远无法 改变某个值是否被视为 null。TryGetEnumOrdinal 同样会绕过它——参见 读取枚举的序号。 该转换器适用于 ADO.NET ClickHouseConnection 路径——从客户端创建的连接会继承这些设置。

原始流式传输

使用 ExecuteRawResultAsync 可按特定 格式 直接流式传输查询结果,绕过数据读取器。这对于将数据导出到文件或传输到其他系统特别有用:
常见格式:JSONEachRow、CSV、TSV、Parquet、Native。所有选项请参阅格式文档。

按查询设置传输压缩

默认情况下,当 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) 。

HttpClient 配置

无需任何配置:驱动程序 构建的 HttpClient 会将 AutomaticDecompression 保持为 DecompressionMethods.None,并由 驱动程序 自行解码响应,因此 Content-Encoding 绝不会在你不知情的情况下被剥离,原始响应 body 会原封不动地按 server 发送的样子交到你手中。
如果你自行提供 HttpClient,同样要关闭 AutomaticDecompression。它不只是一个响应侧的设置:在发送请求时,handler 会把其掩码中所有未出现在待发送 Accept-Encoding 里的算法统统补上。因此,带有 GZip | Deflate 的 handler 会在线上传输中把显式设置的 AcceptEncoding = "lz4" 变成 lz4, gzip, deflate,把显式设置的 "identity" 变成 identity, gzip, deflate;而由于 ClickHouse 是按自身固定的 编解码器 优先级来解析该请求头 (忽略顺序和 q 值) ,它可能返回一个你从未请求过的 编解码器,随后 handler 又将其解码并剥离,让你根本无从察觉。不启用该掩码,才能保证实际发出的内容正是你所选择的。
如果 AcceptEncoding 请求了 驱动程序 无法解码的 编解码器 (snappy) ,则只有 ExecuteRawResultAsync 是安全的。ExecuteReaderAsync、ExecuteScalarAsync 和 ExecuteNonQueryAsync 会抛出 NotSupportedException 并指明该 编解码器 (此前它们会把 compressed bytes 当作结果 format 来解析,从而产生无效数据) 。

错误响应体

当服务器返回 4xx/5xx,且设置了 enable_http_compression=1 时,它会使用与成功响应相同的 编解码器 来压缩错误响应体。驱动程序会对其支持的所有编解码器 (lz4、zstd、gzip、deflate、br/brotli) 进行解码,因此 ClickHouseServerException 中显示的消息是可读的。对于其他编解码器 (snappy、……) ,它会返回一条占位消息,其中会标明 编解码器,并提示到 system.query_log 中查看原始错误文本。

响应解压

Accept-Encoding 只是要求服务端压缩响应——总得有人来解码。驱动程序 会根据响应的 Content-Encoding 自行解码,因此所有常规读取 API (ExecuteReaderAsync、ExecuteScalarAsync、ExecuteNonQueryAsync、QueryAsync<T>、Dapper、EF Core、linq2db) 都能直接处理压缩后的响应,无需任何额外配置。它可解码 lz4、zstd、gzip、deflate 和 br;不支持 snappy。 默认情况下,驱动程序 声明的编码为 zstd, lz4, gzip, deflate,ClickHouse 会以 zstd 作答。若想使用其他编码,可自行设置 Accept-Encoding——在 client 级别:
按查询设置,该设置具有更高的优先次序:
或者在 connection string 中设置,适用于从不直接使用 ClickHouseClientSettings 的 ORM 用户:
设置该值还会在 URL 上强制加上 enable_http_compression=1,ClickHouse 必须先有这个参数才会理会该请求头——即便 UseCompression 为 false 时也一样,因为显式指定编解码器本身就被视为在请求压缩。若未设置任何值,UseCompression=false 则完全不发送 Accept-Encoding。 Accept-Encoding 可以在四个位置设置,其中第一个指定了编解码器的位置生效:
  1. QueryOptions.AcceptEncoding (或 ClickHouseCommand.AcceptEncoding)
  2. 查询上的 CustomHeaders["Accept-Encoding"]
  3. 客户端上的 CustomHeaders["Accept-Encoding"]
  4. ClickHouseClientSettings.AcceptEncoding,或连接字符串关键字 AcceptEncoding
如果四处都未指定,驱动程序会发送其默认列表。未指定任何编解码器的值 (null、空字符串、空白字符,或仅有逗号) 视为未设置,并继续查找下一个位置。若要关闭压缩,请使用 identity。 选择编解码器的是服务端,而非客户端。 ClickHouse 会按其自身固定的优先顺序扫描 Accept-Encoding 中的标记——zstd > br > lz4 > snappy > gzip > deflate——并忽略你列出的顺序以及任何 q 值。因此该请求头只是一种能力通知,而非硬性要求,唯一能左右选择结果的方式就是省略掉哪些标记。默认列表包含 zstd,所以默认查询会以 zstd 响应;其余标记则作为回退方案。br 可以解码,但默认不会在请求头中声明。 各编解码器在载荷大小、服务端 CPU 与客户端 CPU 上的表现对比,取决于你的数据、链路以及服务端的 http_zlib_compression_level (发行默认值:3) ——参见压缩调优。
  • http_zlib_compression_level。 该设置对每一种 HTTP 编解码器都生效,默认值为 3。应根据你的数据、链路速度和 CPU 占用情况进行调优。
  • 快速链路上受 CPU 限制的客户端。 驱动程序会在调用线程上解码响应体,因此当网络不是瓶颈时,客户端的解码速度可能成为限制因素。
符合以下任一情况时,可按查询或在客户端范围内请求使用不同的编解码器:
由于该决定基于响应做出,只要响应的 Content-Encoding 如此声明,无论请求时如何设置,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 始终原封不动地返回收到的字节。
如上所示,请在返回的 stream 离开作用域之前将其读取完毕。当响应确实经过压缩时,你拿到的是以 leaveOpen 方式创建的 decoder,因此释放它不会影响响应;当响应未经过压缩时,你拿到的是 HTTP 内容流本身,释放它会终止 body。无论哪种情况,ClickHouseRawResult 都持有该响应——stream 被释放后,请勿再调用它的其他读取成员。释放 ClickHouseRawResult 始终是必需的,且仅此一步即已足够:它会同时释放响应以及此处引入的 decoder (decoder 会持有池化的 buffer) 。因此上面的 await using 是可选的,保留也无妨。多次顺序调用返回的是同一个 stream;该类型不支持并发使用。 可运行示例请参见 Select_007_ResponseCompression.cs。

插入 (请求) 压缩

Zstd 是插入操作的默认 编解码器:InsertOptions.Compressor 的初始值为 ZstdCompressor.Default,即级别 3 的 zstd。将其设置为其他压缩器可更换 编解码器,设置为 null 则以未压缩的形式发送 body。
驱动程序 中内置了四种 编解码器。每种都提供一个 Default instance,以及一个接受压缩级别和 write buffer 大小的构造函数:
共享压缩器实例。 每个 Default 都是一个共享实例,且这四个压缩器均可安全地被多个线程同时使用——当 InsertOptions.MaxDegreeOfParallelism 大于 1 时便是如此,因为一次 insert 会为每个批次使用一个压缩器。 它们都没有实现 IDisposable。建议自行构造一个实例并重复使用,用法与 Default 相同。
IClickHouseCompressor 是公开的,其实现只需提供两个成员:
服务器必须接受你所指定的 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 也是如此。

调优压缩

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

决定性的那个数字

只要 编解码器 比网络更快,压缩就值得开启。 在读取路径上,这个 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 端的响应压缩更快。

按部署环境划分的粗略指南

该表未涵盖以下三点:
  • 出站流量费用: 如果数据传输需要计费,字节数除了影响延迟外还直接产生费用,这会促使你无论链路速度如何都倾向于更高的压缩级别。
  • 小结果集: 以上讨论均针对较大的载荷。对于小型响应,选用哪种编解码器几乎无关紧要,起主导作用的是单次请求的开销。
  • 并行插入会抬高插入侧的阈值。 上述吞吐量数字都是针对单个线程而言的。InsertOptions.MaxDegreeOfParallelism 默认为 1,调高该值后会并发压缩多个批次,因此客户端的总体编码速率会大致随分配的核心数而提升。因此在高速链路上,即使速度已远超单线程插入不值得压缩的临界点,并行插入仍然值得压缩。请把表中插入一列的取值视为下限;如果你已经在并行批量插入,请先重新测试,再判断链路是否快到不值得压缩。
读取路径只能在多个查询之间实现并行。

选择编解码器

级别

响应压缩由单个服务器设置 http_zlib_compression_level 控制,它对所有 HTTP 编解码器 生效,而不仅仅是 zlib。默认值为 3。 除非有实测数据支撑,否则不要改动它。高于默认值时,付出大量 CPU 却换不来多少体积收益 (以 zstd 为例,3 → 6 大约会让服务器 CPU 翻倍,而字节数仅减少约 14%) ,br 的表现更是糟糕得离谱。低于默认值时,比如级别 1,情况就确实不一样了:lz4 的开销大幅降低,zstd 相对它的 CPU 优势也随之消失。如有需要,可以按 查询 单独设置:

测量你自己的交叉点

要优化编解码器和压缩级别的选择,最快的方法是使用几种不同的编解码器对同一查询计时并进行比较。
若要从 服务器 侧观察同一情况,可从 system.query_log 中读回 ProfileEvents —— 设置 QueryOptions.QueryId,以便定位到对应的行:
如果你自己做基准测试,这里有一个陷阱:不带 ORDER BY 的裸 LIMIT n 每次运行返回的行都不一样,因此每次重复压缩的数据都不同,得出的比率也就成了噪声。请针对固定的结果集进行比较。

原始流插入

使用 InsertRawStreamAsync 可直接从文件流或内存流插入数据,支持 CSV、JSON、Parquet 等任意受支持的 ClickHouse 格式。 从 CSV 文件插入:
driver 会接管该 stream 的所有权。 InsertRawStreamAsync 和 PostStreamAsync 会在请求结束后释放你传入的 stream,无论请求成功还是失败。请勿自行释放,也不要在之后重复使用——这正是上面的示例没有把 FileStream 放在 using 中的原因。你自己写的 using 会在 driver 释放该 stream 之后才执行。对于 FileStream 或 MemoryStream,这第二次释放是无害的;但如果某个 stream 的 Dispose 会归还池化的 buffer 或递减引用计数,就会导致资源被释放两次。所有权只有在 argument 被接受之后才会转移:如果调用因缺少 table、stream 或 format 而 throw ArgumentException 或 ArgumentNullException,则该 stream 仍归你所有。
有关控制数据摄取行为的选项,请参阅 format 设置文档。

更多示例

如需更多实用用法示例,请参阅 GitHub 仓库中的 examples 目录。

ADO.NET

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

使用 ClickHouseDataSource 管理生命周期

始终通过 ClickHouseDataSource 创建连接,以确保正确管理生命周期并使用连接池。DataSource 在内部维护一个 ClickHouseClient,所有连接共享其 HTTP 连接池。
使用依赖注入时:
请勿在生产代码中直接创建 ClickHouseConnection。每次直接实例化都会新建一个 HTTP 客户端和连接池,这在高负载下可能导致套接字耗尽:
请始终使用 ClickHouseDataSource,或共享同一个 ClickHouseClient 实例。

使用 ClickHouseCommand

通过连接创建命令来执行 SQL:
命令方法:
  • ExecuteNonQueryAsync() - 用于 INSERT、UPDATE、DELETE 和 DDL 语句
  • ExecuteScalarAsync() - 返回第一行第一列的值
  • ExecuteReaderAsync() - 返回一个 ClickHouseDataReader,用于遍历结果

使用 ClickHouseDataReader

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

读取枚举的序号

Enum8 或 Enum16 列会以其 label 的形式 materialize:GetFieldType 返回 string,GetString、GetValue 和 GetFieldValue<string> 也都会返回该 label。在枚举列上调用数值类型的 accessor 会抛出 InvalidCastException,因为其中存放的值是字符串。 使用 TryGetEnumOrdinal 即可获取 label 对应的数字:
对于 Enum8/Enum16 列,以及单元不为 NULL 的 Nullable(Enum...) 列,它返回 true 并设置 value;对于 NULL 单元或任何非枚举列,它返回 false,并将 value 置为 0。该序号是来自 wire 的有符号值,因此可能为负数,且 Enum16 的序号可能超出一个字节的范围。

最佳实践

连接生命周期与连接池

ClickHouse.Driver 底层使用 System.Net.Http.HttpClient。HttpClient 会为每个端点维护一个连接池。因此:
  • 数据库会话会通过连接池管理的 HTTP 连接进行多路复用。
  • HTTP 连接会由连接池自动复用和回收。
  • 即使 ClickHouseClient 或 ClickHouseConnection 对象已释放,连接仍可能继续保持活动状态。
推荐做法:
使用自定义 HttpClient 或 HttpClientFactory 时,请确保将 PooledConnectionIdleTimeout 设置为小于服务器 keep_alive_timeout 的值,以避免因连接半关闭而导致错误。Cloud 部署的默认 keep_alive_timeout 为 10 秒。
避免在未共享 HttpClient 的情况下创建多个 ClickHouseClient 或独立的 ClickHouseConnection 实例。每个实例都会创建自己的连接池。

DateTime 处理

  1. 尽可能使用 UTC。 将时间戳存储为 DateTime('UTC') 列,并在代码中使用 DateTimeKind.Utc。这样可以避免时区歧义。
  2. 使用 DateTimeOffset 进行明确的时区处理。 它始终表示某个确定的时间点,并包含偏移信息。
  3. 在 SQL 类型提示中指定时区。 当参数中使用 Unspecified 的 DateTime 值,且目标列不是 UTC 时,请在 SQL 中包含时区信息:

异步插入

异步插入 将批处理的责任从客户端转移到服务器。服务器不再要求客户端进行批处理,而是缓冲传入的数据,并根据可配置的阈值将其刷写到存储中。这对于高并发场景非常有用,例如在可观测性工作负载中,大量 agent 会发送小型载荷。 可通过 CustomSettings 或连接字符串启用异步插入:
两种模式 (由 wait_for_async_insert 控制) :
使用 wait_for_async_insert=0 时,错误只会在刷新期间暴露出来,且无法追溯到原始插入。客户端也不会提供背压,存在服务器过载的风险。
关键设置:

会话

仅在需要有状态的服务器端功能时才启用会话,例如:
  • 临时表 (CREATE TEMPORARY TABLE)
  • 在多条语句之间保持查询上下文
  • 会话级设置 (SET max_threads = 4)
启用会话后,请求会按顺序串行处理,以防止同一会话被并发使用。对于不需要会话状态的 工作负载,这会带来额外开销。
使用 ADO.NET (兼容 ORM) :

支持的数据类型

ClickHouse.Driver 支持所有 ClickHouse 数据类型。下表展示了从数据库读取数据时,ClickHouse 类型与原生 .NET 类型之间的映射。

类型映射:从 ClickHouse 读取

整型


浮点类型


Decimal 类型

Decimal 类型的转换由 UseCustomDecimals 设置控制。

布尔类型


String 类型

默认情况下,String 和 FixedString(N) 列都会作为 string 返回。要将它们改为读取为 byte[],请在连接字符串中设置 ReadStringsAsByteArrays=true。这在存储可能不是有效 UTF-8 的二进制数据时非常有用。该设置同样作用于嵌套在其他类型中的字符串,因此 Array(String) 会读取为 byte[][], Map(String, String) 会读取为 Dictionary<byte[], byte[]> —— 键也包含在内。唯一的例外是 JSON 列,其字符串叶子值始终为文本;参见 JSON type。

日期和时间类型

ClickHouse 在内部将 DateTime 和 DateTime64 值存储为 Unix 时间戳 (即自纪元以来的秒或亚秒单位) 。虽然存储始终采用 UTC,但列可以关联一个时区,这会影响值的显示和解析方式。 读取 DateTime 值时,DateTime.Kind 属性会根据列的时区进行设置: 对于非 UTC 列,返回的 DateTime 表示该时区中的挂钟时间。使用 ClickHouseDataReader.GetDateTimeOffset() 可获取带有该时区正确偏移量的 DateTimeOffset:
对于没有显式指定时区的列 (即 DateTime,而不是 DateTime('Europe/Amsterdam')) ,驱动程序 会返回一个 Kind=Unspecified 的 DateTime。这样可以原样保留存储的挂钟时间,而不对时区作任何假定。 如果你需要让没有显式时区的列具备时区感知行为,可以:
  1. 在列定义中显式指定时区:DateTime('UTC') 或 DateTime('Europe/Amsterdam')
  2. 读取后自行应用时区。

JSON 类型

JSON 列的返回类型由 JsonReadMode 设置控制:
  • Binary (默认):返回 System.Text.Json.Nodes.JsonObject。可对 JSON 数据进行结构化访问,但专用的 ClickHouse 类型 (如 IP 地址、UUID、较大精度的 Decimal) 会在 JSON 结构中转换为字符串表示形式。
  • String:以 string 形式返回原始 JSON。保留 ClickHouse 中 JSON 的精确表示形式,这在你需要不经解析直接传递 JSON,或想自行处理反序列化时非常有用。
None 是第三种模式。其读取方式与 Binary 完全相同,但不会随查询发送任何服务器设置——适用于不允许设置该项的连接。 在列类型中声明的路径为类型化路径,文档中的其他路径则为动态路径。当值为 null 时,两者的表现有所不同。 类型化路径始终会出现在 JsonObject 中。若声明为 Nullable(T) 或 Dynamic,则无论是存储的值为 null,还是文档中根本不存在该路径,返回的都是 JSON 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}}。
这是 server 自身渲染的结果,因此 Binary 与 String 模式现在表现一致。在 1.4.0 之前,值为 null 的类型化路径会被从 JsonObject 中移除,导致 {"x":null} 读回为 {}——而对于 JSON(a.b Nullable(Int64)) 这类嵌套路径,整个 a 子树都会消失。
无论 ReadStringsAsByteArrays 设置为何值,JSON 列中的字符串叶子节点始终以文本形式返回——JsonValue 没有字节数组形式,否则 byte[] 会被渲染为 base64。这一点适用于 String、FixedString,以及被 LowCardinality、Nullable 或 SimpleAggregateFunction 包装的上述类型,同样适用于 Array 和 Map 中的字符串 (包括 map 的键) 。
JSON reader 无法识别其类型的字节数组仍会渲染为 base64:Variant 或 Dynamic 类型的 typed path 所持有的值,其类型只能逐行确定,因此 Variant(Array(UInt8), String) 下的字符串会以 base64 编码形式返回。两种设置下的行为都是如此。如果 JSON map 的键类型不是严格的 String——例如 Map(LowCardinality(String), String)——则会抛出 NotSupportedException。
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}。若某个路径有值,而其父级是标量或数组,则仍会抛出异常,因为子树无法置于二者之下。

Map type

ClickHouse 的 Map(K, V) 在物理上就是 Array(Tuple(K, V)),可以包含多个键相同的条目,而 Dictionary 做不到这一点。因此在默认模式下,重复的键只保留最后一个值,此前的键值对会被丢弃。MapReadMode 设置用于选择采用哪种表示形式:
  • Dictionary (默认) :返回 Dictionary<K, V>。
  • KeyValuePairs:按 server 发送键值对的顺序返回 List<KeyValuePair<K, V>>,因此所有键值对都会保留,包括键重复的条目。
该 mode 决定 Map 列的框架类型,因此它同样影响 GetFieldValue<T>、驱动程序报告的 schema 类型以及 POCO property 映射。只要 map 出现在某个列的类型树中,该设置就会生效 —— 包括 Array(Map(...))、Map(K, Map(...))、Tuple(..., Map(...)) 以及 Dynamic。 在写入路径上,两种 mode 下都接受这两种表示形式 —— 参见 写入 Map。

其他类型

The Dynamic 和 Variant 类型会转换为每一行实际底层类型对应的类型。

几何类型

Geometry 类型是一种 Variant 类型,可容纳任意几何类型。它会被转换为对应的类型。

类型映射:写入 ClickHouse

插入数据时,驱动程序会将 .NET 类型转换为相应的 ClickHouse 类型。下表列出了每种 ClickHouse 列类型可接受的 .NET 类型。

整数类型


浮点类型


布尔类型


String 类型


日期和时间类型

超出范围的值在二进制写入路径中,超出支持范围的 Date、Date32、DateTime 和 DateTime32 值会在 Write 时抛出 ArgumentOutOfRangeException,并指出列类型和支持范围。此前,超出范围的值可能会先经由 32 位整数被静默截断,再由服务器重新解释,从而产生看似真实但实际错误的时间戳。
驱动程序在写入值时会遵循 DateTime.Kind: DateTimeOffset 值始终保留精确时刻。 示例:UTC DateTime (保留精确时刻)
示例:未指定 DateTime (挂钟时间)
**建议:**为获得最简单且最可预测的行为,所有 DateTime 操作都使用 DateTimeKind.Utc 或 DateTimeOffset。这样可以确保你的代码始终保持一致,不受服务器时区、客户端时区或列时区的影响。

HTTP 参数与批量复制

在写入 Unspecified DateTime 值时,HTTP 参数绑定和批量复制之间有一个重要区别: 批量复制 知道目标列的时区,因此会按该时区正确解释 Unspecified 值。 HTTP 参数 不会自动获知列的时区。你必须在 SQL 类型提示中显式指定它:

Decimal 类型


JSON 类型

写入 JSON 时的行为由 JsonWriteMode 设置控制:
  • String (默认) :接受 string、JsonObject、JsonNode 或任意对象。所有输入都会通过 System.Text.Json.JsonSerializer 序列化,并作为 JSON 字符串发送到服务端解析。这是最灵活的模式,无需注册类型即可使用。
  • Binary:仅接受已注册的 POCO 类型。数据会在客户端转换为 ClickHouse 的二进制 JSON 格式,并完整支持类型提示。使用前需要调用 connection.RegisterJsonSerializationType<T>()。在此模式下写入 string 或 JsonNode 值会抛出 ArgumentException。
当 JSON 列带有类型提示 (例如 JSON(id UInt64, price Decimal128(2))) 时,驱动程序会利用这些提示对值进行序列化,从而完整保留类型信息。这样可以保留 UInt64、Decimal、UUID 和 DateTime64 等类型的精度,否则它们在按通用 JSON 序列化时可能会损失精度。 根据 JsonWriteMode,可通过两种方式将 POCO 写入 JSON 列: String 模式 (默认) :POCO 通过 System.Text.Json.JsonSerializer 进行序列化。无需注册类型。这是最简单的方法,也适用于匿名对象。 Binary 模式:POCO 使用驱动的二进制 JSON 格式进行序列化,并完整支持 类型提示。使用前必须通过 connection.RegisterJsonSerializationType<T>() 注册类型。此模式还支持通过特性自定义 path 映射:
  • [ClickHouseJsonPath("path")]:将属性映射到自定义 JSON path。适用于嵌套结构,或属性名与所需的 JSON 键不一致时。仅在 Binary 模式下有效。
  • [ClickHouseJsonIgnore]:序列化时排除此属性。仅在 Binary 模式下有效。
属性名称与列类型提示的匹配是区分大小写的。属性 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 模式下生效) 。

其他类型


几何类型


不支持写入的类型


嵌套类型处理

ClickHouse 嵌套类型 (Nested(...)) 可按数组语义进行读写。

日志与诊断

ClickHouse .NET 客户端集成了 Microsoft.Extensions.Logging 抽象,提供轻量、按需启用的日志功能。启用后,驱动程序会针对连接生命周期事件、命令执行、传输操作以及批量插入操作输出结构化消息。日志功能完全是可选的——未配置日志记录器的应用程序仍可继续运行,且不会带来额外开销。

快速入门

使用 appsettings.json

你可以通过标准的 .NET 配置来设置日志级别:

使用内存中的配置

你也可以在代码中按类别配置日志详细级别:

类别和发出方

该驱动使用专门的类别,以便你可以按组件精细调整日志级别:

示例:排查连接问题

这将记录:
  • HTTP 客户端工厂的选择 (默认连接池或单个连接)
  • HTTP handler 配置 (SocketsHttpHandler 或 HttpClientHandler)
  • 连接池设置 (MaxConnectionsPerServer、PooledConnectionLifetime 等)
  • 超时设置 (ConnectTimeout、Expect100ContinueTimeout 等)
  • SSL/TLS 配置
  • 连接打开/关闭事件
  • 会话 ID 跟踪

调试模式:网络跟踪与诊断

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

OpenTelemetry

该驱动程序内置了对通过 .NET System.Diagnostics.Activity API 实现的 OpenTelemetry 分布式链路追踪的支持。启用后,驱动程序会为数据库操作生成 span,并可将其导出到 Jaeger 或 ClickHouse 自身等可观测性后端 (通过 OpenTelemetry Collector) 。

启用链路追踪

在 ASP.NET Core 应用中,将 ClickHouse 驱动的 ActivitySource 添加到 OpenTelemetry 配置中:
对于控制台应用程序、测试或手动配置:

Span 属性

每个 span 都包含标准的 OpenTelemetry 数据库属性,以及可用于调试的 ClickHouse 特有查询统计信息。

配置选项

通过 ClickHouseDiagnosticsOptions 控制链路追踪行为:
启用 IncludeSqlInActivityTags 可能会在链路追踪中泄露敏感数据。在 production 环境中使用时请务必谨慎。

TLS 配置

通过 HTTPS 连接 ClickHouse 时,您可以通过多种方式配置 TLS/SSL。

自定义证书验证

对于需要自定义证书验证逻辑的生产环境,请提供您自己的 HttpClient,并配置 ServerCertificateCustomValidationCallback 处理程序:
使用自定义 HttpClient 时的重要注意事项
  • 自动解压缩:请让 AutomaticDecompression 保持关闭。驱动会自行解码压缩后的响应,因此无需启用;而且启用它反而会在请求侧带来负面影响:发送时,处理程序还会将其掩码中的每种算法都加入到发出的 Accept-Encoding 中,扩大驱动原本声明的范围,导致 ClickHouse 可能使用您并未请求的 codec 进行响应。参见 响应解压缩。
  • 空闲超时:将 PooledConnectionIdleTimeout 设置为小于服务器的 keep_alive_timeout (ClickHouse Cloud 中为 10 秒) ,以避免半开连接导致的连接错误。

性能调优

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

速览

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

读取:选择物化路径

从结果中取出一行有三种方式,开销各不相同。其中某些路径会对结果进行装箱,导致内存分配增加、性能下降。 对 hits 数据集的 105 个列读取 1,000,000 行时:
ORM 在使用类型化访问器时会走快速路径。 linq2db 会为每一列注册 GetInt64、 GetDouble 和 GetDateTime,因此读取时不会发生装箱。而通过 GetValue 读取的代码 (包括 Dapper 返回的 dynamic 结果) 会对每个值进行装箱。如果某个 ORM 查询是热点查询, 且通过 GetValue 读取,请针对该查询改用 QueryAsync<T>。

插入:批次大小与并行度

批次大小是影响插入吞吐量的最主要因素。InsertOptions.BatchSize 默认为 100,000 行。 使用较大的批次。 对于一次 1,000,000 行的插入,将每个批次由 10,000 行增大到 100,000 行的效果如下: 如果无法控制批次大小 (例如有许多小型 producer 各自独立发送行) ,请使用异步插入,把攒批交给服务器完成。 并行上传。 InsertOptions.MaxDegreeOfParallelism 默认为 1。将其调大即可同时发送多个批次。启用压缩时收益最为明显,因为每个批次会在各自的线程上压缩。session 无法与并行插入同时使用:要么关闭 session,要么保持 MaxDegreeOfParallelism = 1。 去掉 schema 探测。 每次调用 InsertBinaryAsync 都会先发送一个 SELECT ... WHERE 1=0 查询, 以确定列类型。参见跳过 schema 探测查询,通过 ColumnTypes 或 UseSchemaCache 省去这次往返。
免装箱的插入路径仅适用于默认的 RowBinary 格式。RowBinaryWithDefaults 必须 逐个检查值以查找 DBDefault marker,因此仍会走较慢的路径。

压缩:两个方向的结论并不一致

压缩本质上是用 CPU 换字节数。这笔交换是否划算,取决于传输的方向、你与 ClickHouse server 之间连接的带宽、数据与所选压缩算法的契合程度,以及是否需要为传输的每个字节付费。 读取: 保持压缩开启,除非 server 与客户端运行在同一台机器上。这也是默认行为。与不压缩相比,级别为 1 的 zstd 表现如下: 插入: 先测量,再决定是否压缩。节省的开销未必足以抵消开启压缩的代价。此外还需注意,解压会给 server 带来额外负载;对于 Zstd 和 LZ4 来说这一负载不大,但对其他算法 (例如 Brotli) 可能相当高。 关闭插入压缩:
关于 codec 选择、压缩级别,以及如何找到适合自己场景的交叉点,请参阅压缩调优。

缓冲区

ReadBufferSize 用于设置读取 HTTP 响应的缓冲区大小,默认值为 64 KiB。 驱动会从共享池中租用该缓冲区,并在释放 reader 时归还,因此不会为每个查询单独分配一次内存。增大该值可以减少处理大结果集时缓冲区重新填充的次数。驱动会为每个同时处于打开状态的 reader 各持有一个缓冲区,因此内存占用会随缓冲区大小以及并发 reader 数量的增加而上升。
务必释放 reader。 释放 reader 时,它会把占用的池化 buffer 归还到 pool,并释放其 HTTP connection。 若直接丢弃 reader,buffer 不会归还到 pool,还可能导致该 HTTP connection 一直处于不可用状态; 普通的垃圾回收并不能替代显式释放。

运行时与 GC

对于写入密集型应用,请启用 Server GC。 在代码相同、分配字节数相同的情况下,Workstation GC 的插入性能最多比 Server GC 慢 97%。
ASP.NET Core 项目已经默认设置了这一项,而控制台应用、worker service 以及大多数容器镜像则没有。 原因在于第 0 代的预算大小。Workstation GC 使用的预算较小,因此 insert 产生的短生命周期缓冲区来不及在第 0 代被回收,而是被晋升到第 1 代,晋升量随之上升,进而带来多得多的第 2 代回收开销。在某个 insert 场景中,每 1,000 次操作触发的第 2 代回收次数,Server GC 下为 4,000 次,Workstation GC 下则高达 73,000 次。
Server GC 是一项吞吐量设置,而非延迟设置。在同一组测量中,Server GC 的总暂停时间不到前者的一半,但单次暂停更长 (第 95 百分位为 114.6 毫秒,对比 61.9 毫秒) 。如果你的服务对尾部延迟敏感,请先对两种模式分别测量,再做选择。

延迟:复用连接

建立新的 TCP 连接并完成 TLS 握手会耗费大量时间。 复用连接可以显著降低查询延迟。
  • 不要为每个请求都创建一个客户端。每个拥有自身 HttpClient 的新客户端都会创建新的 连接池,并再次付出握手开销。在应用程序的整个生命周期中应始终使用同一个 ClickHouseClient。它是线程安全的,专为 单例使用而设计。
  • 对于 ADO.NET 和 ORM,请使用 ClickHouseDataSource,让所有连接共享同一个连接池。
完整的实践模式请参阅 连接生命周期与连接池。

自行测量

在很多情况下,性能取决于数据的形态、与 server 之间链路的速度、你是否愿意用客户端 CPU 换取服务端 CPU (或反过来) 、硬件限制等因素。 因此建议你结合自己的数据和环境自行测量性能。 若要查看 server 端承担的工作量,请设置 QueryOptions.QueryId,然后读取返回的计数器:

ORM 支持

ORM 需要使用 ADO.NET API (ClickHouseConnection) 。为妥善管理连接生命周期,请通过 ClickHouseDataSource 创建连接:

Dapper

ClickHouse.Driver 可与 Dapper 配合使用。该驱动程序会自动将 Dapper 的 @parameter 语法转换为 ClickHouse 的原生 {parameter:Type} 语法,并根据 .NET 值推断类型。 使用 ClickHouseDataSource 以正确管理连接的生命周期:

参数传递方式

支持 Dapper 的所有标准参数传递方式: 匿名对象:
POCO 类:
字典:
DynamicParameters (来自字典或匿名对象) :

将查询结果映射到 POCO

Dapper 会按名称将列映射到属性 (不区分大小写) :

ClickHouse 原生参数语法

当需要显式控制类型时,可直接在 SQL 中使用 ClickHouse 的 {param:Type} 语法,并通过 Dictionary<string, object> 提供参数值。不要对同一个参数同时使用 @param 语法和 {param:Type} 语法。

WHERE IN

Dapper 原生支持 IN 展开:
Dapper 会将其重写为 WHERE id IN (@Ids1, @Ids2, @Ids3),驱动程序随后会转换每个展开后的参数。 ClickHouse 的 has() 也支持配合 Array 参数使用:

自定义类型处理器

某些 ClickHouse 类型 (如 ITuple、BigInteger 和 ClickHouseDecimal) 需要在启动时注册相应的处理器:
有关类型处理程序实现的示例,请参见 Dapper 示例。

Dapper.Contrib

GetAll<T>() 和 Get<T>(id) 可以正常工作。Insert<T>() 不支持——它会生成 SQL Server 语法 (SCOPE_IDENTITY、[]) 。建议改用 ClickHouseClient 原生的 InsertBinaryAsync 方法。
属性名称必须与 ClickHouse 列名完全一致 (区分大小写) 。

局限性

Linq2db

此驱动与 linq2db 兼容;后者是适用于 .NET 的轻量级 ORM 和 LINQ 提供商。详细文档请参见项目网站。 示例用法: 使用 ClickHouse 提供商创建 DataConnection:
表映射可以通过特性或 Fluent API 配置来定义。如果类名和属性名与表名和列名完全一致,则无需配置:
查询:
批量复制: 使用 BulkCopyAsync 可高效执行批量插入。

Entity Framework Core

ClickHouse 官方的 Entity Framework Core 提供商。可将 C# 类映射到 ClickHouse 表,使用 LINQ 进行查询,并通过 SaveChanges 插入数据——全部采用熟悉的 EF Core 模式。
该提供商仍在积极开发中。当前版本支持 LINQ 查询 (包括 JOIN、子查询和集合运算) 、通过 SaveChanges / BulkInsertAsync 执行 INSERT、支持完整 DDL (CREATE / ALTER / DROP) 的迁移,以及 ClickHouse 特有的表引擎配置。不支持 UPDATE / DELETE。

安装

需要 .NET 10.0 和 EF Core 10。

快速入门

定义实体和 DbContext,然后使用 LINQ 查询:

支持的类型

在需要 Decimal128/Decimal256 列的完整精度时,请使用 ClickHouseDecimal (来自 ClickHouse.Driver.Numerics) ,而不是 decimal——.NET 的 decimal 仅支持 28–29 位有效数字。

支持的 LINQ 操作

查询: Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking GROUP BY 与聚合: GroupBy 配合 Count, LongCount, Sum, Average, Min, Max —— 包括 HAVING (在 .GroupBy() 之后调用 .Where()) 、在单个投影中使用多个聚合,以及按聚合结果执行 OrderBy。 JOIN: Join (INNER) 、GroupJoin/SelectMany 模式 (LEFT 和 CROSS) 。对于不匹配的行,LEFT JOIN 会返回实际的 null (参见下方的 LEFT JOIN null 语义) 。 子查询: 关联 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 函数 —— 包括算术、对数、三角和实用函数。 该提供程序会自动在每条连接路径中注入 set_join_use_nulls=1,以使 JOIN 行为符合 Entity Framework 的预期。 如果你的 ClickHouse 服务器或 profile 禁止更改此设置 (例如 readonly=1 profile) ,可通过以下方式禁用:
启用 opt-out 后,LEFT JOIN 会返回 ClickHouse 列的默认值,EF 基于 null 的导航属性检测将不再按预期工作。请显式与 0 / "" 比较,不要使用 == null。

插入数据

SaveChanges 使用驱动程序提供的原生 InsertBinaryAsync API——采用 RowBinary 编码并压缩请求体,相比参数化 SQL 效率高得多:
实体在保存后会从 Added 状态变为 Unchanged,与其他 EF Core 提供商一致。 批次大小可配置 (默认值为 1000) :

批量插入

对于高吞吐量的数据加载,请使用 BulkInsertAsync 而不是 SaveChanges。这是 DbContext 上的一个扩展方法,会完全绕过 EF Core 的更改跟踪、标识解析和状态管理,转而直接调用驱动程序的 InsertBinaryAsync,并使用 RowBinary 编码和压缩的请求体。 因此,它非常适合加载大型数据集,尤其是在插入后不需要跟踪实体的场景下:
输入可以是任意 IEnumerable<T>——它会以流式方式处理这些实体,无需将它们全部加载到内存中。返回值为插入的行数。插入后,实体不会附加到 DbContext,因此不会发生 Added → Unchanged 状态转换。

枚举

ClickHouse Enum8/Enum16 列可映射为 string 属性或 C# enum 类型。使用 C# 枚举时,提供商会自动在枚举值及其字符串表示形式之间进行转换:

自定义类型转换

EF Core 的 ValueConverter 系统允许你将自定义类型映射到提供商已支持的类型。提供商不会直接看到你的自定义类型——EF Core 会在边界处完成转换。 针对单个属性的转换:
可重用的转换器类:

列类型注解

对于 string、int、DateTime 等标量类型,提供商会自动推断出 ClickHouse 类型。对于参数化类型和包装类型,则需要显式指定 ClickHouse 类型。 使用数据注解 (attribute) :
在 OnModelCreating 中使用 Fluent API:
支持 Array(Nullable(Int32)) 和 LowCardinality(Nullable(String)) 这类嵌套包装类型——提供程序会在每一层嵌套中自动解开 Nullable 和 LowCardinality。

Variant 和 Dynamic 列

ClickHouse Variant(T1, T2, ...) 和 Dynamic 列在 .NET 中会映射为 object。由于 object 过于宽泛,无法自动推断类型,因此必须通过 .HasColumnType() 显式声明存储类型:
读取时,该值会根据存储的判别器自动反序列化为相应的 .NET 类型 (例如 string、ulong、ulong[]) 。

JSON 列

该提供程序支持 ClickHouse 的 Json 列类型,可映射到 System.Text.Json.Nodes.JsonNode (主要) 或 string (通过自动 ValueConverter) :
JSON 的读取和写入既可通过 SaveChanges,也可通过 BulkInsertAsync 完成:
如果你更喜欢原始 JSON 字符串,可将该属性映射为 string,并将列类型设为 Json——提供程序会自动应用 ValueConverter:
  • 不支持 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>()。

表引擎

通过 ToTable(name, t => ...) 流式 API 配置 ClickHouse 表引擎及引擎特定子句。若未配置引擎,提供商默认使用 MergeTree,并根据实体的主键确定 ORDER BY。
支持的引擎系列: 引擎子句: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings。它们都会附加到 HasXxxEngine() 返回的引擎构建器上。 列级功能: HasCodec, HasTtl, HasComment, HasDefault —— 都会纳入迁移。 数据跳过索引 —— 通过 HasIndex(...).HasSkippingIndexType(...):
普通 (非跳过型) 索引会被静默忽略,因为 ClickHouse 没有对应的实现。唯一索引则会抛出异常,因为 ClickHouse 不强制保证唯一性。

迁移

EF Core 的标准迁移工作流:
支持的操作:

迁移限制

除迁移外,该提供商目前还不支持:
  • UPDATE / DELETE
  • 事务:BeginTransaction 是空操作。ClickHouse 不支持 ACID 事务。
  • JSON 路径查询转换:LINQ 中的 entity.Data["key"] 不会转换为 ClickHouse 的 data.key SQL 语法。请对非 JSON 列进行过滤,并在内存中检查 JSON。

局限性

具有 8 个以上元素且最后一位为嵌套 Tuple 的元组

元素超过 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 嵌套区分开来:

AggregateFunction 列

无法直接查询或插入 AggregateFunction(...) 类型的列。 如需插入:
要进行查询:

最后修改于 2026年9月26日