Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

ClickHouse C# client

用于连接 ClickHouse 的官方 C# 客户端。 该客户端的源代码可在 GitHub 仓库 中获取。 最初由 Oleg V. Kozlyuk 开发。

该库提供两个主要 API:

  • ClickHouseClient (推荐) :一个高级、线程安全的客户端,适合以单例方式使用。为查询和批量插入提供简洁的异步 API。最适合大多数应用程序。

  • ADO.NET (ClickHouseDataSourceClickHouseConnectionClickHouseCommand) :标准的 .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 安装该包:

dotnet add package ClickHouse.Driver

或者使用 NuGet 包管理器:

Install-Package ClickHouse.Driver

快速入门

using ClickHouse.Driver;

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

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

配置

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

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

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

连接设置

属性 类型 默认值 连接字符串键 描述
Host string "localhost" Host ClickHouse 服务器的主机名或 IP 地址
Port ushort 8123 (HTTP) / 8443 (HTTPS) Port 端口号;默认值取决于协议
Username string "default" Username 身份验证用户名
Password string "" Password 身份验证密码
Database string "" Database 默认数据库;留空时使用服务器或用户的默认值
Protocol string "http" Protocol 连接协议:"http""https"
Path string null Path 用于反向代理场景的 URL 路径 (例如 /clickhouse)
Timeout TimeSpan 2 分钟 Timeout 操作超时时间 (在连接字符串中以秒存储)

数据格式与序列化

属性 类型 默认值 连接字符串键 描述
UseCompression bool true Compression 启用 gzip 压缩进行数据传输
UseCustomDecimals bool true UseCustomDecimals 对任意精度值使用 ClickHouseDecimal;如果为 false,则使用 .NET decimal (128 位限制)
ReadStringsAsByteArrays bool false ReadStringsAsByteArrays StringFixedString 列读取为 byte[],而不是 string;适用于二进制数据
UseFormDataParameters bool false UseFormDataParameters 以表单数据而非 URL 查询字符串发送参数
ReadBufferSize int 8192 (8 KiB) ReadBufferSize 用于读取 HTTP 查询响应的缓冲区大小 (以字节为单位) 。增大此值可减少大型结果集的缓冲区重填次数。达到或超过 85,000 字节的值会分配到 Large Object Heap,这可能导致 LOH 碎片化和更长的 GC 暂停时间。
ParameterTypeResolver IParameterTypeResolver null 用于 @ 风格参数类型映射的自定义解析器;请参见 自定义参数类型映射
ParameterFormatter IParameterFormatter null 用于参数值序列化的自定义格式化器;请参见 自定义参数值格式化
ReadValueConverter IReadValueConverter null 应用于数据读取器返回值的自定义转换器;请参见 自定义读取值转换
JsonReadMode JsonReadMode Binary JsonReadMode JSON 数据的返回方式:Binary (返回 JsonObject) 或 String (返回原始 JSON 字符串)
JsonWriteMode JsonWriteMode String JsonWriteMode JSON 数据的发送方式:String (通过 JsonSerializer 序列化,接受所有输入) 或 Binary (仅支持带有类型提示的已注册 POCO)

会话管理

属性 类型 默认值 连接字符串键 描述
UseSession bool false UseSession 启用有状态会话;请求将按顺序串行执行
SessionId string null SessionId 会话 ID;如果为 null 且 UseSession 为 true,则自动生成 GUID

安全

属性 类型 默认值 连接字符串键 描述
SkipServerCertificateValidation bool false 跳过 HTTPS 证书验证;不可用于生产环境

HTTP 客户端配置

属性 类型 默认值 连接字符串键 描述
HttpClient HttpClient null 自定义的预配置 HttpClient 实例
HttpClientFactory IHttpClientFactory null 用于创建 HttpClient 实例的自定义工厂
HttpClientName string null 供 HttpClientFactory 创建特定客户端时使用的名称

日志与调试

属性 类型 默认值 连接字符串键 描述
LoggerFactory ILoggerFactory null 用于诊断日志的日志记录器工厂
EnableDebugMode bool false 启用 .NET 网络 trace (要求 LoggerFactory 的级别设为 Trace) ;会显著影响性能

自定义设置与角色

属性 类型 默认值 连接字符串键 描述
CustomSettings IDictionary<string, object> set_* 前缀 ClickHouse 服务器设置,详见下方说明
Roles IReadOnlyList<string> Roles 以逗号分隔的 ClickHouse 角色 (例如 Roles=admin,reader)
ApplicationInfo IReadOnlyDictionary<string, string> 追加到 HTTP User-Agent 请求头中的自由格式标签,用于按应用对查询进行归因。

连接字符串示例

基本连接

Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb

使用自定义 ClickHouse 设置

Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000

QueryOptions

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

属性 类型 说明
QueryId string 用于在 system.query_log 中跟踪查询或取消查询的自定义查询标识符
Database string 覆盖此查询的默认数据库
Roles IReadOnlyList<string> 覆盖此查询使用的客户端角色
CustomSettings IDictionary<string, object> 此查询的 ClickHouse 服务器设置 (例如 max_threads)
CustomHeaders IDictionary<string, string> 此查询的附加 HTTP 请求头
UseSession bool? 覆盖此查询的会话行为
SessionId string 此查询的会话 ID (要求 UseSession = true)
BearerToken string 覆盖此查询的身份验证令牌
ParameterTypeResolver IParameterTypeResolver 覆盖客户端级别的 @ 风格参数类型映射解析器;参见 自定义参数类型映射
ParameterFormatter IParameterFormatter 覆盖客户端级别的 @ 风格参数值序列化格式化器;参见 自定义参数值格式化
ReadValueConverter IReadValueConverter 覆盖应用于数据读取器返回值的客户端级别转换器;参见 自定义读取值转换
MaxExecutionTime TimeSpan? 服务器端查询超时 (以 max_execution_time 设置传递) ;超出时,服务器会取消查询
AcceptEncoding string 按查询覆盖 Accept-Encoding 请求头 (例如 "zstd""zstd, gzip;q=0.5");还会在 URL 上强制设置 enable_http_compression=1。参见 按查询传输压缩

示例:

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

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

InsertOptions

InsertOptionsQueryOptions 的基础上增加了通过 InsertBinaryAsync 执行批量插入操作所需的特定设置。

属性 类型 默认值 描述
BatchSize int 100,000 每个批次的行数
MaxDegreeOfParallelism int 1 并行批次上传的数量
Format RowBinaryFormat RowBinary 二进制格式:RowBinaryRowBinaryWithDefaults
ColumnTypes IReadOnlyDictionary<string, string> null 列名 → ClickHouse 类型字符串。设置后会跳过 schema 探测查询。
UseSchemaCache bool false 在客户端生命周期内,按 (数据库、表) 缓存完整的表 schema。

QueryOptions 的所有属性也可用于 InsertOptions

示例:

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

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

跳过 schema 探测查询

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

选项 1:显式提供列类型

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

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

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

选项 2:缓存 schema

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

var options = new InsertOptions { UseSchemaCache = true };

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

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

ClickHouseClient

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

创建客户端

使用连接字符串或 ClickHouseClientSettings object 创建 ClickHouseClient。可用选项请参阅配置部分。

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

选择一个服务并点击 Connect

ClickHouse Cloud 服务连接按钮

选择 C#。连接详细信息会显示在下方。

ClickHouse Cloud C# 连接详细信息

如果你使用的是自管理 ClickHouse,连接详细信息由你的 ClickHouse 管理员提供。

使用连接字符串:

using ClickHouse.Driver;

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

或者使用 ClickHouseClientSettings

using ClickHouse.Driver;

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

对于依赖注入的场景,请使用 IHttpClientFactory

// In your DI configuration
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

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

执行查询

对于不返回结果的语句,使用 ExecuteNonQueryAsync

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

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

使用 ExecuteScalarAsync 获取单个值:

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

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"服务器版本: {version}");

插入数据

参数化插入

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

using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

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

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

批量插入

使用 InsertBinaryAsync 可高效插入大量行。它使用 ClickHouse 原生的行二进制格式以流式方式传输数据,支持并行批次上传,并可避免参数化查询可能导致的 "URL too long" 错误。

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

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

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

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

var options = new InsertOptions
{
    BatchSize = 100_000,           // 每个批次的行数(默认值:100,000)
    MaxDegreeOfParallelism = 4     // 批次并行上传(默认值:1)
};

POCO 插入

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

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

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

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

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

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

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

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

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // 排除在插入操作之外
    public string InternalTag { get; set; }
}
特性 用途
[ClickHouseColumn(Name = "...")] 覆盖目标列名
[ClickHouseColumn(Type = "...")] 显式声明 ClickHouse 类型
[ClickHouseNotMapped] 将该属性排除在插入之外

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

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

schema 演进

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


读取数据

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

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

using ClickHouse.Driver.ADO.Parameters;

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

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

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

POCO 读取

无需按索引或名称读取列,你可以将查询结果直接流式写入自定义类。只需向客户端注册一次该类型,然后使用 QueryAsync<T>

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

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

}

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

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

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

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

  • 具有一个公开的无参构造函数。
  • 至少有一个公开属性,并且该属性具有公开的、非 init 的 setter。支持 required 属性。
列匹配

列匹配区分大小写。缺失的结果列会使属性保持默认值;多出的结果列会被忽略。

不会执行自动转换,类型不匹配时会抛出 InvalidOperationException

物化单行

手动迭代读取器时,可使用 ClickHouseDataReader.MapTo<T>() 将当前行物化为已注册的 POCO,且不会将读取器向前推进:

var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
注册诊断信息

配置了 LoggerFactory 后,RegisterPocoType<T>()RegisterBinaryInsertType<T>() 会输出一条 Debug 级别的日志 (类别为 ClickHouse.Driver.Client) ,列出哪些属性映射到了哪些列,以及哪些属性被跳过和跳过原因。请参阅日志和诊断


SQL 参数

在 ClickHouse 中,SQL 查询中的查询参数标准格式为 {parameter_name:DataType}

示例:

SELECT {value:Array(UInt16)} as a
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})

标识符参数

Identifier 参数类型允许你安全地绑定数据库、表或列名,而不是使用带引号的字符串字面量。可在 SQL 中通过 {name:Identifier} 语法使用,或通过设置 ClickHouseDbParameter.ClickHouseType = "Identifier" 来使用:

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

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

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


查询 ID

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

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

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

自定义参数类型映射

使用 @ 风格的参数时 (例如 WHERE id = @id) ,驱动程序会根据 .NET 值类型自动推断 ClickHouse 类型。例如,int 会映射为 Int32

如需覆盖这些默认映射,请在 ClickHouseClientSettings 上设置 ParameterTypeResolver。例如,如果你希望所有 DateTime 参数都使用具有毫秒精度的 DateTime64(3),或者希望所有 Decimal 参数都使用特定的标度,而不必为每个参数单独设置 ClickHouseType,这会很有用。

使用 DictionaryParameterTypeResolver 进行简单的类型映射:

using ClickHouse.Driver.ADO.Parameters;

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

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

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

用于高级场景的自定义 IParameterTypeResolver

对于按值或按名称进行解析的场景,请直接实现 IParameterTypeResolver 接口。返回 null 以回退到默认推断:

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

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

你也可以通过 QueryOptions.ParameterTypeResolver 为单个查询设置解析器。设置后,它会优先于客户端级别的解析器。

类型解析优先级:

解析器只是这一优先级事件链中的一环。按优先级从高到低依次为:

  1. 在参数上显式设置的 ClickHouseType
  2. 查询中通过 {name:Type} 语法指定的 SQL 类型提示
  3. IParameterTypeResolver (来自 QueryOptions.ParameterTypeResolver,若未设置则回退到 ClickHouseClientSettings.ParameterTypeResolver)
  4. 内置类型推断 (TypeConverter.ToClickHouseType)

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


自定义参数值格式化

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

ClickHouseClientSettings 中设置 ParameterFormatter,即可为所有参数化查询启用格式化器。该格式化器会接收值、已解析的 ClickHouse 类型名称以及参数名,并返回发送到服务器的字符串表示形式。返回 null 则会回退到默认格式化器。

使用 DictionaryParameterFormatter 进行简单的按 CLR 类型格式化:

using ClickHouse.Driver.ADO.Parameters;

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

面向高级场景的自定义 IParameterFormatter

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

你也可以通过 QueryOptions.ParameterFormatter 为每个查询设置格式化器。设置后,它的优先级高于客户端级别的格式化器。

复合值:

该格式化器既会用于顶层集合参数,也会用于复合值 (ArrayTupleMapNullableLowCardinalityVariant) 中的每个元素。例如,typeof(int) 映射会分别格式化 Array(Int32) 中的每个 Int32 元素。

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

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

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

格式化器优先级:

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

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


自定义读取值转换

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

ClickHouseClientSettings 中设置 ReadValueConverter,即可为所有读取操作启用转换器。该转换器会通过装箱 (GetValue) 和泛型 (GetFieldValue<T>) 两种路径,对每一行的每一列各调用一次。未设置转换器时,没有任何额外开销——读取器会直接返回值。

使用 DictionaryReadValueConverter 进行简单的按 CLR 类型转换:

using ClickHouse.Driver.ADO.Readers;

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

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

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

用于高级场景的自定义 IReadValueConverter

如果你需要根据 ClickHouse 侧的类型字符串进行分派 (例如,区分 DateTimeDateTime('UTC')——两者在 CLR 中都会显示为相同的类型) ,请直接实现 IReadValueConverter

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

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

转换器必须保留运行时 CLR 类型;列元数据 (GetFieldTypeGetSchemaTable) 不会通过它改写,且必须与实际返回的内容保持一致。

你也可以通过 QueryOptions.ReadValueConverter 为每个查询设置转换器;设置后,它的优先级高于客户端级别的转换器。

分派边界:

转换器对每一列只会以整个反序列化后的单元格值调用一次,不会递归处理复合容器。对于 Array(Int32) 列,传入的值是 int[];对于 Tuple(Int32, String),则是 ITuple

该转换器适用于 ADO.NET ClickHouseConnection 路径——从客户端创建的连接会继承这些设置。


原始流式传输

使用 ExecuteRawResultAsync 可按特定 格式 直接流式传输查询结果,绕过数据读取器。这对于将数据导出到文件或传输到其他系统特别有用:

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

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

常见格式:JSONEachRowCSVTSVParquetNative。所有选项请参阅格式文档


按查询设置传输压缩

默认情况下,当 Compression=true (connection-string 的默认值) 时,client 会协商使用 gzip, deflate,HTTP client 会透明地解压 stream。

对于原始导出 (例如 Parquet、Arrow、Native) ,你可能希望协商使用其他 codec (例如 zstdlz4) ,以便在不更改整个 connection 设置的情况下,用 CPU 换取 bandwidth。QueryOptions.AcceptEncodingClickHouseCommand.AcceptEncoding 可为单个请求设置 HTTP Accept-Encoding 请求头,替换原先附加的默认值,并强制在 URL 上设置 enable_http_compression=1 (ClickHouse 要求先设置该参数,才会接受 Accept-Encoding) 。

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

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

HttpClient 配置

驱动程序构建的默认 HttpClient 使用 AutomaticDecompression = GZip | Deflate,会自动解压这些算法对应的内容,并从响应中移除 Content-Encoding。这对于普通查询正合适,但如果你想自己处理原始压缩数据,就不适用了。

在这种情况下,请向 ClickHouseClient 传入一个 HttpClientHttpClientFactory,并将 AutomaticDecompression 设置为 DecompressionMethods.None

错误响应体

当服务器返回 4xx/5xx,且设置了 enable_http_compression=1 时,它会使用与成功响应相同的 codec 来压缩错误响应体。对于 BCL 自带的编解码器 (gzipdeflatebr/brotli) ,驱动程序会自动解压,因此 ClickHouseServerException 中显示的消息是可读的。对于无法解码的编解码器 (zstdlz4、……) ,驱动程序会返回一条占位消息,其中会标明 codec,并提示到 system.query_log 中查看原始错误文本。


原始流插入

使用 InsertRawStreamAsync 可直接从文件或内存流插入数据,支持 CSV、JSON、Parquet 等格式,以及任何ClickHouse 支持的格式

从 CSV 文件插入:

await using var fileStream = File.OpenRead("data.csv");

using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: fileStream,
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);

更多示例

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

ADO.NET

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

使用 ClickHouseDataSource 管理生命周期

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

using ClickHouse.Driver.ADO;

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

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

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

使用依赖注入时:

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

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

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

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

使用 ClickHouseCommand

通过连接创建命令来执行 SQL:

await using var connection = await dataSource.OpenConnectionAsync();

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

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

命令方法:

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

使用 ClickHouseDataReader

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

await using var reader = await command.ExecuteReaderAsync();

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

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

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

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

最佳实践

连接生命周期与连接池

ClickHouse.Driver 底层使用 System.Net.Http.HttpClientHttpClient 会为每个端点维护一个连接池。因此:

  • 数据库会话会通过连接池管理的 HTTP 连接进行多路复用。
  • HTTP 连接会由连接池自动复用和回收。
  • 即使 ClickHouseClientClickHouseConnection 对象已释放,连接仍可能继续保持活动状态。

推荐做法:

场景 推荐方法
一般使用 使用单例 ClickHouseClient
ADO.NET / ORMs 使用 ClickHouseDataSource (创建共享同一连接池的连接)
DI 环境 结合 IHttpClientFactory,将 ClickHouseClientClickHouseDataSource 注册为单例

DateTime 处理

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

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

  3. 在 SQL 类型提示中指定时区。 当参数中使用 Unspecified 的 DateTime 值,且目标列不是 UTC 时,请在 SQL 中包含时区信息:

    var parameters = new ClickHouseParameterCollection();
    parameters.AddParameter("dt", myDateTime);
    
    await client.ExecuteNonQueryAsync(
        "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
        parameters
    );

异步插入

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

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

// 使用 CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // 推荐:等待 flush 确认

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

两种模式 (由 wait_for_async_insert 控制) :

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

关键设置:

Setting Description
async_insert_max_data_size 当缓冲区达到此大小 (字节) 时刷新
async_insert_busy_timeout_ms 在此超时时间 (毫秒) 后刷新
async_insert_max_query_number 累积到这么多查询后刷新

会话

仅在需要有状态的服务器端功能时才启用会话,例如:

  • 临时表 (CREATE TEMPORARY TABLE)
  • 在多条语句之间保持查询上下文
  • 会话级设置 (SET max_threads = 4)

启用会话后,请求会按顺序串行处理,以防止同一会话被并发使用。对于不需要会话状态的 workloads,这会带来额外开销。

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // 可选——如果未提供,则会自动生成
};

using var client = new ClickHouseClient(settings);

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

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

使用 ADO.NET (兼容 ORM) :

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

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

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

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

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

支持的数据类型

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

类型映射:从 ClickHouse 读取

整型

ClickHouse 类型 .NET 类型
Int8 sbyte
UInt8 byte
Int16 short
UInt16 ushort
Int32 int
UInt32 uint
Int64 long
UInt64 ulong
Int128 BigInteger
UInt128 BigInteger
Int256 BigInteger
UInt256 BigInteger

浮点类型

ClickHouse 类型 .NET 类型
Float32 float
Float64 double
BFloat16 float

Decimal 类型

ClickHouse 类型 .NET 类型
Decimal(P, S) decimal / ClickHouseDecimal
Decimal32(S) decimal / ClickHouseDecimal
Decimal64(S) decimal / ClickHouseDecimal
Decimal128(S) decimal / ClickHouseDecimal
Decimal256(S) decimal / ClickHouseDecimal

布尔类型

ClickHouse 类型 .NET 类型
Bool bool

String 类型

ClickHouse 类型 .NET 类型
String string
FixedString(N) string

日期和时间类型

ClickHouse 类型 .NET 类型
Date DateTime
Date32 DateTime
DateTime DateTime
DateTime32 DateTime
DateTime64 DateTime
Time TimeSpan
Time64 TimeSpan

ClickHouse 在内部将 DateTimeDateTime64 值存储为 Unix 时间戳 (即自纪元以来的秒或亚秒单位) 。虽然存储始终采用 UTC,但列可以关联一个时区,这会影响值的显示和解析方式。

读取 DateTime 值时,DateTime.Kind 属性会根据列的时区进行设置:

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

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

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

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

对于没有显式指定时区的列 (即 DateTime,而不是 DateTime('Europe/Amsterdam')) ,驱动程序 会返回一个 Kind=UnspecifiedDateTime。这样可以原样保留存储的挂钟时间,而不对时区作任何假定。

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

  1. 在列定义中显式指定时区:DateTime('UTC')DateTime('Europe/Amsterdam')
  2. 读取后自行应用时区。

JSON 类型

ClickHouse 类型 .NET 类型 备注
Json JsonObject 默认 (JsonReadMode=Binary)
Json string JsonReadMode=String

JSON 列的返回类型由 JsonReadMode 设置控制:

  • Binary (默认) :返回 System.Text.Json.Nodes.JsonObject。可对 JSON 数据进行结构化访问,但专用的 ClickHouse 类型 (如 IP 地址、UUID、较大精度的 Decimal) 会在 JSON 结构中转换为字符串表示形式。

  • String:以 string 形式返回原始 JSON。保留 ClickHouse 中 JSON 的精确表示形式,这在你需要不经解析直接传递 JSON,或想自行处理反序列化时非常有用。

// 通过 settings 配置字符串模式
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// 或通过连接字符串
// "Host=localhost;JsonReadMode=String"

其他类型

ClickHouse 类型 .NET 类型
UUID Guid
IPv4 IPAddress
IPv6 IPAddress
Nothing DBNull
Dynamic 见注释
Array(T) T[] (嵌套的 Array(Array(T)) 会读取为交错数组 T[][];使用 reader.GetFieldValue<T[,]>(ordinal) 将矩形数据具体化为多维 CLR 数组)
Tuple(T1, T2, …) Tuple<T1, T2, ...> / LargeTuple
Map(K, V) Dictionary<K, V>
Nullable(T) T?
Enum8 string
Enum16 string
LowCardinality(T) 与 T 相同
SimpleAggregateFunction 与其底层类型相同
Nested(…) Tuple[]
Variant(T1, T2, …) 见注释
QBit(T, dimension) T[]

几何类型

ClickHouse 类型 .NET 类型
Point Tuple<double, double>
Ring Tuple<double, double>[]
LineString Tuple<double, double>[]
Polygon Ring[]
MultiLineString LineString[]
MultiPolygon Polygon[]
Geometry 见说明

类型映射:写入 ClickHouse

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

整数类型

ClickHouse 类型 可接受的 .NET 类型 备注
Int8 sbyte,任何与 Convert.ToSByte() 兼容的类型
UInt8 byte,任何与 Convert.ToByte() 兼容的类型
Int16 short,任何与 Convert.ToInt16() 兼容的类型
UInt16 ushort,任何与 Convert.ToUInt16() 兼容的类型
Int32 int,任何与 Convert.ToInt32() 兼容的类型
UInt32 uint,任何与 Convert.ToUInt32() 兼容的类型
Int64 long,任何与 Convert.ToInt64() 兼容的类型
UInt64 ulong,任何与 Convert.ToUInt64() 兼容的类型
Int128 BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型
UInt128 BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型
Int256 BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型
UInt256 BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型

浮点类型

ClickHouse 类型 可接受的 .NET 类型 说明
Float32 float,以及任何与 Convert.ToSingle() 兼容的类型
Float64 double,以及任何与 Convert.ToDouble() 兼容的类型
BFloat16 float,以及任何与 Convert.ToSingle() 兼容的类型 截断为 16 位 bfloat 格式

布尔类型

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

String 类型

ClickHouse 类型 可接受的 .NET 类型 说明
String string, byte[], ReadOnlyMemory<byte>, Stream 二进制类型会直接写入;流可以支持寻道,也可以不支持寻道
FixedString(N) string, byte[], ReadOnlyMemory<byte>, Stream String 会按 UTF-8 编码并进行填充;二进制类型必须恰好为 N 字节

日期和时间类型

ClickHouse 类型 可接受的 .NET 类型 说明
Date DateTime, DateTimeOffset, DateOnly, NodaTime 类型 转换为 Unix 天数,存储为 UInt16;支持范围为 [1970-01-01, 2149-06-06]
Date32 DateTime, DateTimeOffset, DateOnly, NodaTime 类型 转换为 Unix 天数,存储为 Int32;支持范围为 [1900-01-01, 2299-12-31]
DateTime DateTime, DateTimeOffset, DateOnly, NodaTime 类型 详见下文;支持范围为 [1970-01-01, 2106-02-07 06:28:15] UTC
DateTime32 DateTime, DateTimeOffset, DateOnly, NodaTime 类型 与 DateTime 相同
DateTime64 DateTime, DateTimeOffset, DateOnly, NodaTime 类型 精度取决于 scale 参数
Time TimeSpan, int 限制为 ±999:59:59;int 视为秒数
Time64 TimeSpan, decimal, double, float, int, long, string 字符串按 [-]HHH:MM:SS[.fraction] 格式解析;限制为 ±999:59:59.999999999

驱动在写入值时会遵循 DateTime.Kind

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

DateTimeOffset 值始终保留精确时刻。

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

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

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

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

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

HTTP 参数与批量复制

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

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

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

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

// 错误:未指定时区提示,将被解释为 UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// 字符串值 "2024-01-15 14:30:00" 被解释为 UTC,而非阿姆斯特丹时间!
DateTime.Kind 目标列 HTTP 参数 (带 tz 提示) HTTP 参数 (无 tz 提示) 批量复制
Utc UTC 保持同一时刻 保持同一时刻 保持同一时刻
Utc Europe/Amsterdam 保持同一时刻 保持同一时刻 保持同一时刻
Local 任意 保持同一时刻 保持同一时刻 保持同一时刻
Unspecified UTC 按 UTC 处理 按 UTC 处理 按 UTC 处理
Unspecified Europe/Amsterdam 按阿姆斯特丹时间处理 按 UTC 处理 按阿姆斯特丹时间处理

Decimal 类型

ClickHouse 类型 可接受的 .NET 类型 说明
Decimal(P,S) decimalClickHouseDecimal,以及任何与 Convert.ToDecimal() 兼容的类型 超出精度时会抛出 OverflowException
Decimal32 decimalClickHouseDecimal,以及任何与 Convert.ToDecimal() 兼容的类型 最大精度为 9
Decimal64 decimalClickHouseDecimal,以及任何与 Convert.ToDecimal() 兼容的类型 最大精度为 18
Decimal128 decimalClickHouseDecimal,以及任何与 Convert.ToDecimal() 兼容的类型 最大精度为 38
Decimal256 decimalClickHouseDecimal,以及任何与 Convert.ToDecimal() 兼容的类型 最大精度为 76

JSON 类型

ClickHouse 类型 可接受的 .NET 类型 说明
Json stringJsonObjectJsonNode、任意对象 行为取决于 JsonWriteMode 设置

写入 JSON 时的行为由 JsonWriteMode 设置控制:

输入类型 JsonWriteMode.String (默认) JsonWriteMode.Binary
string 直接传递 抛出 ArgumentException
JsonObject 通过 ToJsonString() 序列化 抛出 ArgumentException
JsonNode 通过 ToJsonString() 序列化 抛出 ArgumentException
已注册的 POCO 通过 JsonSerializer.Serialize() 序列化 使用类型提示进行二进制编码,支持自定义路径属性
未注册的 POCO / 匿名对象 通过 JsonSerializer.Serialize() 序列化 抛出 ClickHouseJsonSerializationException
  • String (默认) :接受 stringJsonObjectJsonNode 或任意对象。所有输入都会通过 System.Text.Json.JsonSerializer 序列化,并作为 JSON 字符串发送到服务端解析。这是最灵活的模式,无需注册类型即可使用。

  • Binary:仅接受已注册的 POCO 类型。数据会在客户端转换为 ClickHouse 的二进制 JSON 格式,并完整支持类型提示。使用前需要调用 connection.RegisterJsonSerializationType<T>()。在此模式下写入 stringJsonNode 值会抛出 ArgumentException

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

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

当 JSON 列带有类型提示 (例如 JSON(id UInt64, price Decimal128(2))) 时,驱动程序会利用这些提示对值进行序列化,从而完整保留类型信息。这样可以保留 UInt64DecimalUUIDDateTime64 等类型的精度,否则它们在按通用 JSON 序列化时可能会损失精度。

POCO 序列化

根据 JsonWriteMode,可通过两种方式将 POCO 写入 JSON 列:

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

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

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

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

CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
using ClickHouse.Driver.Json;

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

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

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // 不会被序列化
}

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

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

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

限制 (仅 Binary 模式) :

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

其他类型

ClickHouse 类型 可接受的 .NET 类型 说明
UUID Guid, string string 会被解析为 Guid
IPv4 IPAddress, string 必须是 IPv4;string 通过 IPAddress.Parse() 解析
IPv6 IPAddress, string 必须是 IPv6;string 通过 IPAddress.Parse() 解析
Nothing Any 不写入任何内容 (空操作)
Dynamic 不支持 (抛出 NotImplementedException)
Array(T) IList, null null 会写入为空数组。对于嵌套类型 (Array(Array(T)) 及更深层级) ,既接受锯齿形结构 (T[][], List<List<T>>) ,也接受矩形多维 CLR 数组 (T[,], T[,,], …) ;CLR 的维数必须与 ClickHouse 的嵌套深度匹配。
Tuple(T1, T2, …) ITuple, IList 元素数量必须与 Tuple 元数一致。对于超过 7 个元素的情况,请参见 ValueTuple 注意事项
Map(K, V) IDictionary
Nullable(T) null, DBNull, or T 可接受的类型 会在值之前写入 null 标志字节
Enum8 string, sbyte, 数值类型 string 会在枚举字典中查找
Enum16 string, short, 数值类型 string 会在枚举字典中查找
LowCardinality(T) T 可接受的类型 委托给底层类型处理
SimpleAggregateFunction 底层类型可接受的类型 委托给底层类型处理
Nested(…) tuple 的 IList 元素数量必须与字段数量一致
Variant(T1, T2, …) 匹配 T1、T2、… 之一的值 如果没有匹配的类型,则抛出 ArgumentException
QBit(T, dim) IList 委托给 Array;dimension 仅作为元数据

几何类型

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

不支持写入的类型

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

嵌套类型处理

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

CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

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

日志与诊断

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

快速入门

using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

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

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

using var client = new ClickHouseClient(settings);

使用 appsettings.json

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

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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

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

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

using var client = new ClickHouseClient(settings);

使用内存中的配置

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

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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

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

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

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

using var client = new ClickHouseClient(settings);

类别和发出方

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

类别 来源 亮点
ClickHouse.Driver.Connection ClickHouseConnection 连接生命周期、HTTP 客户端工厂选择、连接打开/关闭、会话管理。
ClickHouse.Driver.Command ClickHouseCommand 查询执行开始/完成、耗时、查询 ID、服务器统计信息以及错误详情。
ClickHouse.Driver.Transport ClickHouseConnection 底层 HTTP 流式请求、压缩标志、响应状态码以及传输失败。
ClickHouse.Driver.Client ClickHouseClient 二进制插入、查询及其他操作
ClickHouse.Driver.NetTrace TraceHelper 网络跟踪,仅在启用调试模式时生效

示例:排查连接问题

{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}

这将记录:

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

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

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

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

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

OpenTelemetry

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

启用链路追踪

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

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

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

using OpenTelemetry;
using OpenTelemetry.Trace;

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

Span 属性

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

属性 说明
db.system 始终为 "clickhouse"
db.name 数据库名称
db.user 用户名
db.statement SQL 查询 (如果已启用)
db.clickhouse.read_rows 查询读取的行数
db.clickhouse.read_bytes 查询读取的字节数
db.clickhouse.written_rows 查询写入的行数
db.clickhouse.written_bytes 查询写入的字节数
db.clickhouse.elapsed_ns 服务器端执行时间 (以纳秒为单位)

配置选项

通过 ClickHouseDiagnosticsOptions 控制链路追踪行为:

using ClickHouse.Driver.Diagnostic;

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

// 截断过长的 SQL 语句(默认值:1000 个字符)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;

TLS 配置

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

自定义证书验证

对于需要自定义证书验证逻辑的生产环境,请提供您自己的 HttpClient,并配置 ServerCertificateCustomValidationCallback 处理程序:

using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // 启用压缩时必须设置(默认已启用)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // 示例:接受具有特定指纹的证书
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // 示例:接受由特定签发方签发的证书
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // 默认:使用标准验证
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

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

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

using var client = new ClickHouseClient(settings);

ORM 支持

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

// 以单例方式注册 DataSource
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// 创建供 ORM 使用的连接
await using var connection = await dataSource.OpenConnectionAsync();
// 将连接传递给 ORM...

Dapper

ClickHouse.Driver 可与 Dapper 配合使用。该驱动程序会自动将 Dapper 的 @parameter 语法转换为 ClickHouse 的原生 {parameter:Type} 语法,并根据 .NET 值推断类型。

使用 ClickHouseDataSource 以正确管理连接的生命周期:

var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // 在 DI 中注册为单例服务

using var connection = dataSource.CreateConnection();

参数传递方式

支持 Dapper 的所有标准参数传递方式:

匿名对象:

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

POCO 类:

class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

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

字典:

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

DynamicParameters (来自字典或匿名对象) :

var dynParams = new DynamicParameters(new { Id = 1 });
// 或:new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

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

将查询结果映射到 POCO

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

class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

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

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

ClickHouse 原生参数语法

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

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

WHERE IN

Dapper 原生支持 IN 展开:

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

Dapper 会将其重写为 WHERE id IN (@Ids1, @Ids2, @Ids3),驱动程序随后会转换每个展开后的参数。

ClickHouse 的 has() 也支持配合 Array 参数使用:

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

自定义类型处理器

某些 ClickHouse 类型 (如 ITupleBigIntegerClickHouseDecimal) 需要在启动时注册相应的处理器:

// ClickHouseDecimal(适用于 Decimal64/128/256 列)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

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

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

有关类型处理程序实现的示例,请参见 Dapper 示例

Dapper.Contrib

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

[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

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

属性名称必须与 ClickHouse 列名完全一致 (区分大小写) 。

局限性

项目 状态 详情
Tuple 作为结果 可用 需要注册 SqlMapper.TypeHandler<ITuple>
Tuple 作为参数 不支持 Dapper 无法将 ITuple/Tuple<> 序列化为 DbParameter 的值
嵌套类型作为参数 不支持 原因相同——Dapper 会拒绝将复杂类型用作参数值
Geo 类型作为参数 不支持 Point、Ring、Polygon、LineString、MultiLineString、MultiPolygon
Dapper.Contrib.Insert<T>() 不支持 会生成 SQL Server 专用语法
Nothing 类型 不支持 没有对应的 .NET 有效表示

Linq2db

此驱动与 linq2db 兼容;后者是适用于 .NET 的轻量级 ORM 和 LINQ 提供商。详细文档请参见项目网站。

示例用法:

使用 ClickHouse 提供商创建 DataConnection

using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

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

await using var db = new DataConnection(options);

表映射可以通过特性或 Fluent API 配置来定义。如果类名和属性名与表名和列名完全一致,则无需配置:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

查询:

await using var db = new DataConnection(options);

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

批量复制:

使用 BulkCopyAsync 可高效执行批量插入。

await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

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

await table.BulkCopyAsync(options, products);

Entity Framework Core

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

安装

dotnet add package ClickHouse.EntityFrameworkCore

需要 .NET 10.0 和 EF Core 10。

快速入门

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

using Microsoft.EntityFrameworkCore;

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

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

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

// 查询
await using var ctx = new AnalyticsContext();

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

支持的类型

类别 ClickHouse 类型 CLR 类型
整数 Int8Int64, UInt8UInt64 sbyte, short, int, long, byte, ushort, uint, ulong
大整数 Int128, Int256, UInt128, UInt256 BigInteger
浮点数 Float32, Float64, BFloat16 float, double
Decimal Decimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S) decimalClickHouseDecimal
Bool Bool bool
String String, FixedString(N) string
枚举 Enum8(...), Enum16(...) string 或 C# enum
日期/时间 Date, Date32, DateTime, DateTime64(P, 'TZ') DateOnly, DateTime
Time Time, Time64(N) TimeSpan
UUID UUID Guid
Network IPv4, IPv6 IPAddress
数组 Array(T) T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
Map Map(K, V) Dictionary<K,V>
Tuple Tuple(T1, ...) Tuple<...>ValueTuple<...>
Variant Variant(T1, T2, ...) object
动态 Dynamic object
JSON Json JsonNodestring
地理空间 Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon, Geometry Tuple<double,double> 及其数组;Geometry 使用 object
包装类型 Nullable(T), LowCardinality(T) 自动解包

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

支持的 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 / INAny / EXISTSAll,以及投影中的标量子查询。

集合操作: Concat (→ UNION ALL) 、Union (→ UNION DISTINCT) 、IntersectExcept

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

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

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

LEFT JOIN 的 NULL 语义

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

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

optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());

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

插入数据

SaveChanges 使用驱动程序提供的原生 InsertBinaryAsync API——采用 RowBinary 编码和 GZip 压缩,相比参数化 SQL 效率高得多:

await using var ctx = new AnalyticsContext();

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

await ctx.SaveChangesAsync();

实体在保存后会从 Added 状态变为 Unchanged,与其他 EF Core 提供商一致。

批次大小可配置 (默认值为 1000) :

optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));

批量插入

对于高吞吐量的数据加载,请使用 BulkInsertAsync 而不是 SaveChanges。这是 DbContext 上的一个扩展方法,会完全绕过 EF Core 的更改跟踪、标识解析和状态管理,转而直接调用驱动的 InsertBinaryAsync,并使用 RowBinary 编码和 GZip 压缩。

因此,它非常适合加载大型数据集,尤其是在插入后不需要跟踪实体的场景下:

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

long rowsInserted = await ctx.BulkInsertAsync(events);

输入可以是任意 IEnumerable<T>——它会以流式方式处理这些实体,无需将它们全部加载到内存中。返回值为插入的行数。插入后,实体不会附加到 DbContext,因此不会发生 AddedUnchanged 状态转换。

枚举

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

public enum Status { Active, Inactive, Pending }

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

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

自定义类型转换

EF Core 的 ValueConverter 系统允许你将自定义类型映射到提供商已支持的类型。提供商不会直接看到你的自定义类型——EF Core 会在边界处完成转换。

针对单个属性的转换:

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

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

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

可重用的转换器类:

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

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

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

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

列类型注解

对于 stringintDateTime 等标量类型,提供商会自动推断出 ClickHouse 类型。对于参数化类型和包装类型,则需要显式指定 ClickHouse 类型。

使用数据注解 (attribute) :

using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

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

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

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

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

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

OnModelCreating 中使用 Fluent API:

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

支持 Array(Nullable(Int32))LowCardinality(Nullable(String)) 这类嵌套包装类型——提供程序会在每一层嵌套中自动解开 NullableLowCardinality

Variant 和 Dynamic 列

ClickHouse Variant(T1, T2, ...)Dynamic 列在 .NET 中会映射为 object。由于 object 过于宽泛,无法自动推断类型,因此必须通过 .HasColumnType() 显式声明存储类型:

public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

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

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

JSON 列

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

using System.Text.Json.Nodes;

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

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

JSON 的读取和写入既可通过 SaveChanges,也可通过 BulkInsertAsync 完成:

ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"

如果你更喜欢原始 JSON 字符串,可将该属性映射为 string,并将列类型设为 Json——提供程序会自动应用 ValueConverter

public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // 原始 JSON 字符串
}

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

表引擎

通过 ToTable(name, t => ...) 流式 API 配置 ClickHouse 表引擎及引擎特定子句。若未配置引擎,提供商默认使用 MergeTree,并根据实体的主键确定 ORDER BY

modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});

支持的引擎系列:

Engine 流式方法 说明
MergeTree HasMergeTreeEngine() 未配置时默认使用
ReplacingMergeTree HasReplacingMergeTreeEngine("Version", "IsDeleted")HasReplacingMergeTreeEngine<T>(e => e.Version) Version / IsDeleted 列为可选
SummingMergeTree HasSummingMergeTreeEngine(…)HasSummingMergeTreeEngine<T>(e => new { … }) 可选求和列
AggregatingMergeTree HasAggregatingMergeTreeEngine()
CollapsingMergeTree HasCollapsingMergeTreeEngine("Sign")HasCollapsingMergeTreeEngine<T>(e => e.Sign) Sign 列必须为 Int8
VersionedCollapsingMergeTree HasVersionedCollapsingMergeTreeEngine("Sign", "Version")<T>(e => e.Sign, e => e.Version)
GraphiteMergeTree HasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, Memory HasLogEngine(), HasTinyLogEngine(), HasStripeLogEngine(), HasMemoryEngine() 不支持 ORDER BY / PARTITION BY

引擎子句: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings。它们都会附加到 HasXxxEngine() 返回的引擎构建器上。

列级功能: HasCodec, HasTtl, HasComment, HasDefault —— 都会纳入迁移。

数据跳过索引 —— 通过 HasIndex(...).HasSkippingIndexType(...)

modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// 带参数的索引(如 bloom_filter、tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);

普通 (非跳过型) 索引会被静默忽略,因为 ClickHouse 没有对应的实现。唯一索引则会抛出异常,因为 ClickHouse 不强制保证唯一性。

迁移

EF Core 的标准迁移工作流:

dotnet ef migrations add InitialCreate
dotnet ef database update

支持的操作:

Operation Emits
CREATE TABLE 包括引擎子句、ORDER BY、PARTITION BY、SETTINGS、列编解码器/生存时间 (TTL)/注释/默认值
ALTER TABLE ADD COLUMN
ALTER TABLE DROP COLUMN
ALTER TABLE MODIFY COLUMN 处理类型变更,以及注解 (CODEC、TTL、COMMENT、DEFAULT) 的添加/删除
ALTER TABLE RENAME COLUMN
RENAME TABLE
ALTER TABLE ADD INDEX / DROP INDEX 仅限数据跳过索引
CREATE DATABASE / DROP DATABASE 通过 EnsureCreated / EnsureDeleted 及迁移

迁移限制

特性 原因
外键 ClickHouse 不会强制执行外键。迁移会拒绝 AddForeignKey;模型验证器会在构建模型时发出警告。
唯一约束 / 唯一索引 ClickHouse 不保证唯一性。唯一索引会在迁移时抛出错误。
服务器生成的值 (自增 / IDENTITY) ClickHouse 没有等效机制。
Nested(…) 尚不支持将其映射为 CLR 类型。
作为 JSON 的拥有实体 (.ToJson()) 尚未实现拥有实体的结构化 JSON 映射。请改为在 Json 列上使用 JsonNode / string (参见 JSON 列) 。

除迁移外,该提供商目前还不支持:

  • 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.TupleValueTuple,因为两者在元素数量 >7 时都会使用 TRest 嵌套。元素不超过 7 个的元组,或最后一个元素本身不是元组的元组,则不受影响。

解决方法: 在内部元组外再包一层,这样驱动程序就能将其与 TRest 嵌套区分开来:

// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))

AggregateFunction 列

无法直接查询或插入 AggregateFunction(...) 类型的列。

如需插入:

INSERT INTO t VALUES (uniqState(1));

要进行查询:

SELECT uniqMerge(c) FROM t;

Navigation