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 个发行版和最新的两个长期支持版。

安装

从 NuGet 安装软件包:
或者使用 NuGet 包管理器:

快速入门

配置

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

连接设置

数据格式与序列化

会话管理

UseSession 标志可让服务器会话持续保留,以便使用 SET 语句和临时表。会话空闲 60 秒后会被重置 (默认超时时间) 。可通过 ClickHouse 语句或服务器配置调整会话设置,延长会话的存续时间。ClickHouseConnection 类通常支持并行操作 (多个线程可并发执行查询) 。但启用 UseSession 标志后,每个连接在同一时刻只能执行一个查询 (这是服务器端的限制) 。

安全

HTTP 客户端配置

日志与调试

自定义设置与角色

通过连接字符串配置自定义设置时,需添加 set_ 前缀,例如 “set_max_threads=4”。使用 ClickHouseClientSettings 对象时,则不要添加 set_ 前缀。可用设置的完整列表请参阅此处。

连接字符串示例

基本连接

使用自定义 ClickHouse 设置


QueryOptions

QueryOptions 允许您针对单个查询覆盖客户端级别的设置。所有属性均为可选项,仅在显式指定时才会覆盖客户端的默认值。 示例:

InsertOptions

InsertOptions 在 QueryOptions 的基础上,增加了通过 InsertBinaryAsync 执行批量插入时使用的设置。 QueryOptions 的所有属性也适用于 InsertOptions。 示例:

跳过 schema 探测查询

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

ClickHouseClient

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

创建客户端

使用连接字符串或 ClickHouseClientSettings 对象创建 ClickHouseClient。有关可用选项,请参阅配置部分。 ClickHouse Cloud 服务的连接信息可在 ClickHouse Cloud 控制台中查看。 选择一个服务,然后点击 Connect: 选择 C#,下方将显示连接信息。 如果使用自管理 ClickHouse,连接信息由 ClickHouse 管理员提供。 使用连接字符串:
也可以使用 ClickHouseClientSettings:
在依赖注入场景中,请使用 IHttpClientFactory:
ClickHouseClient 适合长期使用,并在整个应用程序中共享。只需创建一个实例 (通常采用单例模式) ,供所有数据库操作复用。客户端会在内部管理 HTTP 连接池。

执行查询

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

插入数据

参数化插入

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

批量插入

使用 InsertBinaryAsync 可高效插入大量行。它采用 ClickHouse’s 原生行二进制格式流式传输数据,支持并行上传多个批次,并可避免参数化查询可能导致的 “URL 过长” 错误。
处理大型数据集时,使用 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 演进

注册类型后,即使 target table 新增了列,POCO 插入仍可正常进行。驱动程序只会插入 POCO 映射的列,因此服务器会自动填充使用 DEFAULT (或其他默认表达式) 的新列。无需修改代码,也无需重新注册。

插入查询的放置位置

二进制插入会将 INSERT INTO ... FORMAT ... 语句写在请求正文的第一行,置于数据行之前。请求正文默认会被压缩,因此只检查 URL 的路由和日志系统无法获取该语句。将 InsertOptions.QueryPlacement 设置为 InsertQueryPlacement.Url,即可改用 query URL 参数发送该语句,让请求正文只包含数据行:
当代理、负载均衡器或 gateway 需要根据 query 参数路由请求或检查请求时,或者当你希望在访问日志和可观测性工具中看到该语句时,请使用此模式。此模式需要显式启用,因为语句会占用 URL 长度。实际限制以 .NET 运行时、中间组件和服务器施加的限制中最严格的一项为准。在 .NET 6 至 .NET 9 中,System.Uri 将编码后的完整请求 URI 限制为 65,519 个字符;如果超出此限制,驱动程序会抛出 InvalidOperationException,提示你改用 InsertQueryPlacement.Body。ClickHouse 的 http_max_uri_size 默认为 1 MiB,但中间组件可能施加更严格的限制。在 body 模式下,语句和行不受此 URL 长度限制;其他请求选项仍可出现在 URL 中。 此设置与 Compressor 无关:两种模式下,请求正文的编码方式相同。

读取数据

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

读取为 POCO 对象

无需按索引或名称逐列读取,你可以将查询结果以流式方式直接映射到自定义类。只需在客户端注册一次该类型,然后使用 QueryAsync<T>:
RegisterPocoType<T>() 会同时配置插入和读取映射,并预先验证这两种映射。RegisterBinaryInsertType<T>() 保持不变,为保持向后兼容,仍仅用于插入。 注册的类型必须具备:
  • 公共无参构造函数。
  • 至少一个带有公共且非 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,而无需将读取器移至下一行:
当你必须自行控制读取器循环时,请使用 MapTo<T>,例如需要将原始列访问 与 POCO 物化结合使用时。它通过读取器返回的装箱值读取行,因此不支持 上述其他属性类型,而且比 QueryAsync<T> 产生更多内存分配。如果只需要获取行,建议使用 QueryAsync<T>;具体数据请参阅 选择物化路径。 在客户端级别或单次查询中配置的读取值转换器对两种读取路径均生效, 且不会禁用无装箱读取。驱动程序根据每列的读取方式调用相应的重载进行转换:无装箱读取的 列调用类型化的 ConvertValue<T>,复合列调用装箱版本的 ConvertValue。请确保两个重载的实现 保持一致,否则同一列通过不同路径读取时会得到不同的结果。 配置 LoggerFactory 后,RegisterPocoType<T>() 和 RegisterBinaryInsertType<T>() 会输出一条 Debug 级别的日志 (类别为 ClickHouse.Driver.Client) ,说明属性与列的映射关系,以及哪些属性被跳过、跳过的原因。参见日志与诊断。

SQL 参数

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

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":
值会原样发送,服务器会将其作为未加引号的 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 是一个钩子,用于控制参数值的序列化方式。当内置格式化方式 (例如 DateTime 精度、十进制数的区域性格式、字符串转义或数字表示形式) 不符合你的 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 的空值标记 (\N) 。

自定义读取值转换

IReadValueConverter 可在反序列化后转换数据读取器返回的值,而不改变其 CLR 类型。常见用途包括:为不带时区信息的 DateTime 列设置 DateTime.Kind = Utc、修剪或规范化字符串,以及在 JSON 列的值传入应用程序代码前对其进行后处理。 在 ClickHouseClientSettings 中设置 ReadValueConverter,即可对所有读取操作应用转换器。无论走装箱 (GetValue) 还是泛型 (GetFieldValue<T>) 路径,转换器都会对每行的每列调用一次。未设置转换器时,不会产生额外开销——读取器会直接返回值。 使用 DictionaryReadValueConverter 按 CLR 类型进行简单转换:
运行时 CLR 类型未通过 For<T> 注册的值会原样传递。分派时会精确匹配 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。TryGetEnumOrdinal 同样会绕过转换器——请参阅 读取枚举的序数值。 转换器也适用于 ADO.NET 的 ClickHouseConnection 路径——从客户端创建的连接会继承这些设置。

原始流式传输

使用 ExecuteRawResultAsync 可以绕过数据读取器,直接以指定格式流式传输查询结果。此方法适用于将数据导出到文件或传递给其他系统:
常用格式:JSONEachRow、CSV、TSV、Parquet、Native。所有可用格式请参阅格式文档。

单次查询的传输压缩

默认情况下,当 Compression=true (连接字符串的默认值) 时,客户端会协商使用 zstd, lz4, gzip, deflate,并自动解码数据流。 导出原始数据 (例如 Parquet、Arrow、Native) 时,你可能希望协商使用其他编解码器 (例如 zstd 或 lz4) ,以增加 CPU 开销来降低带宽占用,同时不更改整个连接的设置。QueryOptions.AcceptEncoding 和 ClickHouseCommand.AcceptEncoding 会为单个请求设置 HTTP Accept-Encoding 请求头,覆盖原有的默认值,并在 URL 中强制设置 enable_http_compression=1 (ClickHouse 只有在设置该参数后才会遵循 Accept-Encoding) 。

HttpClient 配置

无需配置:驱动创建的 HttpClient 将 AutomaticDecompression 保持为 DecompressionMethods.None,由驱动自行解码响应。因此,Content-Encoding 不会在你不知情的情况下被移除,你收到的原始响应正文与服务器发送的完全一致。
如果使用自己的 HttpClient,也要关闭 AutomaticDecompression。此设置并非只影响响应:发送请求时,处理程序会把掩码中尚未出现在传出 Accept-Encoding 中的所有算法都添加进去。因此,掩码为 GZip | Deflate 的处理程序会在传输时将显式设置的 AcceptEncoding = "lz4" 变为 lz4, gzip, deflate,将显式设置的 "identity" 变为 identity, gzip, deflate。ClickHouse 会按自身固定的编解码器优先级解析请求头 (忽略顺序和 q 值) ,因此可能用你并未请求的编解码器返回响应;处理程序随后又会解码响应并移除相关信息,让你无从察觉。关闭该掩码,才能确保提供的选项与你选择的完全一致。
如果 AcceptEncoding 请求了驱动无法解码的编解码器 (snappy) ,则只有 ExecuteRawResultAsync 能安全使用。ExecuteReaderAsync、ExecuteScalarAsync 和 ExecuteNonQueryAsync 会抛出指明该编解码器的 NotSupportedException (此前,它们会将压缩后的字节当作结果格式来解析,产生无用数据) 。

错误响应正文

当服务器返回 4xx/5xx 响应且已设置 enable_http_compression=1 时,会使用处理成功响应时所用的编解码器压缩错误响应正文。驱动程序会对其支持的所有编解码器 (lz4、zstd、gzip、deflate、br/brotli) 进行解码,确保 ClickHouseServerException 中的错误消息可读。对于不支持的编解码器 (如 snappy) ,驱动程序会返回一段占位文本,注明编解码器名称,并提示用户前往 system.query_log 查看原始错误信息。

响应解压缩

Accept-Encoding 只是请求服务器压缩响应,收到响应后仍需解码。驱动会根据响应的 Content-Encoding 自行解码,因此所有常规读取 API (ExecuteReaderAsync、ExecuteScalarAsync、ExecuteNonQueryAsync、QueryAsync<T>、Dapper、EF Core、linq2db) 都能直接处理压缩响应,无需额外配置。驱动可解码 lz4、zstd、gzip、deflate 和 br;不支持 snappy。 默认情况下,驱动会声明支持 zstd, lz4, gzip, deflate,ClickHouse 则会使用 zstd 压缩响应。如需使用其他编码方式,可自行设置 Accept-Encoding,例如为整个客户端设置:
按查询单独设置,且此设置优先:
对于不直接使用 ClickHouseClientSettings 的 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 处理响应正文:未设置该字段或其值为 identity 时,保持原样;指定受支持的编解码器时,进行解码;指定其他值时,则会引发错误,并在错误中指出该值。无需担心重复解码——如果调用方提供的处理程序已通过 AutomaticDecompression 解码响应正文,它也会移除 Content-Encoding,因此驱动程序看到的是明文数据,不会再次解码。 原始结果不会声明任何编解码器。 ExecuteRawResultAsync (以及公开的 PostStreamAsync / InsertRawStreamAsync) 会将响应正文原样交给你。因此,除非你明确指定编解码器,否则它们不会请求使用编解码器。驱动程序不会解码这类响应正文;如果默认请求使用编解码器,就可能在不知情的情况下把导出结果变成压缩文件。因此,无论 HttpClient 如何配置,规则都一样:响应正文会按服务器发送时的样子原样返回;除非你请求使用编解码器,否则服务器发送的是明文。 如果想导出压缩后的字节数据,可以在整个客户端或单次查询中明确请求使用编解码器。 显式设置的 AcceptEncoding (无论在哪个级别设置) 仍适用于原始请求。需要解码结果时,可以调用 ClickHouseRawResult.ReadDecompressedStreamAsync();ReadAsStreamAsync、ReadAsByteArrayAsync、ReadAsStringAsync 和 CopyToAsync 则始终按接收时的原样返回数据。
如上所示,请在返回的流超出作用域之前将其读取完毕。如果响应经过压缩,返回的流是使用 leaveOpen 创建的解码器,释放它不会影响响应;如果响应未经压缩,返回的就是 HTTP 内容流本身,释放该流会结束对响应正文的读取。无论哪种情况,响应都由 ClickHouseRawResult 管理——流释放后,不要再调用它的其他读取成员。始终需要释放 ClickHouseRawResult,且只需释放它即可:它会同时释放响应和此处创建的所有解码器 (解码器持有池化缓冲区) 。因此,上面的 await using 可选,保留也无妨。重复的顺序调用会返回同一个流;此类型不能并发使用。 可参阅 Select_007_ResponseCompression.cs,查看可运行的示例。

插入 (请求) 压缩

Zstd 是插入操作的默认编解码器:InsertOptions.Compressor 的初始值为 ZstdCompressor.Default, 即压缩级别为 3 的 zstd。若要更改编解码器,可将其设置为其他压缩器;若要发送 未压缩的请求正文,则将其设置为 null。
驱动内置四种编解码器。每种都有一个 Default 实例,并提供一个接受压缩级别 和写入缓冲区大小的构造函数:
共享压缩器实例。 每个 Default 都对应一个共享实例,四种压缩器均可由多个线程 同时安全地使用。InsertOptions.MaxDegreeOfParallelism 大于 1 时就会出现这种情况, 因为一次插入会为每个批次使用一个压缩器。它们都未实现 IDisposable。 请自行创建一个实例,并像使用 Default 一样重复使用它。
IClickHouseCompressor 是公开的,实现它只需提供两个成员:
服务器必须接受你指定的 Content-Encoding。其余成员 — Decompress、MethodByte、MaxEncodedLength、Encode 和 Decode — 都有默认 实现,且会抛出 NotSupportedException,因此只需重写编解码器用到的成员。 除了压缩请求,还应实现 Decompress 来解码响应正文;如果响应正文已损坏或格式不正确, 应由它返回的流引发 InvalidDataException。 InsertOptions.Compressor 只控制二进制插入。驱动程序的其他请求正文遵循不同的压缩规则,都不会经过它:
  • 每个 SQL 文本请求 (ExecuteReaderAsync、ExecuteScalarAsync、ExecuteNonQueryAsync、QueryAsync<T>、ExecuteRawResultAsync、ADO.NET 层) 只要 UseCompression 为 true (默认值) ,都会以 Content-Encoding: gzip 发送语句。这里的编解码器无法配置:AcceptEncoding 只影响响应,因此请求只能使用 gzip 或不压缩。设置 Compression=false 则会以明文发送语句。语句通常很短,一般无需在意这一点;但通过代理或抓包查看请求时,了解这一行为很有用。
  • 多部分请求正文 — 即参数以表单数据发送的查询 (UseFormDataParameters=true) — 无论 UseCompression 如何设置,始终不压缩。
  • 原始上传 (InsertRawStreamAsync、PostStreamAsync) 使用每次调用单独指定的标志,既不受 UseCompression 影响,也不受 InsertOptions.Compressor 影响:设置该标志时使用 gzip,否则不压缩。注意,InsertRawStreamAsync 的 useCompression 参数默认为 true,因此除非传入 false,否则原始上传都会使用 gzip 压缩 — 即使客户端设置了 Compression=false 也是如此。

调整压缩配置

压缩是用 CPU 开销换取更小的数据量。是否值得,几乎完全取决于网络连接速度与编解码器运行速度的对比。没有一种设置适合所有人。

判断是否值得压缩的关键数值

只要编解码器的压缩速度快于网络传输速度,压缩就值得启用。 在读取路径上,这个临界值比大多数人预想的要低,因为 ClickHouse 在输出缓冲区中以单线程方式压缩 HTTP 响应。在配备 16 个 vCPU 的 ClickHouse Cloud 服务上测得 (hits、RowBinary、压缩级别 3) ,服务器生成压缩数据的速度约为 100–200 MB/s。 因此,对于较大的查询结果,假设同一时间只处理一个查询,当网络速度达到约 100 MB/s 时,压缩就开始得不偿失。同一云区域内的单条 HTTPS 数据流 通常能超过这一速度,而经由公网、VPN 或跨区域的传输速度通常低于这一水平。 插入路径在链路速度更高时仍能从压缩中受益,因为客户端使用独立的 CPU 核心进行压缩,通常比服务器压缩响应的速度更快。

按部署环境选择压缩方式的参考指南

此表未考虑以下三点:
  • **出站流量成本:**如果数据传输按量计费,除了延迟,传输的每个字节也有成本,因此 无论链路速度如何,都更值得考虑提高压缩级别。
  • **小结果集:**上述建议针对大型载荷。对于较小的响应,编解码器几乎不影响性能, 主要开销来自每次请求。
  • **并行插入会提高插入时的带宽临界值。**上表中的所有吞吐量数据均以单个线程为准。InsertOptions.MaxDegreeOfParallelism 默认为 1;提高该值后,可并发压缩多个批次,客户端的总体编码速率也会大致随分配的核心数增长。因此,即使链路速度快到单线程插入不再值得压缩,并行插入在更高的链路速度下仍可能值得 压缩。请将表中“插入”列的建议视为下限;如果已经并行处理批次,请先重新测试,再判断链路是否快到无需压缩。
读取路径只有在执行多个查询时才能并行处理。

选择编解码器

级别

响应压缩由服务器设置 http_zlib_compression_level 控制。该设置适用于所有 HTTP 编解码器,而不仅仅是 zlib,默认值为 3。 除非实测表明确有必要,否则不要调整。设为高于默认值的级别,会显著增加 CPU 开销,却只能小幅缩减响应体积 (对于 zstd,从 3 提高到 6 会使服务器 CPU 开销大约翻倍,而字节数仅减少约 14%) ;br 的开销则会高得离谱。设为低于默认值的级别时,情况确实有所不同:在级别 1,lz4 的开销大幅降低,zstd 相比 lz4 也不再有 CPU 开销优势。如有需要,可针对每个查询单独设置:

测定适合自身场景的性能交叉点

要选出合适的编解码器和压缩级别,最快的方法是用几种编解码器分别执行同一查询,记录耗时并进行比较。
要查看同一情况在服务器端的表现,可从 system.query_log 中读取 ProfileEvents——设置 QueryOptions.QueryId,以便找到对应的行:
自行开展基准测试时,有一点容易忽略:仅使用 LIMIT n 而不指定 ORDER BY,每次运行返回的行都不同,因此每轮测试压缩的数据也不同,得出的压缩率便无法用于比较。应使用固定的结果集进行比较。

原始流插入

使用 InsertRawStreamAsync,可直接从文件流或内存流插入 CSV、JSON、Parquet 等格式的数据,或使用任何受支持的 ClickHouse 格式。 从 CSV 文件插入:
驱动程序会接管流的所有权。 请求结束时,无论成功还是失败,InsertRawStreamAsync 和 PostStreamAsync 都会释放传入的 流。请勿自行释放该流,也不要在此后复用它——这就是上面的示例没有用 using 包裹 FileStream 的原因。如果自行使用 using,其释放操作会在驱动程序释放流之后执行。对于 FileStream 或 MemoryStream,再次调用 Dispose 不会造成问题;但如果流的 Dispose 会归还池中的 缓冲区或递减引用计数,就会导致资源被释放两次。只有参数通过检查后,流的所有权才会转移:如果调用因缺少表、流或格式而抛出 ArgumentException 或 ArgumentNullException,流仍由你负责释放。
如需了解控制数据摄取行为的选项,请参阅格式设置文档。

更多示例

更多实际使用示例,请参阅 GitHub 仓库中的示例目录。

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 列物化后得到的是标签:GetFieldType 返回 string, GetString、GetValue 和 GetFieldValue<string> 返回的也都是标签。对枚举列使用数值访问器 会抛出 InvalidCastException,因为实际存储的值是字符串。 使用 TryGetEnumOrdinal 获取标签对应的数值:
对于 Enum8/Enum16 列,以及单元格值不为 NULL 的 Nullable(Enum...) 列,该方法返回 true 并设置 value。如果单元格值为 NULL,或 该列不是枚举列,则返回 false,并将 value 设为 0。序数值是从传输数据中读取的 有符号值,因此可能为负数;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 实例。每个实例都会创建自己的连接池。

日期时间处理

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

可通过 UseCustomDecimals 设置控制 Decimal 类型的转换。

布尔类型


字符串类型

默认情况下,String 和 FixedString(N) 列均以 string 类型返回。如需将其读取为 byte[],请在连接字符串中设置 ReadStringsAsByteArrays=true。如果存储的二进制数据不一定是有效的 UTF-8,这一设置会很有用。该设置也适用于嵌套在其他类型中的字符串,因此 Array(String) 会读取为 byte[][], Map(String, String) 则会读取为 Dictionary<byte[], byte[]>,键也不例外。唯一的例外是 JSON 列:其中的字符串叶节点始终按文本处理;参见 JSON 类型。

日期和时间类型

ClickHouse 在内部以 Unix 时间戳存储 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}}。
这是服务器自身呈现的结果,因此 Binary 和 String 模式现在的行为一致。1.4.0 之前, 值为 null 的类型化路径会从 JsonObject 中移除,导致 {"x":null} 读回时变为 {};对于 JSON(a.b Nullable(Int64)) 这样的嵌套路径,整个 a 子树都会消失。
JSON 列中的字符串叶节点始终以文本形式返回,无论 ReadStringsAsByteArrays 设置为何值。这是因为 JsonValue 不支持字节数组形式,byte[] 因而会 呈现为 base64 编码的字符串。这适用于 String、FixedString,以及由 LowCardinality、Nullable 或 SimpleAggregateFunction 包装的类型,也适用于 Array 和 Map 中的字符串, 包括 Map 的键。
对于 JSON 读取器无法获知其类型的字节数组,仍会将其呈现为 base64 编码的字符串: Variant 或 Dynamic 类型化路径中的值只能逐行确定类型,因此 Variant(Array(UInt8), String) 下的字符串会以 base64 编码形式返回。两种设置下的行为相同。如果 JSON 映射的键类型不是严格的 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 类型

ClickHouse 的 Map(K, V) 底层以 Array(Tuple(K, V)) 形式存储,因此可以包含键相同的多个键值对。但 Dictionary 不允许重复键,所以在默认模式下,同一个键只会保留最后一个值,之前的键值对会被丢弃。MapReadMode 设置用于选择返回形式:
  • Dictionary (默认) :返回 Dictionary<K, V>。
  • KeyValuePairs:返回 List<KeyValuePair<K, V>>,顺序与服务器发送键值对的顺序一致,因此所有键值对都会保留,包括键相同的键值对。
该模式决定 Map 列在框架中对应的类型,因此也会影响 GetFieldValue<T>、驱动报告的 schema 类型以及 POCO 属性映射。无论 Map 出现在列类型结构中的哪一层,该模式都适用,包括 Array(Map(...))、Map(K, Map(...))、Tuple(..., Map(...)) 和 Dynamic。 无论采用哪种模式,写入时都支持这两种表示形式——参见写入 Map。

其他类型

Dynamic 和 Variant 类型会根据每行实际存储的底层类型,转换为对应的类型。

几何类型

Geometry 是一种 Variant 类型,可以存储上述任意几何类型的值。读取时,该值会转换为对应的类型。

类型映射:写入 ClickHouse

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

整数类型


浮点类型


布尔类型


字符串类型


日期和时间类型

超出范围的值通过二进制协议写入时,如果 Date、Date32、DateTime 或 DateTime32 的值超出支持范围,Write 会抛出 ArgumentOutOfRangeException,并指出列类型及其支持范围。此前,超出范围的值可能在转换为 32 位整数时被悄然截断,再由服务器重新解释,生成看似有效但实际错误的时间戳。
驱动程序写入值时会根据 DateTime.Kind 进行处理: DateTimeOffset 值始终保持原有时间点。 示例:UTC 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 列: 字符串模式 (默认) :使用 System.Text.Json.JsonSerializer 序列化 POCO。无需注册类型。这种方式最简单,也适用于匿名对象。 二进制模式:使用驱动程序的二进制 JSON 格式序列化 POCO,完整支持类型提示。使用前必须通过 connection.RegisterJsonSerializationType<T>() 注册类型。此模式支持通过特性自定义 JSON 路径映射:
  • [ClickHouseJsonPath("path")]:将属性映射到自定义 JSON 路径。适用于嵌套结构,或属性名与目标 JSON 键名不一致的情况。仅在二进制模式下有效。
  • [ClickHouseJsonIgnore]:序列化时忽略该属性。仅在二进制模式下有效。
属性名与列类型提示的匹配区分大小写。属性 UserId 只会匹配定义为 UserId 的提示,不会匹配 userid。这与 ClickHouse 的行为一致:userName 和 UserName 这样的路径可以作为不同字段共存。 限制 (仅适用于 Binary mode) :
  • 序列化前,必须通过 connection.RegisterJsonSerializationType<T>() 在连接上注册 POCO 类型。序列化未注册的类型会抛出 ClickHouseJsonSerializationException。
  • 字典和数组/列表属性必须在列定义中指定类型提示,才能正确序列化。如果没有类型提示,请改用 String mode。
  • 只有当列定义为相应路径指定了 Nullable(T) 类型提示时,POCO 属性中的 null 值才会被写入。ClickHouse 不允许动态 JSON 路径使用 Nullable 类型,因此未指定类型提示的 null 属性会被跳过。
  • 在 String mode 下,ClickHouseJsonPath 和 ClickHouseJsonIgnore 属性会被忽略 (它们仅在 Binary mode 下生效) 。

其他类型


几何类型


不支持写入的类型


嵌套类型的处理

ClickHouse 的嵌套类型 (Nested(...)) 支持按数组语义读写。

日志与诊断

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

快速入门

使用 appsettings.json

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

使用内存配置

您还可以在代码中按类别设置日志的详细程度:

类别与日志输出源

驱动程序使用独立的日志类别,便于按组件精细调整日志级别:

示例:排查连接问题

这将记录以下信息:
  • HTTP 客户端工厂的选择 (默认连接池或单个连接)
  • HTTP 处理器配置 (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,并可通过 OpenTelemetry Collector 将其导出到 Jaeger 或 ClickHouse 等可观测性后端。

启用追踪

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

Span 属性

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

配置选项

通过 ClickHouseDiagnosticsOptions 配置追踪行为:
启用 IncludeSqlInActivityTags 可能导致敏感数据泄露到链路追踪数据中。在生产环境中启用时请务必谨慎。

TLS 配置

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

自定义证书验证

如果生产环境需要自定义证书验证逻辑,请提供自行创建的 HttpClient,并为其配置 ServerCertificateCustomValidationCallback 处理程序:
提供自定义 HttpClient 时的注意事项
  • 自动解压缩:不要启用 AutomaticDecompression。驱动程序会自行解码压缩响应,无需启用此选项。启用后还会影响请求:发送请求时,处理程序还会将其掩码中的所有算法添加到传出的 Accept-Encoding 中,使其范围超出驱动程序原本声明的算法,ClickHouse 因而可能使用你未请求的编解码器返回响应。参见响应解压缩。
  • 空闲超时:将 PooledConnectionIdleTimeout 设置为小于服务器的 keep_alive_timeout (ClickHouse Cloud 为 10 秒) ,以免半开连接引发连接错误。

性能调优

本节介绍如何使用客户端实现最佳性能,以及如何根据具体用例调整各项配置选项,优化客户端性能。

一览

| 如果你需要 | 建议 | |---|---|---| | 将行读取为 POCO 对象 | 使用 QueryAsync<T>,而不是 MapTo<T> | | 执行大量插入操作 | 增大 InsertOptions.BatchSize | | 运行以插入操作为主的控制台应用或工作线程应用 | 启用 Server GC | | 通过网络读取大量结果 | 保持响应压缩开启 (默认开启) | | 通过高速链路插入数据 | 尝试 InsertOptions.Compressor = null | | 多次向同一张表插入数据 | 使用 UseSchemaCache 或 ColumnTypes | | 读取超大结果集 | 增大 ReadBufferSize |

读取:选择物化路径

从查询结果中读取行数据有多种方式,开销各不相同。有些方式会对结果进行装箱,增加内存分配并降低性能。 读取 hits 数据集中的 1,000,000 行、每行 105 列时:
ORM 使用强类型访问器时,可走快速路径。 linq2db 为每一列注册 GetInt64、 GetDouble 和 GetDateTime,因此读取时无需装箱。通过 GetValue 读取数据的代码 (包括读取 Dapper 返回的 dynamic 结果) 会将每个值装箱。如果某个 ORM 查询频繁执行, 且通过 GetValue 读取数据,请对该查询使用 QueryAsync<T>。

插入:批次大小与并行度

批次大小是影响插入吞吐量的最关键因素。InsertOptions.BatchSize 默认为 100,000 行。 使用大批次。 在插入 1,000,000 行数据时,将每个 批次的行数从 10,000 增加到 100,000,结果如下: 如果无法控制批次大小 (例如,多个小型生产者分别发送数据行) ,请使用异步插入,由服务器将数据合并成批次。 并行上传。 InsertOptions.MaxDegreeOfParallelism 默认为 1。增大此值可同时发送 多个批次。启用压缩时,效果尤其明显,因为每个批次都由独立的线程压缩。 会话与并行插入不能同时使用:请关闭会话,或保持 MaxDegreeOfParallelism = 1。 跳过 schema 探测。 每次调用 InsertBinaryAsync 都会先发送一条 SELECT ... WHERE 1=0 查询, 以确定列类型。参阅跳过 schema 探测查询,了解如何通过 ColumnTypes 或 UseSchemaCache 省去这次往返请求。
免装箱插入路径适用于默认的 RowBinary 格式。RowBinaryWithDefaults 必须 逐一检查值,以查找 DBDefault 标记,因此仍使用较慢的路径。

压缩:两个方向的效果并不一致

压缩是用 CPU 开销换取更少的传输字节数。是否划算,取决于 数据传输方向、客户端与 ClickHouse 服务器之间的连接带宽、所选压缩算法对数据的压缩效果,以及是否按传输量付费。 **读取:**除非服务器与客户端运行在同一台机器上,否则应保持压缩开启。默认情况下压缩已开启。与不压缩相比,使用 1 级 zstd 的结果如下: **插入:**启用压缩前先测试。收益可能不足以抵消压缩带来的开销。还要注意,解压缩会增加服务器负载;Zstd 和 LZ4 带来的负载较小,但其他算法 (例如 Brotli) 可能带来较高负载。 要关闭插入压缩:
如需了解如何选择编解码器、设置压缩级别,以及如何找到适合自身场景的性能平衡点,请参阅 压缩调优。

缓冲区

ReadBufferSize 设置读取 HTTP 响应时使用的缓冲区大小,默认值为 64 KiB。 驱动程序会从共享池中借用缓冲区,并在释放读取器时将其归还,因此无需 每次查询都重新分配。处理大规模结果集时,增大该值可减少 缓冲区重新填充的次数。驱动程序会为每个同时打开的读取器保留一个缓冲区,因此内存占用 会随缓冲区大小和并发读取器数量的增加而增加。
*务必释放读取器。*释放读取器时,它会将缓冲区归还至缓冲池,并释放其 HTTP 连接。 如果直接丢弃读取器,缓冲区就不会归还至缓冲池,HTTP 连接也可能无法继续使用; 常规垃圾回收不能代替显式释放。

运行时与垃圾回收

对于插入操作密集型应用程序,请启用 Server GC。 在代码相同、分配的字节数也相同的情况下, Workstation GC 的插入操作最多比 Server GC 慢 97%。
ASP.NET Core 项目已默认启用此设置。控制台应用程序、工作线程服务和大多数容器 镜像则没有启用。 原因在于第 0 代的内存预算。Workstation GC 的预算较小,因此插入操作创建的 短期缓冲区未能在第 0 代被回收,而是进入第 1 代。 这会增加对象晋升,并大幅增加第 2 代的回收工作。在一项插入操作测试中, 每 1,000 次操作对应的第 2 代垃圾回收次数,使用 Server GC 时为 4,000 次,使用 Workstation GC 时为 73,000 次。
Server GC 是提高吞吐量的设置,而非降低延迟的设置。在同一组测量中,Server GC 的 总暂停时间不到 Workstation GC 的一半,但单次暂停时间更长 (第 95 百分位分别为 114.6 毫秒和 61.9 毫秒) 。如果你的服务对尾部延迟敏感,请先测量 两种模式的表现,再做选择。

延迟:复用连接

建立新的 TCP 连接并完成 TLS 握手需要花费不少时间。 复用连接可以显著降低查询延迟。
  • 不要为每个请求都创建一个客户端。每个拥有独立 HttpClient 的新客户端都会创建新的 连接池,并再次产生握手开销。应在应用程序的整个生命周期内复用同一个 ClickHouseClient。它是线程安全的,适合 作为单例使用。
  • 使用 ADO.NET 和 ORM 时,请使用 ClickHouseDataSource,让所有连接共享同一个连接池。
有关连接复用的完整实践方法,请参阅 连接生命周期与连接池。

自行测量

在许多情况下,性能取决于数据特征、客户端与服务器之间的连接速度、 如何权衡客户端和服务器端的 CPU 开销、硬件限制等因素。 因此,建议结合自己的数据和环境实际测量性能。 要了解服务器端的工作量,请设置 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:
可以通过特性或流畅式配置定义表映射。如果类名和属性名分别与表名和列名完全一致,则无需额外配置:
查询:
批量复制: 使用 BulkCopyAsync 高效批量插入数据。

Entity Framework Core

ClickHouse 官方提供的 Entity Framework Core 提供程序。使用熟悉的 EF Core 模式,即可将 C# 类映射到 ClickHouse 表、通过 LINQ 查询数据,并通过 SaveChanges 插入数据。
此提供程序仍在积极开发中。当前版本支持 LINQ 查询 (包括连接、子查询和集合运算) 、通过 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 (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) ,可通过以下方式禁用这一行为:
启用该选择退出选项后,LEFT JOIN 会返回 ClickHouse 列的默认值,导致 EF 依赖 null 判断导航属性的机制无法按预期工作。请显式与 0 / "" 比较,而不要使用 == null。

插入数据

SaveChanges 使用驱动程序原生的 InsertBinaryAsync API,以 RowBinary 格式编码数据并压缩请求正文,效率远高于参数化 SQL:
保存后,实体会与使用其他 EF Core 提供程序时一样,从 Added 状态变为 Unchanged 状态。 批次大小可配置 (默认值为 1000) :

批量插入

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

枚举

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

自定义类型转换

EF Core 的 ValueConverter 机制可将自定义类型映射为提供商已支持的类型。提供商不会直接接触自定义类型——EF Core 会在两者之间完成转换。 单个属性的转换:
可复用的转换器类:

列类型注解

对于 string、int、DateTime 等标量类型,提供商会自动推断对应的 ClickHouse 类型。对于参数化类型和包装器,则需要显式指定 ClickHouse 类型。 使用数据注解 (特性) :
在 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,也可通过自动 ValueConverter 映射为 string:
使用 SaveChanges 和 BulkInsertAsync 均可读写 JSON:
如果希望使用原始 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 => ...) Fluent 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 个及以上元素、且最后一个元素是嵌套元组的元组

包含超过 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年10月14日