跳转到主要内容
用于连接 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

安装

通过 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:**强类型配置对象,可从配置文件中加载,也可在代码中设置。
下面列出了所有设置、它们的默认值及其作用。

连接设置

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

数据格式与序列化

属性类型默认值连接字符串键描述
UseCompressionbooltrueCompression启用 gzip 压缩进行数据传输
UseCustomDecimalsbooltrueUseCustomDecimals对任意精度值使用 ClickHouseDecimal;如果为 false,则使用 .NET decimal (128 位限制)
ReadStringsAsByteArraysboolfalseReadStringsAsByteArraysStringFixedString 列读取为 byte[],而不是 string;适用于二进制数据
UseFormDataParametersboolfalseUseFormDataParameters以表单数据而非 URL 查询字符串发送参数
ParameterTypeResolverIParameterTypeResolvernull用于 @ 风格参数类型映射的自定义解析器;请参见 自定义参数类型映射
JsonReadModeJsonReadModeBinaryJsonReadModeJSON 数据的返回方式:Binary (返回 JsonObject) 或 String (返回原始 JSON 字符串)
JsonWriteModeJsonWriteModeStringJsonWriteModeJSON 数据的发送方式:String (通过 JsonSerializer 序列化,接受所有输入) 或 Binary (仅支持带有类型提示的已注册 POCO)

会话管理

属性类型默认值连接字符串键描述
UseSessionboolfalseUseSession启用有状态会话;请求将按顺序串行执行
SessionIdstringnullSessionId会话 ID;如果为 null 且 UseSession 为 true,则自动生成 GUID
UseSession 标志会启用服务器会话持久化,从而可以使用 SET 语句和临时表。会话在 60 秒无活动后会被重置 (默认超时) 。可通过 ClickHouse 语句或服务器配置中的会话设置来延长会话生命周期。ClickHouseConnection 类通常支持并行操作 (多个线程可并发运行查询) 。但启用 UseSession 标志后,任意时刻每个连接只允许有一个活动查询 (这是服务器端限制) 。

安全

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

HTTP 客户端配置

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

日志与调试

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

自定义设置与角色

属性类型默认值连接字符串键描述
CustomSettingsIDictionary<string, object>set_* 前缀ClickHouse 服务器设置,详见下方说明
RolesIReadOnlyList<string>Roles以逗号分隔的 ClickHouse 角色 (例如 Roles=admin,reader)
使用连接字符串设置自定义设置时,请使用 set_ 前缀,例如 set_max_threads=4。使用 ClickHouseClientSettings 对象时,不要使用 set_ 前缀。有关可用设置的完整列表,请参见此处

连接字符串示例

基本连接

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 允许你按查询覆盖客户端级别的设置。所有属性均为可选,只有在指定时才会覆盖客户端默认值。
属性类型说明
QueryIdstring用于在 system.query_log 中跟踪查询或取消查询的自定义查询标识符
Databasestring覆盖此查询的默认数据库
RolesIReadOnlyList<string>覆盖此查询使用的客户端角色
CustomSettingsIDictionary<string, object>此查询的 ClickHouse 服务器设置 (例如 max_threads)
CustomHeadersIDictionary<string, string>此查询的附加 HTTP 请求头
UseSessionbool?覆盖此查询的会话行为
SessionIdstring此查询的会话 ID (要求 UseSession = true)
BearerTokenstring覆盖此查询的身份验证令牌
ParameterTypeResolverIParameterTypeResolver覆盖客户端级别的 @ 风格参数类型映射解析器;参见 自定义参数类型映射
MaxExecutionTimeTimeSpan?服务器端查询超时 (以 max_execution_time 设置传递) ;超出时,服务器会取消查询
示例:
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 执行批量插入操作所需的特定设置。
属性类型默认值描述
BatchSizeint100,000每个批次的行数
MaxDegreeOfParallelismint1并行批次上传的数量
FormatRowBinaryFormatRowBinary二进制格式:RowBinaryRowBinaryWithDefaults
ColumnTypesIReadOnlyDictionary<string, string>null列名 → ClickHouse 类型字符串。设置后会跳过 schema 探测查询。
UseSchemaCacheboolfalse在客户端生命周期内,按 (数据库、表) 缓存完整的表 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);
  • ColumnTypes 的优先级高于 UseSchemaCache。如果两者都已设置,则使用显式指定的类型。
  • schema 缓存无法检测 ALTER TABLE 带来的变更。如果你修改了表的 schema,请创建新的 ClickHouseClient,或避免对该表使用 UseSchemaCache
  • 缓存的作用域仅限于 ClickHouseClient 实例,并以 (database,table) 为键。同一张表的不同列子集会共享同一个缓存的 schema。

ClickHouseClient

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

创建客户端

使用连接字符串或 ClickHouseClientSettings 对象创建 ClickHouseClient。可用选项请参阅配置部分。 你的 ClickHouse Cloud 服务的详细信息可在 ClickHouse Cloud 控制台中查看。 选择一个服务并点击 Connect 选择 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
// 在您的 DI 配置中
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

// 使用工厂创建客户端
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");
ClickHouseClient 设计为可长期使用,并可在整个应用程序中共享。只需创建一次 (通常作为单例) ,并在所有数据库操作中重复使用。该客户端会在内部管理 HTTP 连接池。

执行查询

对于不返回结果的语句,使用 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)
};
  • 客户端会在插入前通过 SELECT * FROM <table> WHERE 1=0 自动拉取表结构。提供的值必须与目标列的类型匹配。若要跳过此查询,请使用 InsertOptions.ColumnTypesInsertOptions.UseSchemaCache
  • MaxDegreeOfParallelism > 1 时,批次会并行上传。会话与并行插入不兼容;请禁用会话,或将 MaxDegreeOfParallelism 设为 1
  • 如果你希望服务器为未提供的列应用 DEFAULT 值,请在 InsertOptions.Format 中使用 RowBinaryFormat.RowBinaryWithDefaults

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 缓存) 。
object[] 重载不同,InsertBinaryAsync<T> 不接受显式列列表。列由已注册类型的映射属性决定。要控制插入哪些列,可使用 [ClickHouseNotMapped] 排除属性,或使用 [ClickHouseColumn(Name = "...")] 为属性重命名。如果在 InsertOptions 中设置了 ColumnTypes,它们会覆盖 POCO 特性。

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

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)})
SQL ‘绑定’参数通过 HTTP URI 查询参数传递,因此如果使用过多,可能会导致出现 “URL 过长” 异常。为避免这一限制,在批量插入数据时请使用 InsertBinaryAsync

查询 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
);
如果要指定自定义 QueryId,请确保每次调用使用的值都是唯一的。随机生成的 GUID 是不错的选择。

自定义参数类型映射

使用 @ 风格的参数时 (例如 WHERE id = @id) ,驱动程序会根据 .NET 值类型自动推断 ClickHouse 类型。例如,int 会映射为 Int32DateTime 会映射为 DateTime 如需覆盖这些默认映射,请在 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);     // 映射到 DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // 映射到 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; // 回退到默认推断

        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 路径——由客户端创建的连接会继承这些设置。

原始流式传输

使用 ExecuteRawResultAsync 可按特定 format 直接流式传输查询结果,绕过数据读取器。这对于将数据导出到文件或传输到其他系统特别有用:
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。所有选项请参阅格式文档

原始流插入

使用 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"] // 可选:指定列
);
有关控制数据摄取行为的选项,请参阅格式设置文档

更多示例

如需更多实用用法示例,请参阅 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();
        // 使用连接...
    }
}
请勿在生产代码中直接创建 ClickHouseConnection。每次直接实例化都会新建一个 HTTP 客户端和连接池,这在高负载下可能导致套接字耗尽:
// 不要这样做 - 每次都会创建新的连接池
using var conn = new ClickHouseConnection("Host=localhost");
await conn.OpenAsync();
请始终使用 ClickHouseDataSource,或共享同一个 ClickHouseClient 实例。

使用 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 注册为单例
使用自定义 HttpClientHttpClientFactory 时,请确保将 PooledConnectionIdleTimeout 设置为小于服务器 keep_alive_timeout 的值,以避免因连接半关闭而导致错误。Cloud 部署的默认 keep_alive_timeout 为 10 秒。
避免在未共享 HttpClient 的情况下创建多个 ClickHouseClient 或独立的 ClickHouseConnection 实例。每个实例都会创建自己的连接池。

DateTime 处理

  1. 尽可能使用 UTC。 将时间戳存储为 DateTime('UTC') 列,并在代码中使用 DateTimeKind.Utc。这样可以避免时区歧义。
  2. 使用 DateTimeOffset 进行明确的时区处理。 它始终表示某个确定的时间点,并包含偏移信息。
  3. 在 SQL 类型提示中指定时区。 当参数中使用 Unspecified 的 DateTime 值,且目标列不是 UTC 时,请在 SQL 中包含时区信息:
    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 控制) :
ModeBehaviorUse case
wait_for_async_insert=1插入会在数据写入磁盘后返回。错误也会返回给客户端。推荐用于大多数工作负载
wait_for_async_insert=0数据进入缓冲区后,插入会立即返回。不保证数据一定会被持久化。仅适用于可接受数据丢失的场景
使用 wait_for_async_insert=0 时,错误只会在刷新期间暴露出来,且无法追溯到原始插入。客户端也不会提供背压,存在服务器过载的风险。
关键设置:
SettingDescription
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 类型
Int8sbyte
UInt8byte
Int16short
UInt16ushort
Int32int
UInt32uint
Int64long
UInt64ulong
Int128BigInteger
UInt128BigInteger
Int256BigInteger
UInt256BigInteger

浮点类型

ClickHouse 类型.NET 类型
Float32float
Float64double
BFloat16float

Decimal 类型

ClickHouse 类型.NET 类型
Decimal(P, S)decimal / ClickHouseDecimal
Decimal32(S)decimal / ClickHouseDecimal
Decimal64(S)decimal / ClickHouseDecimal
Decimal128(S)decimal / ClickHouseDecimal
Decimal256(S)decimal / ClickHouseDecimal
Decimal 类型的转换由 UseCustomDecimals 设置控制。

布尔类型

ClickHouse 类型.NET 类型
Boolbool

String 类型

ClickHouse 类型.NET 类型
Stringstring
FixedString(N)string
默认情况下,StringFixedString(N) 列都会作为 string 返回。要将它们改为读取为 byte[],请在连接字符串中设置 ReadStringsAsByteArrays=true。这在存储可能不是有效 UTF-8 的二进制数据时非常有用。

日期和时间类型

ClickHouse 类型.NET 类型
DateDateTime
Date32DateTime
DateTimeDateTime
DateTime32DateTime
DateTime64DateTime
TimeTimeSpan
Time64TimeSpan
ClickHouse 在内部将 DateTimeDateTime64 值存储为 Unix 时间戳 (即自纪元以来的秒或亚秒单位) 。虽然存储始终采用 UTC,但列可以关联一个时区,这会影响值的显示和解析方式。 读取 DateTime 值时,DateTime.Kind 属性会根据列的时区进行设置:
列定义返回的 DateTime.Kind说明
DateTime('UTC')Utc显式指定 UTC 时区
DateTime('Europe/Amsterdam')Unspecified已应用时区偏移
DateTimeUnspecified挂钟时间按原样保留
对于非 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 类型备注
JsonJsonObject默认 (JsonReadMode=Binary)
JsonstringJsonReadMode=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 类型
UUIDGuid
IPv4IPAddress
IPv6IPAddress
NothingDBNull
Dynamic见注释
Array(T)T[]
Tuple(T1, T2, …)Tuple<T1, T2, ...> / LargeTuple
Map(K, V)Dictionary<K, V>
Nullable(T)T?
Enum8string
Enum16string
LowCardinality(T)与 T 相同
SimpleAggregateFunction与其底层类型相同
Nested(…)Tuple[]
Variant(T1, T2, …)见注释
QBit(T, dimension)T[]
Dynamic 和 Variant 类型会转换为每一行实际底层类型对应的类型。

几何类型

ClickHouse 类型.NET 类型
PointTuple<double, double>
RingTuple<double, double>[]
LineStringTuple<double, double>[]
PolygonRing[]
MultiLineStringLineString[]
MultiPolygonPolygon[]
Geometry见说明
Geometry 类型是一种 Variant 类型,可容纳任意几何类型。它会被转换为对应的类型。

类型映射:写入 ClickHouse

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

整数类型

ClickHouse 类型可接受的 .NET 类型备注
Int8sbyte,任何与 Convert.ToSByte() 兼容的类型
UInt8byte,任何与 Convert.ToByte() 兼容的类型
Int16short,任何与 Convert.ToInt16() 兼容的类型
UInt16ushort,任何与 Convert.ToUInt16() 兼容的类型
Int32int,任何与 Convert.ToInt32() 兼容的类型
UInt32uint,任何与 Convert.ToUInt32() 兼容的类型
Int64long,任何与 Convert.ToInt64() 兼容的类型
UInt64ulong,任何与 Convert.ToUInt64() 兼容的类型
Int128BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型
UInt128BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型
Int256BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型
UInt256BigIntegerdecimaldoublefloatintuintlongulong,任何与 Convert.ToInt64() 兼容的类型

浮点类型

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

布尔类型

ClickHouse 类型可接受的 .NET 类型说明
Boolbool

String 类型

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

日期和时间类型

ClickHouse 类型可接受的 .NET 类型说明
DateDateTime, DateTimeOffset, DateOnly, NodaTime 类型转换为 Unix 天数,存储为 UInt16
Date32DateTime, DateTimeOffset, DateOnly, NodaTime 类型转换为 Unix 天数,存储为 Int32
DateTimeDateTime, DateTimeOffset, DateOnly, NodaTime 类型详见下文
DateTime32DateTime, DateTimeOffset, DateOnly, NodaTime 类型与 DateTime 相同
DateTime64DateTime, DateTimeOffset, DateOnly, NodaTime 类型精度取决于 scale 参数
TimeTimeSpan, int限制为 ±999:59:59;int 视为秒数
Time64TimeSpan, decimal, double, float, int, long, string字符串按 [-]HHH:MM:SS[.fraction] 格式解析;限制为 ±999:59:59.999999999
驱动在写入值时会遵循 DateTime.Kind
DateTime.KindHTTP 参数批量写入
Utc保留精确时刻保留精确时刻
Local保留精确时刻保留精确时刻
Unspecified按参数类型时区中的本地时钟时间处理 (默认为 UTC)按列时区中的本地时钟时间处理
DateTimeOffset 值始终保留精确时刻。 示例:UTC DateTime (保留精确时刻)
var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// 存储为 12:00 UTC
// 从 DateTime('Europe/Amsterdam') 列读取:13:00 (UTC+1)
// 从 DateTime('UTC') 列读取:12:00 UTC
示例:未指定 DateTime (挂钟时间)
var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// 写入 DateTime('Europe/Amsterdam') 列:存储为阿姆斯特丹时间 14:30
// 从 DateTime('Europe/Amsterdam') 列读取: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 提示)批量复制
UtcUTC保持同一时刻保持同一时刻保持同一时刻
UtcEurope/Amsterdam保持同一时刻保持同一时刻保持同一时刻
Local任意保持同一时刻保持同一时刻保持同一时刻
UnspecifiedUTC按 UTC 处理按 UTC 处理按 UTC 处理
UnspecifiedEurope/Amsterdam按阿姆斯特丹时间处理按 UTC 处理按阿姆斯特丹时间处理

Decimal 类型

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

JSON 类型

ClickHouse 类型可接受的 .NET 类型说明
JsonstringJsonObjectJsonNode、任意对象行为取决于 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 类型说明
UUIDGuid, stringstring 会被解析为 Guid
IPv4IPAddress, string必须是 IPv4;string 通过 IPAddress.Parse() 解析
IPv6IPAddress, string必须是 IPv6;string 通过 IPAddress.Parse() 解析
NothingAny不写入任何内容 (空操作)
Dynamic不支持 (抛出 NotImplementedException)
Array(T)IList, nullnull 会写入为空数组
Tuple(T1, T2, …)ITuple, IList元素数量必须与 Tuple 元数一致
Map(K, V)IDictionary
Nullable(T)nullDBNull 或 T 可接受的类型会在值之前写入 null 标志字节
Enum8string, sbyte, 数值类型string 会在枚举字典中查找
Enum16string, short, 数值类型string 会在枚举字典中查找
LowCardinality(T)T 可接受的类型委托给底层类型处理
SimpleAggregateFunction底层类型可接受的类型委托给底层类型处理
Nested(…)tuple 的 IList元素数量必须与字段数量一致
Variant(T1, T2, …)匹配 T1、T2、… 之一的值如果没有匹配的类型,则抛出 ArgumentException
QBit(T, dim)IList委托给 Array;dimension 仅作为元数据

几何类型

ClickHouse 类型可接受的 .NET 类型说明
PointSystem.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.ConnectionClickHouseConnection连接生命周期、HTTP 客户端工厂选择、连接打开/关闭、会话管理。
ClickHouse.Driver.CommandClickHouseCommand查询执行开始/完成、耗时、查询 ID、服务器统计信息以及错误详情。
ClickHouse.Driver.TransportClickHouseConnection底层 HTTP 流式请求、压缩标志、响应状态码以及传输失败。
ClickHouse.Driver.ClientClickHouseClient二进制插入、查询及其他操作
ClickHouse.Driver.NetTraceTraceHelper网络跟踪,仅在启用调试模式时生效

示例:排查连接问题

{
    "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.statementSQL 查询 (如果已启用)
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;
启用 IncludeSqlInActivityTags 可能会在链路追踪中泄露敏感数据。在 production 环境中使用时请务必谨慎。

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);
使用自定义 HttpClient 时的重要注意事项
  • 自动解压缩:如果未禁用压缩,则必须启用 AutomaticDecompression (压缩默认处于启用状态) 。
  • 空闲超时:将 PooledConnectionIdleTimeout 设置为小于服务器的 keep_alive_timeout (ClickHouse Cloud 中为 10 秒) ,以避免半开连接导致的连接错误。

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 模式。
该提供商仍在积极开发中。当前版本支持 LINQ 查询 (包括 JOIN、子查询和集合运算) 、通过 SaveChanges / BulkInsertAsync 执行 INSERT、支持完整 DDL (CREATE / ALTER / DROP) 的迁移,以及 ClickHouse 特有的表引擎配置。不支持 UPDATE / DELETE

安装

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, UInt8UInt64sbyte, short, int, long, byte, ushort, uint, ulong
大整数Int128, Int256, UInt128, UInt256BigInteger
浮点数Float32, Float64, BFloat16float, double
DecimalDecimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S)decimalClickHouseDecimal
BoolBoolbool
StringString, FixedString(N)string
枚举Enum8(...), Enum16(...)string 或 C# enum
日期/时间Date, Date32, DateTime, DateTime64(P, 'TZ')DateOnly, DateTime
TimeTime, Time64(N)TimeSpan
UUIDUUIDGuid
NetworkIPv4, IPv6IPAddress
数组Array(T)T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
MapMap(K, V)Dictionary<K,V>
TupleTuple(T1, ...)Tuple<...>ValueTuple<...>
VariantVariant(T1, T2, ...)object
动态Dynamicobject
JSONJsonJsonNodestring
地理空间Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon, GeometryTuple<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");
  • 不支持 JSON 路径转换 — LINQ 中的 entity.Data["name"] 不会转换为 ClickHouse 的 data.name SQL 语法。请对非 JSON 列进行过滤,并在内存中检查 JSON 内容。
  • NULL 语义 — 对于 NULL 值,ClickHouse 的 JSON 类型返回的是 {} (空对象) ,而不是 SQL NULL。
  • 整数精度 — ClickHouse JSON 会将所有整数存储为 Int64。通过 JsonNode 读取时,应使用 GetValue<long>(),而不是 GetValue<int>()

表引擎

通过 ToTable(name, t => ...) 流式 API 配置 ClickHouse 表引擎及引擎特定子句。若未配置引擎,提供商默认使用 MergeTree,并根据实体的主键确定 ORDER BY
modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});
支持的引擎系列:
Engine流式方法说明
MergeTreeHasMergeTreeEngine()未配置时默认使用
ReplacingMergeTreeHasReplacingMergeTreeEngine("Version", "IsDeleted")HasReplacingMergeTreeEngine<T>(e => e.Version)Version / IsDeleted 列为可选
SummingMergeTreeHasSummingMergeTreeEngine(…)HasSummingMergeTreeEngine<T>(e => new { … })可选求和列
AggregatingMergeTreeHasAggregatingMergeTreeEngine()
CollapsingMergeTreeHasCollapsingMergeTreeEngine("Sign")HasCollapsingMergeTreeEngine<T>(e => e.Sign)Sign 列必须为 Int8
VersionedCollapsingMergeTreeHasVersionedCollapsingMergeTreeEngine("Sign", "Version")<T>(e => e.Sign, e => e.Version)
GraphiteMergeTreeHasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, MemoryHasLogEngine(), 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
支持的操作:
OperationEmits
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。

局限性

AggregateFunction 列

无法直接查询或插入 AggregateFunction(...) 类型的列。 如需插入:
INSERT INTO t VALUES (uniqState(1));
要进行查询:
SELECT uniqMerge(c) FROM t;

最后修改于 2026年6月10日