-
ClickHouseClient(推荐) :适合以单例模式使用的高级线程安全客户端。提供简洁的异步 API,支持查询和批量插入。适用于大多数应用程序。 -
ADO.NET (
ClickHouseDataSource、ClickHouseConnection、ClickHouseCommand) :标准的 .NET 数据库抽象。集成 ORM (Dapper、Linq2db) 或需要兼容 ADO.NET 时必须使用。ClickHouseBulkCopy是一个辅助类,可通过 ADO.NET 连接高效插入数据。ClickHouseBulkCopy已弃用,将在后续版本中移除;请改用ClickHouseClient.InsertBinaryAsync。
迁移指南
- 更新
.csproj文件,将包名改为ClickHouse.Driver,并使用 NuGet 上的最新版本。 - 将代码库中所有对
ClickHouse.Client的引用改为ClickHouse.Driver。
支持的 .NET 版本
ClickHouse.Driver 支持以下 .NET 版本:
- .NET 6.0
- .NET 8.0
- .NET 9.0
- .NET 10.0
支持的 ClickHouse 版本
该客户端正式支持最新的 3 个发行版和最新的两个长期支持版。安装
从 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 查询:
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 参数发送该语句,让请求正文只包含数据行:
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>() 保持不变,为保持向后兼容,仍仅用于插入。
注册的类型必须具备:
- 公共无参构造函数。
- 至少一个带有公共且非
initsetter 的公共属性。支持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":
查询 ID
每个查询都会获得一个唯一的query_id,可用它从 system.query_log 表中检索数据,或取消长时间运行的查询。你可以通过 QueryOptions 指定自定义查询 ID:
自定义参数类型映射
使用@ 风格的参数 (例如 WHERE id = @id) 时,驱动程序会根据 .NET 值的类型自动推断对应的 ClickHouse 类型。例如,int 会映射为 Int32。
如需覆盖这些默认行为,请在 ClickHouseClientSettings 中设置 ParameterTypeResolver。例如,可以让所有 DateTime 参数都使用具有毫秒精度的 DateTime64(3),或让所有 Decimal 值都使用指定标度,而无需为每个参数单独设置 ClickHouseType。
使用 DictionaryParameterTypeResolver 进行简单的类型映射:
IParameterTypeResolver:
如需根据参数值或名称解析类型,请直接实现 IParameterTypeResolver 接口。返回 null 即可回退到默认的类型推断逻辑:
QueryOptions.ParameterTypeResolver 为单个查询设置解析器。设置后,该解析器的优先级高于客户端级解析器。
类型解析的优先级顺序:
解析器只是确定参数类型时的一个环节。各项的优先级从高到低依次为:
- 在参数上显式设置的
ClickHouseType - 查询中通过
{name:Type}语法指定的 SQL 类型提示 IParameterTypeResolver(优先使用QueryOptions.ParameterTypeResolver,未设置时使用ClickHouseClientSettings.ParameterTypeResolver)- 内置类型推断 (
TypeConverter.ToClickHouseType)
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) ,驱动程序会给格式化器的输出加上单引号,但不会转义其中的内容。如果返回的字符串包含未转义的单引号或反斜杠,复合字面量就会无效,服务器将拒绝该查询。
顶层字符串参数 (未嵌入复合值中) 会原样使用,不加引号,因此无需转义。
格式化器优先级:
IParameterFormatter(优先使用QueryOptions.ParameterFormatter,未设置时回退到ClickHouseClientSettings.ParameterFormatter) 。如果它返回非 null 值,就使用该值。HttpParameterFormatter内置的按类型格式化逻辑。
null 或 DBNull 值;这些值始终序列化为 ClickHouse 的空值标记 (\N) 。
自定义读取值转换
IReadValueConverter 可在反序列化后转换数据读取器返回的值,而不改变其 CLR 类型。常见用途包括:为不带时区信息的 DateTime 列设置 DateTime.Kind = Utc、修剪或规范化字符串,以及在 JSON 列的值传入应用程序代码前对其进行后处理。
在 ClickHouseClientSettings 中设置 ReadValueConverter,即可对所有读取操作应用转换器。无论走装箱 (GetValue) 还是泛型 (GetFieldValue<T>) 路径,转换器都会对每行的每列调用一次。未设置转换器时,不会产生额外开销——读取器会直接返回值。
使用 DictionaryReadValueConverter 按 CLR 类型进行简单转换:
For<T> 注册的值会原样传递。分派时会精确匹配 CLR 类型,因此请注册读取器实际生成的类型 (例如,对于 JsonReadMode.Binary 模式下的 JSON 列,应注册 For<JsonObject>) 。
适用于高级场景的自定义 IReadValueConverter:
如果需要根据 ClickHouse 端的类型字符串进行分派 (例如,区分 DateTime 和 DateTime('UTC'),两者在 CLR 中呈现为同一种类型) ,请直接实现 IReadValueConverter:
GetFieldType、GetSchemaTable) 不会经过转换器处理,且必须与返回值保持一致。
也可以通过 QueryOptions.ReadValueConverter 为单次查询设置转换器;设置后,它的优先级高于客户端级转换器。
调用边界:
转换器对每列调用一次,接收反序列化后的完整单元格值;它不会递归处理复合容器。对于 Array(Int32) 列,传入的值是 int[];对于 Tuple(Int32, String) 列,传入的值是 ITuple。
调用哪个重载:
两个重载的行为必须一致,因为驱动调用哪个重载取决于调用方读取列的
方式:
ConvertValue<T>— 类型化访问器GetByte、GetSByte、GetInt16/32/64、GetUInt16/32/64、GetFloat、GetDouble、GetGuid、GetDateTime、GetIPAddress、GetBigInteger和GetFieldValue<T>,以及 POCO 读取路径上所有无需装箱的列。ConvertValue(装箱) —GetValue、GetValues、索引器、GetChar、GetTuple,以及GetBoolean、GetDecimal和GetString中执行类型转换的路径。
IsDBNull 完全不会调用转换器:它直接读取空值标志,因此转换器无法
改变一个值是否被视为 null。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 不会在你不知情的情况下被移除,你收到的原始响应正文与服务器发送的完全一致。
错误响应正文
当服务器返回 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 用户,也可以在连接字符串中指定:
enable_http_compression=1。ClickHouse 只有在启用该参数后才会处理这个请求头。即使 UseCompression 为 false 也是如此,因为显式指定编解码器就意味着请求使用压缩。如果未设置该值,UseCompression=false 时则完全不会发送 Accept-Encoding。
Accept-Encoding 可以在四处设置。按以下顺序,首个指定了编解码器的设置生效:
QueryOptions.AcceptEncoding(或ClickHouseCommand.AcceptEncoding)- 查询上的
CustomHeaders["Accept-Encoding"] - 客户端上的
CustomHeaders["Accept-Encoding"] ClickHouseClientSettings.AcceptEncoding,或连接字符串关键字AcceptEncoding
identity。
编解码器由服务器选择,而不是客户端。 ClickHouse 按自身固定的优先级顺序扫描 Accept-Encoding 中的标记:zstd > br > lz4 > snappy > gzip > deflate,既不考虑标记的排列顺序,也不考虑任何 q 值。因此,这个请求头只是声明支持哪些编解码器,并非要求使用某一种;要影响选择,只能从中去掉某些标记。默认列表包含 zstd,因此默认查询的响应使用 zstd;其余标记用于备选。客户端可以解码 br,但默认不会声明支持它。
各编解码器在载荷大小、服务器 CPU 占用和客户端 CPU 占用方面的表现,取决于数据、网络链路以及服务器的 http_zlib_compression_level (随附默认值:3) ——参见压缩调优。
http_zlib_compression_level。 该设置适用于所有 HTTP 编解码器,默认值为 3。应根据数据、链路速度和 CPU 使用情况调整此值。- 快速链路上受 CPU 性能限制的客户端。 驱动程序在调用线程上解码响应正文,因此当网络不是瓶颈时,客户端的解码速度可能成为限制因素。
Content-Encoding 处理响应正文:未设置该字段或其值为 identity 时,保持原样;指定受支持的编解码器时,进行解码;指定其他值时,则会引发错误,并在错误中指出该值。无需担心重复解码——如果调用方提供的处理程序已通过 AutomaticDecompression 解码响应正文,它也会移除 Content-Encoding,因此驱动程序看到的是明文数据,不会再次解码。
原始结果不会声明任何编解码器。 ExecuteRawResultAsync (以及公开的 PostStreamAsync / InsertRawStreamAsync) 会将响应正文原样交给你。因此,除非你明确指定编解码器,否则它们不会请求使用编解码器。驱动程序不会解码这类响应正文;如果默认请求使用编解码器,就可能在不知情的情况下把导出结果变成压缩文件。因此,无论 HttpClient 如何配置,规则都一样:响应正文会按服务器发送时的样子原样返回;除非你请求使用编解码器,否则服务器发送的是明文。 如果想导出压缩后的字节数据,可以在整个客户端或单次查询中明确请求使用编解码器。
显式设置的 AcceptEncoding (无论在哪个级别设置) 仍适用于原始请求。需要解码结果时,可以调用 ClickHouseRawResult.ReadDecompressedStreamAsync();ReadAsStreamAsync、ReadAsByteArrayAsync、ReadAsStringAsync 和 CopyToAsync 则始终按接收时的原样返回数据。
leaveOpen 创建的解码器,释放它不会影响响应;如果响应未经压缩,返回的就是 HTTP 内容流本身,释放该流会结束对响应正文的读取。无论哪种情况,响应都由 ClickHouseRawResult 管理——流释放后,不要再调用它的其他读取成员。始终需要释放 ClickHouseRawResult,且只需释放它即可:它会同时释放响应和此处创建的所有解码器 (解码器持有池化缓冲区) 。因此,上面的 await using 可选,保留也无妨。重复的顺序调用会返回同一个流;此类型不能并发使用。
可参阅 Select_007_ResponseCompression.cs,查看可运行的示例。
插入 (请求) 压缩
Zstd 是插入操作的默认编解码器:InsertOptions.Compressor 的初始值为 ZstdCompressor.Default,
即压缩级别为 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 文件插入:
如需了解控制数据摄取行为的选项,请参阅格式设置文档。
更多示例
更多实际使用示例,请参阅 GitHub 仓库中的示例目录。ADO.NET
该库通过ClickHouseConnection、ClickHouseCommand 和 ClickHouseDataReader 提供完整的 ADO.NET 支持。集成 ORM (Dapper、Linq2db) 或使用标准的 .NET 数据库抽象时,都需要使用此 API。
使用 ClickHouseDataSource 管理生命周期
始终通过ClickHouseDataSource 创建连接,以确保妥善管理生命周期并复用连接池。DataSource 内部维护一个 ClickHouseClient,所有连接共用其 HTTP 连接池。
使用 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对象已释放,连接仍可能保持活动状态。
日期时间处理
-
尽可能使用 UTC。 将时间戳存储在
DateTime('UTC')列中,并在代码中使用DateTimeKind.Utc,以避免时区歧义。 -
使用
DateTimeOffset显式处理时区。 它始终表示一个确定的时间点,并包含时区偏移量信息。 -
在 SQL 类型提示中指定时区。 使用
Unspecified类型的 DateTime 值作为参数写入非 UTC 列时,请在 SQL 中指定时区:
异步插入
异步插入将批处理工作从客户端转移到服务器。客户端无需再进行批处理;服务器会缓冲传入的数据,并在达到可配置的阈值时将其刷写到存储中。这种方式适用于可观测性工作负载等高并发场景,例如大量 agent 持续发送小型载荷的情况。 通过CustomSettings 或连接字符串启用异步插入:
wait_for_async_insert 控制) :
关键设置:
会话
仅在需要使用有状态的服务器端功能时才启用会话,例如:- 临时表 (
CREATE TEMPORARY TABLE) - 在多条语句之间保留查询上下文
- 会话级设置 (
SET max_threads = 4)
支持的数据类型
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。这会原样保留存储的挂钟时间,不会对时区作任何假设。
如果需要对未显式指定时区的列进行带时区信息的处理,可以采用以下任一方式:
- 在列定义中显式指定时区:
DateTime('UTC')或DateTime('Europe/Amsterdam') - 读取后自行指定时区。
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——这两种情况无法区分:
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。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(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
该驱动通过 .NETSystem.Diagnostics.Activity API 原生支持 OpenTelemetry 分布式链路追踪。启用后,驱动会为数据库操作生成 span,并可通过 OpenTelemetry Collector 将其导出到 Jaeger 或 ClickHouse 等可观测性后端。
启用追踪
在 ASP.NET Core 应用中,将 ClickHouse 驱动程序的ActivitySource 添加到 OpenTelemetry 配置中:
Span 属性
每个 span 都包含标准的 OpenTelemetry 数据库属性,以及可用于调试的 ClickHouse 特有的查询统计信息。配置选项
通过ClickHouseDiagnosticsOptions 配置追踪行为:
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。
驱动程序会从共享池中借用缓冲区,并在释放读取器时将其归还,因此无需
每次查询都重新分配。处理大规模结果集时,增大该值可减少
缓冲区重新填充的次数。驱动程序会为每个同时打开的读取器保留一个缓冲区,因此内存占用
会随缓冲区大小和并发读取器数量的增加而增加。
运行时与垃圾回收
对于插入操作密集型应用程序,请启用 Server GC。 在代码相同、分配的字节数也相同的情况下, Workstation GC 的插入操作最多比 Server GC 慢 97%。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 参数传递方式: 匿名对象:DynamicParameters (通过字典或匿名对象创建) :
查询并映射到 POCO
Dapper 按名称将列映射到属性 (不区分大小写) :ClickHouse 原生参数语法
如需显式指定参数类型,请直接在 SQL 中使用 ClickHouse 的{param:Type} 语法,并通过 Dictionary<string, object> 提供参数值。不要对同一个参数混用 @param 和 {param:Type} 两种语法。
WHERE IN
Dapper 原生的 IN 展开功能可正常使用:WHERE id IN (@Ids1, @Ids2, @Ids3),再由驱动程序逐一转换展开后的参数。
ClickHouse 的 has() 函数也支持 Array 参数:
自定义类型处理器
某些 ClickHouse 类型 (如ITuple、BigInteger 和 ClickHouseDecimal) 需要在启动时注册相应的类型处理器:
Dapper.Contrib
GetAll<T>() 和 Get<T>(id) 可正常使用。Insert<T>() 则无法使用,因为它会生成 SQL Server 语法 (SCOPE_IDENTITY、[]) 。建议改用 ClickHouseClient 原生的 InsertBinaryAsync 方法。
限制
Linq2db
此驱动程序兼容 linq2db,后者是适用于 .NET 的轻量级 ORM 和 LINQ 提供程序。详细文档请参阅项目网站。 用法示例: 使用 ClickHouse 提供程序创建DataConnection:
BulkCopyAsync 高效批量插入数据。
Entity Framework Core
ClickHouse 官方提供的 Entity Framework Core 提供程序。使用熟悉的 EF Core 模式,即可将 C# 类映射到 ClickHouse 表、通过 LINQ 查询数据,并通过SaveChanges 插入数据。
- NuGet:
ClickHouse.EntityFrameworkCore - 源代码:GitHub
此提供程序仍在积极开发中。当前版本支持 LINQ 查询 (包括连接、子查询和集合运算) 、通过
SaveChanges / BulkInsertAsync 执行 INSERT、具有完整 DDL 功能 (CREATE / ALTER / DROP) 的迁移,以及 ClickHouse 特有的表引擎配置。暂不支持 UPDATE / DELETE。安装
快速入门
定义实体和DbContext,然后使用 LINQ 查询:
支持的类型
如果需要保留
Decimal128/Decimal256 列的全精度,请使用 ClickHouseDecimal (来自 ClickHouse.Driver.Numerics) ,而不是 decimal;.NET 的 decimal 只能表示 28–29 位有效数字。
支持的 LINQ 操作
查询:Where、OrderBy、Take、Skip、Select、First、Single、Any、All、Count、Distinct、AsNoTracking
GROUP BY 与聚合: GroupBy 可与 Count、LongCount、Sum、Average、Min、Max 配合使用,还支持 HAVING (在 .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) ,可通过以下方式禁用这一行为:
0 / "" 比较,而不要使用 == null。
插入数据
SaveChanges 使用驱动程序原生的 InsertBinaryAsync API,以 RowBinary 格式编码数据并压缩请求正文,效率远高于参数化 SQL:
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() 显式指定:
string、ulong、ulong[]) 。
JSON 列
该提供商支持 ClickHouse 的Json 列类型,默认映射为 System.Text.Json.Nodes.JsonNode,也可通过自动 ValueConverter 映射为 string:
SaveChanges 和 BulkInsertAsync 均可读写 JSON:
string,列类型设为 Json。提供商会自动应用 ValueConverter:
- 不支持 JSON 路径转换 — LINQ 中的
entity.Data["name"]无法转换为 ClickHouse 的data.nameSQL 语法。请按非 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(...) 配置:
迁移
标准 EF Core 迁移流程:迁移限制
除迁移功能外,该提供商目前还不支持:
UPDATE/DELETE- 事务:
BeginTransaction是空操作。ClickHouse 不支持 ACID 事务。 - JSON 路径查询转换:LINQ 中的
entity.Data["key"]无法转换为 ClickHouse 的data.keySQL 语法。请按非 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)
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>。
驱动程序会将第 8 个参数视为 TRest (即将其展平) ,因此最后一个元素为嵌套元组的 8 元素元组会被错误地序列化。
System.Tuple 和 ValueTuple 在元素超过 7 个时都会使用 TRest 嵌套结构,因此都存在这一问题。元素不超过 7 个的元组,以及最后一个元素本身不是元组的元组,则不受影响。
解决方法: 在内部元组外再包一层,使驱动程序能够将其与 TRest 嵌套结构区分开:
AggregateFunction 列
不能直接查询AggregateFunction(...) 类型的列,也不能直接向其中插入数据。
如需插入数据: