Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

Cliente C# do ClickHouse

O cliente C# oficial para se conectar ao ClickHouse. O código-fonte do cliente está disponível no repositório do GitHub. Desenvolvido originalmente por Oleg V. Kozlyuk.

A biblioteca fornece duas APIs principais:

  • ClickHouseClient (recomendado): um cliente de alto nível, thread-safe, projetado para uso como singleton. Fornece uma API assíncrona simples para consultas e inserções em massa. Ideal para a maioria das aplicações.

  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): abstrações padrão de banco de dados do .NET. Necessário para integração com ORM (Dapper, Linq2db) e quando você precisa de compatibilidade com ADO.NET. ClickHouseBulkCopy é uma classe auxiliar para inserir dados com eficiência usando uma conexão ADO.NET. ClickHouseBulkCopy foi descontinuado e será removido em um lançamento futuro; use ClickHouseClient.InsertBinaryAsync no lugar.

Ambas as APIs compartilham o mesmo pool de conexões HTTP subjacente e podem ser usadas juntas na mesma aplicação.

Guia de migração

  1. Atualize o arquivo .csproj com o novo nome do pacote ClickHouse.Driver e a versão mais recente no NuGet.
  2. Atualize todas as referências a ClickHouse.Client para ClickHouse.Driver no seu código.

Versões compatíveis do .NET

ClickHouse.Driver oferece suporte às seguintes versões do .NET:

  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

Versões compatíveis do ClickHouse

O cliente oferece suporte oficial aos 3 lançamentos mais recentes, além dos 2 lançamentos LTS mais recentes.

Instalação

Instale o pacote via NuGet:

dotnet add package ClickHouse.Driver

Ou use o Gerenciador de Pacotes NuGet:

Install-Package ClickHouse.Driver

Início rápido

using ClickHouse.Driver;

// Cria um cliente (geralmente como um singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Executa uma consulta
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);

Configuração

Há duas formas de configurar sua conexão com o ClickHouse:

  • String de conexão: pares de chave/valor separados por ponto e vírgula que especificam o host, as credenciais de autenticação e outras opções de conexão.
  • Objeto ClickHouseClientSettings: um objeto de configuração fortemente tipado que pode ser carregado de arquivos de configuração ou definido no código.

Abaixo está a lista completa de todas as configurações, seus valores padrão e seus efeitos.

Configurações de conexão

Propriedade Tipo Padrão Chave da string de conexão Descrição
Host string "localhost" Host Nome do host ou endereço IP do servidor do ClickHouse
Port ushort 8123 (HTTP) / 8443 (HTTPS) Port Número da porta; o padrão depende do protocolo
Username string "default" Username Nome de usuário para authentication
Password string "" Password Senha de authentication
Database string "" Database Banco de dados padrão; se vazio, usa o padrão do servidor/usuário
Protocol string "http" Protocol Protocolo de conexão: "http" ou "https"
Path string null Path Caminho da URL para cenários com reverse proxy (por exemplo, /clickhouse)
Timeout TimeSpan 2 minutos Timeout Tempo limite da operação (armazenado como segundos na string de conexão)

Formato e serialização de dados

Propriedade Tipo Padrão Chave da string de conexão Descrição
UseCompression bool true Compression Ativa a compactação gzip para a transferência de dados
UseCustomDecimals bool true UseCustomDecimals Usa ClickHouseDecimal para precisão arbitrária; se false, usa o decimal do .NET (limite de 128 bits)
ReadStringsAsByteArrays bool false ReadStringsAsByteArrays Lê colunas String e FixedString como byte[] em vez de string; útil para dados binários
UseFormDataParameters bool false UseFormDataParameters Envia parâmetros como form data em vez de string de consulta da URL
ReadBufferSize int 8192 (8 KiB) ReadBufferSize Tamanho em bytes do buffer usado para ler respostas HTTP de consultas. Aumente-o para reduzir o reabastecimento do buffer em grandes result sets. Valores iguais ou superiores a 85.000 bytes são alocados no Large Object Heap, o que pode causar fragmentação do LOH e pausas mais longas do GC.
ParameterTypeResolver IParameterTypeResolver null Resolver personalizado para mapeamento de tipo de parâmetro no estilo @; consulte Mapeamento personalizado de tipo de parâmetro
ParameterFormatter IParameterFormatter null Formatador personalizado para serialização de valores de parâmetros; consulte Formatação personalizada de valores de parâmetros
ReadValueConverter IReadValueConverter null Transformação personalizada aplicada aos valores retornados pelo leitor de dados; consulte Conversão personalizada de valores lidos
JsonReadMode JsonReadMode Binary JsonReadMode Como os dados JSON são retornados: Binary (retorna JsonObject) ou String (retorna a string JSON bruta)
JsonWriteMode JsonWriteMode String JsonWriteMode Como os dados JSON são enviados: String (serializa via JsonSerializer, aceita todas as entradas) ou Binary (somente POCOs registrados com type hints)

Gerenciamento de sessão

Propriedade Tipo Padrão Chave da string de conexão Descrição
UseSession bool false UseSession Habilita sessões com estado; serializa solicitações
SessionId string null SessionId ID da sessão; gera automaticamente um GUID se for null e UseSession for true

Segurança

Propriedade Tipo Padrão Chave da string de conexão Descrição
SkipServerCertificateValidation bool false Ignora a validação do certificado HTTPS; não use em produção

Configuração do cliente HTTP

Propriedade Tipo Padrão Chave da string de conexão Descrição
HttpClient HttpClient null Instância personalizada de HttpClient pré-configurada
HttpClientFactory IHttpClientFactory null Fábrica personalizada para criar instâncias de HttpClient
HttpClientName string null Nome usado pelo HttpClientFactory para criar um cliente específico

Logging e depuração

Propriedade Tipo Padrão Chave da string de conexão Descrição
LoggerFactory ILoggerFactory null Fábrica de loggers para diagnóstico
EnableDebugMode bool false Habilita o rastreamento de rede do .NET (requer LoggerFactory com o nível definido como Trace); impacto significativo no desempenho

Configurações personalizadas e roles

Propriedade Tipo Padrão Chave da string de conexão Descrição
CustomSettings IDictionary<string, object> Vazio prefixo set_* Configurações do servidor ClickHouse; veja a observação abaixo
Roles IReadOnlyList<string> Vazio Roles Roles do ClickHouse separadas por vírgulas (por exemplo, Roles=admin,reader)
ApplicationInfo IReadOnlyDictionary<string, string> Vazio Tags de formato livre anexadas ao cabeçalho HTTP User-Agent para atribuição de consultas por aplicação.

Exemplos de string de conexão

Conexão básica

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

Com configurações personalizadas do ClickHouse

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

QueryOptions

QueryOptions permite substituir configurações do cliente individualmente para cada consulta. Todas as propriedades são opcionais e só substituem os padrões do cliente quando especificadas.

Propriedade Tipo Descrição
QueryId string Identificador personalizado da consulta para rastreamento em system.query_log ou cancelamento
Database string Substitui o banco de dados padrão desta consulta
Roles IReadOnlyList<string> Substitui os roles do cliente para esta consulta
CustomSettings IDictionary<string, object> Configurações do servidor ClickHouse para esta consulta (por exemplo, max_threads)
CustomHeaders IDictionary<string, string> Cabeçalhos HTTP adicionais para esta consulta
UseSession bool? Substitui o comportamento da sessão para esta consulta
SessionId string ID da sessão para esta consulta (requer UseSession = true)
BearerToken string Substitui o token de autenticação para esta consulta
ParameterTypeResolver IParameterTypeResolver Substitui o resolver do cliente para o mapeamento de tipo de parâmetro no estilo @; consulte Mapeamento personalizado de tipo de parâmetro
ParameterFormatter IParameterFormatter Substitui o formatador do cliente para a serialização do valor do parâmetro no estilo @; consulte Formatação personalizada do valor do parâmetro
ReadValueConverter IReadValueConverter Substitui a transformação no nível do cliente aplicada aos valores retornados pelo leitor de dados; consulte Conversão personalizada de valor lido
MaxExecutionTime TimeSpan? Timeout da consulta no servidor (passado como configuração max_execution_time); o servidor cancela a consulta se esse limite for excedido
AcceptEncoding string Substituição do cabeçalho Accept-Encoding por consulta (por exemplo, "zstd", "zstd, gzip;q=0.5"); também força enable_http_compression=1 na URL. Consulte Compactação de transporte por consulta.

Exemplo:

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

InsertOptions estende QueryOptions com configurações específicas para operações de inserção em massa via InsertBinaryAsync.

Property Type Default Description
BatchSize int 100,000 Número de linhas por lote
MaxDegreeOfParallelism int 1 Número de envios paralelos de lotes
Format RowBinaryFormat RowBinary Formato binário: RowBinary ou RowBinaryWithDefaults
ColumnTypes IReadOnlyDictionary<string, string> null Nome da coluna → string do tipo ClickHouse. Ignora a consulta de sondagem de esquema quando definido.
UseSchemaCache bool false Mantém em cache o esquema completo da tabela para cada (banco de dados, tabela) durante toda a vida útil do cliente.

Todas as propriedades de QueryOptions também estão disponíveis em InsertOptions.

Exemplo:

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

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

Ignorando a consulta de sondagem do esquema

Por padrão, InsertBinaryAsync envia uma consulta SELECT ... WHERE 1=0 antes de cada inserção para identificar os tipos das colunas. Em cenários de alta taxa de transferência, você pode eliminar essa sobrecarga de duas formas:

Opção 1: Informe explicitamente os tipos das colunas

Quando você conhece o esquema da tabela em tempo de compilação, passe-o diretamente por meio de ColumnTypes. Nenhuma consulta de esquema é enviada:

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

Opção 2: Armazene o esquema em cache

Ao inserir repetidamente na mesma tabela, defina UseSchemaCache = true para consultar o esquema uma única vez e reutilizá-lo nas inserções subsequentes na mesma instância do ClickHouseClient:

var options = new InsertOptions { UseSchemaCache = true };

// Primeira chamada busca o esquema do servidor
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Segunda chamada reutiliza o esquema em cache — sem round-trip adicional
await client.InsertBinaryAsync("my_table", columns, batch2, options);

ClickHouseClient

ClickHouseClient é a API recomendada para interagir com o ClickHouse. Ela é thread-safe, foi projetada para uso como singleton e gerencia internamente um pool de conexões HTTP.

Criando um cliente

Crie um ClickHouseClient com uma string de conexão ou um objeto ClickHouseClientSettings. Consulte a seção Configuração para conhecer as opções disponíveis.

Os detalhes do seu serviço do ClickHouse Cloud estão disponíveis no console do ClickHouse Cloud.

Selecione um serviço e clique em Connect:

Botão Connect do serviço do ClickHouse Cloud

Escolha C#. Os detalhes da conexão são exibidos abaixo.

Detalhes da conexão do ClickHouse Cloud para C#

Se você estiver usando ClickHouse autogerenciado, os detalhes da conexão serão definidos pelo administrador do ClickHouse.

Usando uma string de conexão:

using ClickHouse.Driver;

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

Ou use ClickHouseClientSettings:

using ClickHouse.Driver;

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

Para cenários com injeção de dependência, use 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");

Executando consultas

Use ExecuteNonQueryAsync para instruções que não retornam resultados:

// Criar uma tabela
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Remover uma tabela
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");

Use ExecuteScalarAsync para obter um único valor:

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

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Versão do servidor: {version}");

Inserção de dados

Inserções parametrizadas

Insira dados por meio de consultas parametrizadas com ExecuteNonQueryAsync. Os tipos dos parâmetros devem ser especificados no SQL usando a sintaxe {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
);

Inserção em massa

Use InsertBinaryAsync para inserir grandes volumes de linhas com eficiência. Ele transmite os dados usando o formato binário nativo de linhas do ClickHouse, oferece suporte ao envio paralelo de lotes e evita erros de "URL muito longa" que podem ocorrer com consultas parametrizadas.

// Preparar dados como IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

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

// Inserção básica
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");

Para grandes volumes de dados, configure o envio em lotes e o paralelismo com InsertOptions:

var options = new InsertOptions
{
    BatchSize = 100_000,           // Linhas por lote (padrão: 100.000)
    MaxDegreeOfParallelism = 4     // Uploads de lotes em paralelo (padrão: 1)
};

Inserções com POCO

Em vez de construir arrays object[], você pode inserir diretamente objetos POCO com tipagem forte. Registre o tipo uma vez e, em seguida, passe IEnumerable<T>:

// Defina um POCO correspondente às colunas da sua tabela
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Registre o tipo (uma vez por ciclo de vida do cliente)
client.RegisterBinaryInsertType<SensorReading>();

// Insira diretamente — os nomes das colunas são derivados dos nomes das propriedades
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);

Por padrão, todas as propriedades públicas legíveis são mapeadas para colunas com base em uma correspondência estrita de nomes que diferencia maiúsculas de minúsculas. Você pode personalizar esse mapeamento com atributos:

public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Mapeia para uma coluna com nome diferente
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Tipo explícito do ClickHouse
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Exclui do insert
    public string InternalTag { get; set; }
}
Atributo Finalidade
[ClickHouseColumn(Name = "...")] Sobrescrever o nome da coluna de destino
[ClickHouseColumn(Type = "...")] Declarar explicitamente o tipo do ClickHouse
[ClickHouseNotMapped] Excluir a propriedade da inserção

Quando todas as propriedades mapeadas especificam um Type explícito, a consulta de sondagem do esquema é ignorada por completo. Quando apenas algumas propriedades têm tipos explícitos, o driver recorre à consulta de sondagem do esquema para o conjunto completo de colunas.

InsertBinaryAsync<T> oferece suporte às mesmas InsertOptions (batching, paralelismo, cache de esquema) que a sobrecarga object[].

Evolução do esquema

Os inserts de POCO funcionam perfeitamente quando colunas são adicionadas à tabela de destino depois que o tipo é registrado. Como o driver insere apenas as colunas mapeadas pelo POCO, quaisquer novas colunas com DEFAULT (ou outras expressões padrão) são preenchidas automaticamente pelo servidor. Não é necessário alterar o código nem fazer um novo registro.


Lendo dados

Use ExecuteReaderAsync para executar consultas SELECT. O ClickHouseDataReader retornado fornece acesso tipado às colunas do resultado por meio de métodos como GetInt64(), GetString() e GetFieldValue<T>().

Chame Read() para avançar para a próxima linha. Ele retorna false quando não há mais linhas. Acesse as colunas pelo índice (baseado em 0) ou pelo nome da coluna.

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

Leitura com POCO

Em vez de ler colunas por índice ou nome, você pode direcionar os resultados da consulta diretamente para suas próprias classes. Registre o tipo uma vez no cliente e, em seguida, use 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}");
}
Registro

RegisterPocoType<T>() configura os mapeamentos de inserção e de leitura e valida ambos de antemão. RegisterBinaryInsertType<T>() permanece inalterado e continua sendo exclusivo para inserção por compatibilidade com versões anteriores.

Um tipo registrado deve ter:

  • Um construtor público sem parâmetros.
  • Pelo menos uma propriedade pública com um setter público que não seja init. Propriedades required são compatíveis.
Correspondência de colunas

A correspondência de colunas diferencia maiúsculas de minúsculas. Colunas de resultado ausentes mantêm as propriedades com seu valor padrão; colunas de resultado adicionais são ignoradas.

Não há conversões automáticas, e uma incompatibilidade de tipo lança InvalidOperationException.

Materializando uma única linha

Ao iterar manualmente sobre um reader, use ClickHouseDataReader.MapTo<T>() para materializar a linha atual em um POCO registrado sem avançar o reader:

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}");
}
Diagnósticos de registro

Quando uma LoggerFactory está configurada, RegisterPocoType<T>() e RegisterBinaryInsertType<T>() geram um log no nível Debug (categoria ClickHouse.Driver.Client) informando quais propriedades foram mapeadas para quais colunas e quais foram ignoradas, bem como o motivo. Consulte Logging e diagnósticos.


Parâmetros SQL

No ClickHouse, o formato padrão para parâmetros em consultas SQL é {parameter_name:DataType}.

Exemplos:

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

Parâmetros do tipo Identifier

O tipo de parâmetro Identifier permite vincular com segurança o nome de um banco de dados, tabela ou coluna, em vez de um literal de string entre aspas. Use-o com a sintaxe {name:Identifier} em SQL ou definindo 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);

O valor é enviado literalmente, e o servidor o substitui como um identificador SQL não entre aspas, aplicando seu próprio uso de backticks e escape. Identificadores que contêm caracteres especiais (inclusive backticks) podem fazer o percurso de ida e volta com segurança.


ID da consulta

Cada consulta recebe um query_id único, que pode ser usado para obter dados da tabela system.query_log ou cancelar consultas de longa execução. Você pode especificar um ID de consulta personalizado por meio de QueryOptions:

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

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

Mapeamento personalizado de tipos de parâmetro

Ao usar parâmetros no estilo @ (por exemplo, WHERE id = @id), o driver infere automaticamente o tipo do ClickHouse com base no tipo de valor do .NET. Por exemplo, int é mapeado para Int32.

Para substituir esses padrões, defina ParameterTypeResolver em ClickHouseClientSettings. Isso é útil quando você quer que todos os parâmetros DateTime usem DateTime64(3) para precisão de milissegundos ou que todos os decimais usem uma escala específica, sem precisar definir ClickHouseType em cada parâmetro individualmente.

Usando DictionaryParameterTypeResolver para mapeamentos simples de tipo:

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 personalizado para cenários avançados:

Para resolução com base no valor ou no nome, implemente diretamente a interface IParameterTypeResolver. Retorne null para usar a inferência padrão:

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

Você também pode definir um resolver para uma única consulta por meio de QueryOptions.ParameterTypeResolver. Quando definido, ele tem precedência sobre o resolver no nível do cliente.

Precedência da resolução de tipos:

O resolver é uma etapa em uma cadeia de precedência. Da maior para a menor prioridade:

  1. ClickHouseType explícito definido no parâmetro
  2. Type hint de SQL da sintaxe {name:Type} na consulta
  3. IParameterTypeResolver (de QueryOptions.ParameterTypeResolver, com fallback para ClickHouseClientSettings.ParameterTypeResolver)
  4. Inferência de tipo integrada (TypeConverter.ToClickHouseType)

O resolver também funciona com o caminho do ADO.NET ClickHouseConnection — as configurações são herdadas pelas conexões criadas a partir do cliente.


Formatação personalizada de valores de parâmetros

IParameterFormatter é um hook que define como os valores dos parâmetros são serializados. Use-o quando a formatação padrão (por exemplo, precisão de DateTime, convenção decimal, escaping de strings, representação de números) não corresponder ao que seu esquema ou suas ferramentas downstream esperam.

Defina ParameterFormatter em ClickHouseClientSettings para instalar um formatador para todas as consultas parametrizadas. O formatador recebe o valor, o nome do tipo ClickHouse resolvido e o nome do parâmetro, e retorna a representação em string que é enviada ao servidor. Retorne null para deixar o processamento seguir para o formatador padrão.

Usando DictionaryParameterFormatter para formatação simples por tipo 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 personalizado para casos avançados:

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
    }
}

Você também pode definir um formatador por consulta via QueryOptions.ParameterFormatter. Quando definido, ele tem precedência sobre o formatador em nível de cliente.

Valores compostos:

O formatador é executado tanto para parâmetros de collection de nível superior quanto para cada elemento dentro de valores compostos (Array, Tuple, Map, Nullable, LowCardinality, Variant). Por exemplo, um mapeamento typeof(int) formata individualmente cada elemento Int32 de um Array(Int32).

Uso de aspas simples em contextos compostos:

Para types do ClickHouse semelhantes a string (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) embutidos em um literal composto, o driver envolve a saída do formatador em aspas simples, mas não escapa seu conteúdo. Se a string retornada contiver uma aspa simples ou barra invertida sem escape, o literal composto ficará malformado e o servidor rejeitará a consulta.

Parâmetros de string de nível superior (não embutidos em um composto) são usados literalmente, sem aspas, portanto não é necessário escaping nesse caso.

Precedência do formatador:

  1. IParameterFormatter (de QueryOptions.ParameterFormatter, com fallback para ClickHouseClientSettings.ParameterFormatter). Se ele retornar um valor não nulo, esse valor será usado.
  2. Formatação interna específica de cada tipo em HttpParameterFormatter.

O formatador não é consultado para valores null ou DBNull; eles são sempre serializados como a sentinela nula do ClickHouse (\N).


Conversão personalizada de valores lidos

IReadValueConverter permite transformar os valores retornados pelo leitor de dados após a desserialização, sem alterar o tipo CLR deles. Usos típicos: definir DateTime.Kind = Utc em uma coluna DateTime sem timezone, aparar ou normalizar strings, ou fazer o pós-processamento de uma coluna JSON antes que ela chegue ao código da aplicação.

Defina ReadValueConverter em ClickHouseClientSettings para instalar um conversor para todas as leituras. O conversor é invocado uma vez por coluna por linha, tanto no caminho com boxing (GetValue) quanto no genérico (GetFieldValue<T>). Quando nenhum conversor é definido, a sobrecarga é zero — o leitor retorna os valores diretamente.

Usando DictionaryReadValueConverter para conversão simples por tipo 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);

Valores cujo tipo CLR em runtime não está registrado com For<T> passam inalterados. O despacho é feito pelo tipo CLR exato, portanto registre o tipo real produzido pelo leitor (por exemplo, For<JsonObject> para uma coluna JSON em JsonReadMode.Binary).

IReadValueConverter personalizado para cenários avançados:

Se você precisar despachar com base na string de tipo do lado do ClickHouse (por exemplo, para distinguir DateTime de DateTime('UTC') — ambos aparecem como o mesmo tipo CLR), implemente IReadValueConverter diretamente:

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

O conversor deve preservar o tipo CLR de runtime; os metadados da coluna (GetFieldType, GetSchemaTable) não passam por ele e devem permanecer consistentes com o valor retornado.

Você também pode definir um conversor por consulta via QueryOptions.ReadValueConverter; quando definido, ele tem precedência sobre o conversor no nível do cliente.

Limite do despacho:

O conversor é invocado uma vez por coluna com o valor completo da célula desserializada; ele não processa recursivamente contêineres compostos. Para uma coluna Array(Int32), o valor passado é um int[]; para Tuple(Int32, String), é um ITuple.

O conversor funciona com o caminho ClickHouseConnection do ADO.NET — as configurações são herdadas pelas conexões criadas a partir do cliente.


Fluxo bruto

Use ExecuteRawResultAsync para transmitir diretamente os resultados da consulta em um formato específico, sem passar pelo leitor de dados. Isso é útil para exportar dados para arquivos ou repassá-los a outros sistemas:

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();

Formatos comuns: JSONEachRow, CSV, TSV, Parquet, Native. Consulte a documentação sobre formatos para ver todas as opções.


Compressão de transporte por consulta

Por padrão, o cliente negocia gzip, deflate quando Compression=true (o padrão da string de conexão), e o cliente HTTP descomprime o fluxo de forma transparente.

Para exportações brutas (por exemplo, Parquet, Arrow, Native), talvez você queira negociar um codec diferente (por exemplo, zstd ou lz4) para trocar CPU por largura de banda sem alterar a configuração da conexão como um todo. QueryOptions.AcceptEncoding e ClickHouseCommand.AcceptEncoding definem o cabeçalho HTTP Accept-Encoding para uma única solicitação, substituindo qualquer valor padrão definido anteriormente, e forçam enable_http_compression=1 na URL (o que o ClickHouse exige antes de respeitar 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();

Configuração do HttpClient

O HttpClient padrão que o driver cria tem AutomaticDecompression = GZip | Deflate, o que descomprime esses algoritmos de forma transparente e remove Content-Encoding da resposta. Isso é o ideal para consultas normais, mas não se você quiser processar por conta própria os dados brutos e comprimidos.

Nesse caso, passe um HttpClient ou HttpClientFactory para o ClickHouseClient com AutomaticDecompression = DecompressionMethods.None

Corpos de erro

Quando o servidor responde com um 4xx/5xx e enable_http_compression=1 foi definido, ele compacta o corpo do erro com o mesmo codec que usaria em uma resposta bem-sucedida. O driver descompacta esses corpos para os codecs incluídos na BCL (gzip, deflate, br/brotli), para que a mensagem exibida em ClickHouseServerException seja legível. Para codecs que ele não consegue decodificar (zstd, lz4, …), o driver retorna uma mensagem substituta que informa o codec e aponta para system.query_log, onde está o texto original do erro.


Inserção bruta via stream

Use InsertRawStreamAsync para inserir dados diretamente de streams de arquivo ou de memória em formatos como CSV, JSON, Parquet ou qualquer formato compatível do ClickHouse.

Inserir de um arquivo 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
);

Mais exemplos

Para mais exemplos práticos de uso, consulte o diretório examples no repositório do GitHub.

ADO.NET

A biblioteca oferece suporte completo ao ADO.NET por meio de ClickHouseConnection, ClickHouseCommand e ClickHouseDataReader. Essa API é necessária para a integração com ORMs (Dapper, Linq2db) e quando você precisa das abstrações padrão de banco de dados do .NET.

Gerenciamento do ciclo de vida com ClickHouseDataSource

Sempre crie conexões a partir de um ClickHouseDataSource para garantir o gerenciamento adequado do ciclo de vida e o uso de pool de conexões. A DataSource gerencia internamente um único ClickHouseClient, e todas as conexões compartilham seu pool de conexões HTTP.

using ClickHouse.Driver.ADO;

// Crie o DataSource uma vez (registre como singleton no DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Crie conexões leves conforme necessário
await using var connection = await dataSource.OpenConnectionAsync();

// Use a conexão
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();

Para injeção de dependências:

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

// No seu serviço
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

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

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Use a conexão...
    }
}

Usando o ClickHouseCommand

Crie comandos usando uma conexão para executar SQL:

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

// Criar comando com SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Executar e ler resultados
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}

Métodos de comando:

  • ExecuteNonQueryAsync() - Para instruções INSERT, UPDATE, DELETE e DDL
  • ExecuteScalarAsync() - Retorna a primeira coluna da primeira linha
  • ExecuteReaderAsync() - Retorna um ClickHouseDataReader para percorrer os resultados

Usando ClickHouseDataReader

O ClickHouseDataReader fornece acesso tipado aos resultados da consulta:

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

while (reader.Read())
{
    // Acesso por índice de coluna
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Acesso por nome de coluna
    var email = reader.GetString("email");

    // Acesso genérico
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Verificar nulo
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}

Boas práticas

Ciclo de vida da conexão e pool de conexões

ClickHouse.Driver usa System.Net.Http.HttpClient internamente. O HttpClient tem um pool de conexões por endpoint. Como consequência:

  • As sessões do banco de dados são multiplexadas por conexões HTTP gerenciadas pelo pool de conexões.
  • As conexões HTTP são recicladas automaticamente pelo pool.
  • As conexões podem permanecer ativas mesmo depois que os objetos ClickHouseClient ou ClickHouseConnection são descartados.

Padrões recomendados:

Cenário Abordagem recomendada
Uso geral Use um ClickHouseClient singleton
ADO.NET / ORMs Use ClickHouseDataSource (cria conexões que compartilham o mesmo pool)
Ambientes de DI Registre ClickHouseClient ou ClickHouseDataSource como singleton com IHttpClientFactory

Tratamento de DateTime

  1. Use UTC sempre que possível. Armazene timestamps como colunas DateTime('UTC') e use DateTimeKind.Utc no seu código. Isso elimina ambiguidades de fuso horário.

  2. Use DateTimeOffset para lidar explicitamente com o fuso horário. Ele sempre representa um instante específico e inclui a informação de offset.

  3. Especifique o fuso horário nas type hints de SQL. Ao usar parâmetros com valores DateTime Unspecified destinados a colunas que não usam UTC, inclua o fuso horário no SQL:

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

Inserções assíncronas

Inserções assíncronas transferem do cliente para o servidor a responsabilidade pelo agrupamento em lotes. Em vez de exigir esse agrupamento no lado do cliente, o servidor armazena em buffer os dados recebidos e os grava no armazenamento com base em limites configuráveis. Isso é útil em cenários de alta concorrência, como workloads de observabilidade, em que muitos agentes enviam payloads pequenos.

Habilite inserções assíncronas via CustomSettings ou pela connection string:

// Usando CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recomendado: aguardar confirmação de flush

// Ou via connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"

Dois modos (controlados por wait_for_async_insert):

Modo Comportamento Caso de uso
wait_for_async_insert=1 A inserção retorna depois que os dados são gravados em disco. Os erros são retornados ao cliente. Recomendado para a maioria das workloads
wait_for_async_insert=0 A inserção retorna imediatamente quando os dados são armazenados no buffer. Não há garantia de que os dados serão persistidos. Somente quando a perda de dados for aceitável

Configurações principais:

Configuração Descrição
async_insert_max_data_size Executa o flush quando o buffer atinge este tamanho (bytes)
async_insert_busy_timeout_ms Executa o flush após esse timeout (milissegundos)
async_insert_max_query_number Executa o flush após esse número de consultas se acumularem

Sessões

Ative sessões apenas quando precisar de recursos com estado no servidor, por exemplo:

  • Tabelas temporárias (CREATE TEMPORARY TABLE)
  • Manter o contexto da consulta em várias instruções
  • Configurações no nível da sessão (SET max_threads = 4)

Quando as sessões estão ativadas, as solicitações são serializadas para evitar o uso simultâneo da mesma sessão. Isso adiciona sobrecarga a cargas de trabalho que não exigem estado de sessão.

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Opcional -- será gerado automaticamente se não informado
};

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

Usando ADO.NET (para compatibilidade com ORMs):

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();

Tipos de dados compatíveis

ClickHouse.Driver é compatível com todos os tipos de dados do ClickHouse. As tabelas abaixo mostram o mapeamento entre os tipos do ClickHouse e os tipos nativos do .NET na leitura de dados do banco de dados.

Mapeamento de tipos: leitura do ClickHouse

Tipos inteiros

Tipo do ClickHouse Tipo do .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

Tipos de ponto flutuante

Tipo do ClickHouse Tipo .NET
Float32 float
Float64 double
BFloat16 float

Tipos decimais

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

Tipo booleano

Tipo do ClickHouse Tipo .NET
Bool bool

Tipos String

Tipo do ClickHouse Tipo .NET
String string
FixedString(N) string

Tipos de data e hora

Tipo do ClickHouse Tipo .NET
Date DateTime
Date32 DateTime
DateTime DateTime
DateTime32 DateTime
DateTime64 DateTime
Time TimeSpan
Time64 TimeSpan

O ClickHouse armazena internamente os valores DateTime e DateTime64 como timestamps Unix (segundos ou frações de segundo desde a epoch). Embora o armazenamento seja sempre em UTC, as colunas podem ter um fuso horário associado, o que afeta como os valores são exibidos e interpretados.

Ao ler valores DateTime, a propriedade DateTime.Kind é definida com base no fuso horário da coluna:

Definição da coluna DateTime.Kind retornado Observações
DateTime('UTC') Utc Fuso horário UTC explícito
DateTime('Europe/Amsterdam') Unspecified Deslocamento aplicado
DateTime Unspecified Hora local preservada como está

Para colunas que não estão em UTC, o DateTime retornado representa a hora local nesse fuso horário. Use ClickHouseDataReader.GetDateTimeOffset() para obter um DateTimeOffset com o deslocamento correto para esse fuso horário:

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)

Para colunas sem um fuso horário explícito (ou seja, DateTime em vez de DateTime('Europe/Amsterdam')), o driver retorna um DateTime com Kind=Unspecified. Isso preserva exatamente a hora local como foi armazenada, sem fazer suposições sobre o fuso horário.

Se você precisar de um comportamento sensível a fuso horário para colunas sem fusos horários explícitos, faça uma destas opções:

  1. Use fusos horários explícitos nas definições das colunas: DateTime('UTC') ou DateTime('Europe/Amsterdam')
  2. Aplique o fuso horário manualmente após a leitura.

Tipo JSON

Tipo ClickHouse Tipo .NET Observações
Json JsonObject Padrão (JsonReadMode=Binary)
Json string Quando JsonReadMode=String

O tipo de retorno das colunas JSON é controlado pela configuração JsonReadMode:

  • Binary (padrão): Retorna System.Text.Json.Nodes.JsonObject. Fornece acesso estruturado aos dados JSON, mas tipos especializados do ClickHouse (como endereços IP, UUIDs e valores decimais grandes) são convertidos para suas representações em string dentro da estrutura JSON.

  • String: Retorna o JSON bruto como string. Preserva a representação exata do JSON no ClickHouse, o que é útil quando você precisa repassar o JSON sem fazer o parsing ou quando deseja cuidar da desserialização por conta própria.

// Configurar o modo string via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Ou via connection string
// "Host=localhost;JsonReadMode=String"

Outros tipos

Tipo do ClickHouse Tipo .NET
UUID Guid
IPv4 IPAddress
IPv6 IPAddress
Nothing DBNull
Dynamic Consulte a nota
Array(T) T[] (Array(Array(T)) aninhado é lido como T[][] irregular; use reader.GetFieldValue<T[,]>(ordinal) para materializar dados retangulares como uma matriz CLR multidimensional)
Tuple(T1, T2, …) Tuple<T1, T2, ...> / LargeTuple
Map(K, V) Dictionary<K, V>
Nullable(T) T?
Enum8 string
Enum16 string
LowCardinality(T) O mesmo que T
SimpleAggregateFunction O mesmo que o tipo subjacente
Nested(…) Tuple[]
Variant(T1, T2, …) Consulte a nota
QBit(T, dimension) T[]

Tipos de geometria

Tipo ClickHouse Tipo .NET
Point Tuple<double, double>
Ring Tuple<double, double>[]
LineString Tuple<double, double>[]
Polygon Ring[]
MultiLineString LineString[]
MultiPolygon Polygon[]
Geometry Consulte a observação

Mapeamento de tipos: escrita no ClickHouse

Ao inserir dados, o driver converte tipos .NET nos tipos correspondentes do ClickHouse. As tabelas abaixo mostram quais tipos .NET são aceitos para cada tipo de coluna do ClickHouse.

Tipos inteiros

Tipo do ClickHouse Tipos .NET aceitos Observações
Int8 sbyte, qualquer tipo compatível com Convert.ToSByte()
UInt8 byte, qualquer tipo compatível com Convert.ToByte()
Int16 short, qualquer tipo compatível com Convert.ToInt16()
UInt16 ushort, qualquer tipo compatível com Convert.ToUInt16()
Int32 int, qualquer tipo compatível com Convert.ToInt32()
UInt32 uint, qualquer tipo compatível com Convert.ToUInt32()
Int64 long, qualquer tipo compatível com Convert.ToInt64()
UInt64 ulong, qualquer tipo compatível com Convert.ToUInt64()
Int128 BigInteger, decimal, double, float, int, uint, long, ulong, qualquer tipo compatível com Convert.ToInt64()
UInt128 BigInteger, decimal, double, float, int, uint, long, ulong, qualquer tipo compatível com Convert.ToInt64()
Int256 BigInteger, decimal, double, float, int, uint, long, ulong, qualquer tipo compatível com Convert.ToInt64()
UInt256 BigInteger, decimal, double, float, int, uint, long, ulong, qualquer tipo compatível com Convert.ToInt64()

Tipos de ponto flutuante

Tipo do ClickHouse Tipos .NET aceitos Observações
Float32 float, qualquer tipo compatível com Convert.ToSingle()
Float64 double, qualquer tipo compatível com Convert.ToDouble()
BFloat16 float, qualquer tipo compatível com Convert.ToSingle() Trunca para o formato BFloat16 de 16 bits

Tipo booleano

Tipo do ClickHouse Tipos .NET aceitos Observações
Bool bool

Tipos String

Tipo do ClickHouse Tipos .NET aceitos Observações
String string, byte[], ReadOnlyMemory<byte>, Stream Tipos binários são gravados diretamente; streams podem ter ou não suporte a seek
FixedString(N) string, byte[], ReadOnlyMemory<byte>, Stream A string é codificada em UTF-8 e preenchida; os tipos binários devem ter exatamente N bytes

Tipos de data e hora

Tipo do ClickHouse Tipos .NET aceitos Observações
Date DateTime, DateTimeOffset, DateOnly, tipos NodaTime Convertido em dias Unix como UInt16; intervalo suportado [1970-01-01, 2149-06-06]
Date32 DateTime, DateTimeOffset, DateOnly, tipos NodaTime Convertido em dias Unix como Int32; intervalo suportado [1900-01-01, 2299-12-31]
DateTime DateTime, DateTimeOffset, DateOnly, tipos NodaTime Veja abaixo mais detalhes; intervalo suportado [1970-01-01, 2106-02-07 06:28:15] UTC
DateTime32 DateTime, DateTimeOffset, DateOnly, tipos NodaTime Igual a DateTime
DateTime64 DateTime, DateTimeOffset, DateOnly, tipos NodaTime Precisão baseada no parâmetro de escala
Time TimeSpan, int Limitado a ±999:59:59; int é tratado como segundos
Time64 TimeSpan, decimal, double, float, int, long, string string é interpretada como [-]HHH:MM:SS[.fraction]; limitado a ±999:59:59.999999999

O driver respeita DateTime.Kind ao gravar valores:

DateTime.Kind Parâmetros HTTP Em massa
Utc Instante preservado Instante preservado
Local Instante preservado Instante preservado
Unspecified Tratado como hora local no fuso horário do tipo do parâmetro (o padrão é UTC) Tratado como hora local no fuso horário da coluna

Os valores de DateTimeOffset sempre preservam o instante exato.

Exemplo: DateTime UTC (instante preservado)

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

Exemplo: DateTime não especificado (hora local)

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

Recomendação: para obter o comportamento mais simples e previsível, use DateTimeKind.Utc ou DateTimeOffset em todas as operações com DateTime. Isso garante que seu código funcione de forma consistente, independentemente do fuso horário do servidor, do cliente ou da coluna.

Parâmetros HTTP vs bulk copy

Há uma diferença importante entre a vinculação de parâmetros HTTP e o bulk copy ao gravar valores Unspecified de DateTime:

Bulk Copy conhece o fuso horário da coluna de destino e interpreta corretamente os valores Unspecified nesse fuso.

Parâmetros HTTP não conhecem automaticamente o fuso horário da coluna. Você deve especificá-lo na dica de tipo SQL:

// CORRETO: Fuso horário no type hint SQL - o tipo é extraído automaticamente
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRETO: Sem o type hint de fuso horário, interpretado como UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// O valor String "2024-01-15 14:30:00" é interpretado como UTC, não como horário de Amsterdã!
DateTime.Kind Coluna de destino Parâmetro HTTP (com indicação de fuso horário) Parâmetro HTTP (sem indicação de fuso horário) Bulk Copy
Utc UTC Instante preservado Instante preservado Instante preservado
Utc Europe/Amsterdam Instante preservado Instante preservado Instante preservado
Local Qualquer Instante preservado Instante preservado Instante preservado
Unspecified UTC Interpretado como UTC Interpretado como UTC Interpretado como UTC
Unspecified Europe/Amsterdam Interpretado como horário de Amsterdã Interpretado como UTC Interpretado como horário de Amsterdã

Tipos Decimal

Tipo do ClickHouse Tipos .NET aceitos Observações
Decimal(P,S) decimal, ClickHouseDecimal, qualquer tipo compatível com Convert.ToDecimal() Lança OverflowException se exceder a precisão
Decimal32 decimal, ClickHouseDecimal, qualquer tipo compatível com Convert.ToDecimal() Precisão máxima: 9
Decimal64 decimal, ClickHouseDecimal, qualquer tipo compatível com Convert.ToDecimal() Precisão máxima: 18
Decimal128 decimal, ClickHouseDecimal, qualquer tipo compatível com Convert.ToDecimal() Precisão máxima: 38
Decimal256 decimal, ClickHouseDecimal, qualquer tipo compatível com Convert.ToDecimal() Precisão máxima: 76

Tipo JSON

Tipo ClickHouse Tipos .NET aceitos Observações
Json string, JsonObject, JsonNode, qualquer objeto O comportamento depende da configuração JsonWriteMode

O comportamento ao escrever JSON é controlado pela configuração JsonWriteMode:

Tipo de entrada JsonWriteMode.String (padrão) JsonWriteMode.Binary
string Passado diretamente Lança ArgumentException
JsonObject Serializado com ToJsonString() Lança ArgumentException
JsonNode Serializado com ToJsonString() Lança ArgumentException
POCO registrado Serializado com JsonSerializer.Serialize() Codificação binária com suporte a dicas de tipo e atributos de caminho personalizados
POCO não registrado / objeto anônimo Serializado com JsonSerializer.Serialize() Lança ClickHouseJsonSerializationException
  • String (padrão): Aceita string, JsonObject, JsonNode ou qualquer objeto. Todas as entradas são serializadas com System.Text.Json.JsonSerializer e enviadas como strings JSON para processamento no servidor. Este é o modo mais flexível e funciona sem registro de tipo.

  • Binary: Aceita apenas tipos POCO registrados. Os dados são convertidos no cliente para o formato JSON binário do ClickHouse, com suporte completo a dicas de tipo. Requer chamar connection.RegisterJsonSerializationType<T>() antes do uso. Escrever valores string ou JsonNode nesse modo lança ArgumentException.

// O modo String padrão funciona com qualquer entrada
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// O modo Binary requer habilitação explícita e registro de tipo
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
Colunas JSON tipadas

Quando uma coluna JSON tem dicas de tipo (por exemplo, JSON(id UInt64, price Decimal128(2))), o driver usa essas dicas para serializar valores com total fidelidade aos tipos. Isso preserva a precisão de tipos como UInt64, Decimal, UUID e DateTime64, que, de outra forma, perderiam precisão ao serem serializados como JSON genérico.

Serialização de POCO

POCOs podem ser gravados em colunas JSON de duas formas, dependendo do JsonWriteMode:

Modo String (padrão): os POCOs são serializados por meio de System.Text.Json.JsonSerializer. Não é necessário registrar tipos. Esta é a abordagem mais simples e funciona com objetos anônimos.

Modo binário: os POCOs são serializados usando o formato JSON binário do driver, com suporte completo a type hints. Os tipos devem ser registrados com connection.RegisterJsonSerializationType<T>() antes do uso. Esse modo oferece suporte a mapeamentos de path personalizados por meio de atributos:

  • [ClickHouseJsonPath("path")]: Mapeia uma propriedade para um path JSON personalizado. Útil para estruturas aninhadas ou quando o nome da propriedade difere da chave JSON desejada. Funciona apenas no modo binário.

  • [ClickHouseJsonIgnore]: Exclui uma propriedade da serialização. Funciona apenas no modo binário.

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; }  // Não é serializado
}

// No modo Binary: registre o tipo e habilite o modo Binary
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Inserir POCO - serializado em JSON com estrutura aninhada por meio de atributos de caminho personalizados
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// JSON resultante: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}

A correspondência entre o nome da propriedade e as dicas de tipo da coluna diferencia maiúsculas de minúsculas. Uma propriedade UserId só corresponderá a uma dica definida como UserId, não como userid. Isso está de acordo com o comportamento do ClickHouse, que permite que caminhos como userName e UserName coexistam como campos separados.

Limitações (apenas no modo Binary):

  • Os tipos POCO precisam ser registrados na conexão com connection.RegisterJsonSerializationType<T>() antes da serialização. Tentar serializar um tipo não registrado lança ClickHouseJsonSerializationException.
  • Propriedades de Dicionário e array/lista exigem dicas de tipo na definição da coluna para serem serializadas corretamente. Sem essas dicas, use o modo String.
  • Valores nulos em propriedades POCO só são gravados quando o caminho tem uma dica de tipo Nullable(T) na definição da coluna. O ClickHouse não permite tipos Nullable em caminhos JSON dinâmicos, portanto propriedades nulas sem dica são ignoradas.
  • Os atributos ClickHouseJsonPath e ClickHouseJsonIgnore são ignorados no modo String (eles só funcionam no modo Binary).

Outros tipos

ClickHouse Type Accepted .NET Types Notas
UUID Guid, string string é analisada como Guid
IPv4 IPAddress, string Deve ser IPv4; string é analisada via IPAddress.Parse()
IPv6 IPAddress, string Deve ser IPv6; string é analisada via IPAddress.Parse()
Nothing Qualquer Não grava nada (no-op)
Dynamic Sem suporte (lança NotImplementedException)
Array(T) IList, null null grava um array vazio. Para tipos Nested (Array(Array(T)) e mais profundos), são aceitos tanto formatos irregulares (T[][], List<List<T>>) quanto arrays CLR multidimensionais retangulares (T[,], T[,,], …); o rank do CLR deve corresponder à profundidade de aninhamento no ClickHouse.
Tuple(T1, T2, …) ITuple, IList A quantidade de elementos deve corresponder à aridade da tupla. Veja a ressalva sobre ValueTuple para mais de 7 elementos.
Map(K, V) IDictionary
Nullable(T) null, DBNull, ou tipos aceitos por T Grava o byte indicador de null antes do valor
Enum8 string, sbyte, tipos numéricos string é procurada no dicionário do enum
Enum16 string, short, tipos numéricos string é procurada no dicionário do enum
LowCardinality(T) Tipos aceitos por T Delega ao tipo subjacente
SimpleAggregateFunction Tipos aceitos pelo tipo subjacente Delega ao tipo subjacente
Nested(…) IList de tuplas A quantidade de elementos deve corresponder à quantidade de campos
Variant(T1, T2, …) Valor correspondente a um de T1, T2, … Lança ArgumentException se não houver correspondência de tipo
QBit(T, dim) IList Delega a Array; a dimensão é apenas metadado

Tipos de geometria

Tipo do ClickHouse Tipos .NET aceitos Observações
Point System.Drawing.Point, ITuple, IList (2 elementos)
Ring IList de Point
LineString IList de Point
Polygon IList de Ring
MultiLineString IList de LineString
MultiPolygon IList de Polygon
Geometry Qualquer tipo de geometria acima Variant de todos os tipos de geometria

Não suportado para escrita

Tipo do ClickHouse Notas
Dynamic Lança NotImplementedException
AggregateFunction Lança AggregateFunctionException

Tratamento do tipo Nested

Os tipos aninhados do ClickHouse (Nested(...)) podem ser lidos e gravados usando semântica de arrays.

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

Logging e diagnósticos

O cliente .NET do ClickHouse se integra às abstrações Microsoft.Extensions.Logging para oferecer logging leve e opcional. Quando habilitado, o driver emite mensagens estruturadas para eventos do ciclo de vida da conexão, execução de comandos, operações de transporte e operações de inserção em massa. O logging é totalmente opcional — aplicações que não configuram um logger continuam em execução sem sobrecarga adicional.

Início rápido

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

Usando appsettings.json

Você pode configurar os níveis de log usando a configuração padrão do .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);

Usando configuração em memória

Você também pode configurar o nível de verbosidade do logging por categoria no código:

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

Categorias e emissores

O driver usa categorias específicas para que você possa ajustar com precisão os níveis de log por componente:

Categoria Origem Destaques
ClickHouse.Driver.Connection ClickHouseConnection Ciclo de vida da conexão, seleção da fábrica de clientes HTTP, abertura/fechamento de conexão e gerenciamento de sessão.
ClickHouse.Driver.Command ClickHouseCommand Início/conclusão da execução da consulta, temporização, IDs de consulta, estatísticas do servidor e detalhes de erro.
ClickHouse.Driver.Transport ClickHouseConnection Requisições HTTP streaming de baixo nível, sinalizadores de compressão, códigos de status da resposta e falhas de transporte.
ClickHouse.Driver.Client ClickHouseClient Insert binário, consultas e outras operações
ClickHouse.Driver.NetTrace TraceHelper Rastreamento de rede, somente quando o modo de depuração está habilitado

Exemplo: Diagnóstico de problemas de conexão

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

Isso registrará:

  • Seleção da fábrica do cliente HTTP (pool padrão vs. conexão única)
  • Configuração do handler HTTP (SocketsHttpHandler ou HttpClientHandler)
  • Configurações do pool de conexões (MaxConnectionsPerServer, PooledConnectionLifetime etc.)
  • Configurações de timeout (ConnectTimeout, Expect100ContinueTimeout etc.)
  • Configuração de SSL/TLS
  • Eventos de abertura/fechamento de conexões
  • Rastreamento do ID da sessão

Modo de depuração: rastreamento de rede e diagnósticos

Para ajudar a diagnosticar problemas de rede, a biblioteca do driver inclui um auxiliar que habilita o rastreamento de baixo nível dos componentes internos de rede do .NET. Para habilitá-lo, você deve passar uma LoggerFactory com o nível definido como Trace e definir EnableDebugMode como true (ou habilitá-lo manualmente pela classe ClickHouse.Driver.Diagnostic.TraceHelper). Os eventos serão registrados na categoria ClickHouse.Driver.NetTrace. Aviso: isso gerará logs extremamente detalhados e afetará o desempenho. Não é recomendável habilitar o modo de depuração em production.

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Deve estar no nível Trace para exibir eventos de rede
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Habilita o rastreamento de rede de baixo nível
};

OpenTelemetry

O driver oferece suporte nativo ao rastreamento distribuído com OpenTelemetry por meio da API .NET System.Diagnostics.Activity. Quando habilitado, o driver emite spans para operações de banco de dados que podem ser exportados para backends de observabilidade, como Jaeger ou o próprio ClickHouse (por meio do OpenTelemetry Collector).

Habilitando o rastreamento

Em aplicações ASP.NET Core, adicione o ActivitySource do driver do ClickHouse à configuração do OpenTelemetry:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Assina os spans do driver ClickHouse
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Ou AddJaegerExporter(), etc.

Para aplicativos de console, testes ou configuração manual:

using OpenTelemetry;
using OpenTelemetry.Trace;

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

Atributos de span

Cada span inclui atributos de banco de dados padrão do OpenTelemetry, além de estatísticas de consulta específicas do ClickHouse que podem ser usadas para depuração.

Atributo Descrição
db.system Sempre "clickhouse"
db.name Nome do banco de dados
db.user Nome de usuário
db.statement Consulta SQL (se estiver habilitada)
db.clickhouse.read_rows Linhas lidas pela consulta
db.clickhouse.read_bytes Bytes lidos pela consulta
db.clickhouse.written_rows Linhas gravadas pela consulta
db.clickhouse.written_bytes Bytes gravados pela consulta
db.clickhouse.elapsed_ns Tempo de execução no servidor em nanossegundos

Opções de configuração

Controle o comportamento do rastreamento por meio de ClickHouseDiagnosticsOptions:

using ClickHouse.Driver.Diagnostic;

// Incluir instruções SQL nos spans (padrão: false por segurança)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncar instruções SQL longas (padrão: 1000 caracteres)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;

Configuração de TLS

Ao se conectar ao ClickHouse via HTTPS, você pode configurar o comportamento do TLS/SSL de várias formas.

Validação personalizada de certificados

Para ambientes de produção que exigem uma lógica personalizada de validação de certificados, forneça seu próprio HttpClient com um handler ServerCertificateCustomValidationCallback configurado:

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

var handler = new HttpClientHandler
{
    // Obrigatório quando a compactação está ativada (padrão)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Exemplo: Aceitar a impressão digital de um certificado específico
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Exemplo: Aceitar certificados de um emissor específico
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Padrão: usar a validação padrão
        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);

Suporte a ORMs

ORMs exigem a API ADO.NET (ClickHouseConnection). Para gerenciar corretamente o ciclo de vida da conexão, crie as conexões a partir de um ClickHouseDataSource:

// Registre o DataSource como singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Crie conexões para uso pelo ORM
await using var connection = await dataSource.OpenConnectionAsync();
// Passe a conexão para o seu ORM...

Dapper

ClickHouse.Driver funciona com Dapper. O driver converte automaticamente a sintaxe @parameter do Dapper para a sintaxe nativa {parameter:Type} do ClickHouse, com os tipos inferidos a partir dos valores do .NET.

Use ClickHouseDataSource para gerenciar corretamente o ciclo de vida da conexão:

var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Registrar como singleton na injeção de dependência

using var connection = dataSource.CreateConnection();

Estilos de passagem de parâmetros

Todos os estilos padrão de passagem de parâmetros do Dapper são compatíveis:

Objetos anônimos:

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

Classes do tipo 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);

Dicionário:

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

DynamicParameters (a partir de um dicionário ou de um objeto anônimo):

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

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

Consultas com POCOs

O Dapper mapeia colunas para propriedades pelo nome (sem diferenciar maiúsculas de minúsculas):

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

// A partir de uma tabela
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// A partir de um literal
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();

Sintaxe nativa de parâmetros do ClickHouse

Quando precisar de controle explícito sobre o tipo, use diretamente no SQL a sintaxe {param:Type} do ClickHouse com um Dictionary<string, object> para os valores dos parâmetros. Não combine a sintaxe @param com a sintaxe {param:Type} para o mesmo parâmetro.

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

WHERE IN

A expansão nativa do IN no Dapper funciona:

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

O Dapper reescreve isso como WHERE id IN (@Ids1, @Ids2, @Ids3), e o driver converte cada parâmetro expandido.

O has() do ClickHouse com parâmetro Array também funciona:

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

Manipuladores de tipo personalizados

Alguns tipos do ClickHouse, como ITuple, BigInteger e ClickHouseDecimal, precisam ter manipuladores registrados na inicialização:

// ClickHouseDecimal (para as colunas Decimal64/128/256)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (para as colunas Int128/Int256/UInt128/UInt256)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (para as colunas IPv4/IPv6)
SqlMapper.AddTypeHandler(new IpAddressHandler());

Consulte o exemplo do Dapper para ver uma implementação de exemplo de um manipulador de tipos.

Dapper.Contrib

GetAll<T>() e Get<T>(id) funcionam. Insert<T>() não — ele gera sintaxe do SQL Server (SCOPE_IDENTITY, []). Recomenda-se usar, em vez disso, o método nativo InsertBinaryAsync do ClickHouseClient.

[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);

Os nomes das propriedades devem corresponder exatamente aos nomes de coluna do ClickHouse (diferenciam maiúsculas de minúsculas).

Limitações

O que Status Detalhes
Tuple como resultado Funciona Requer o registro de SqlMapper.TypeHandler<ITuple>
Tuple como parâmetro Não suportado O Dapper não consegue serializar ITuple/Tuple<> como valor de DbParameter
Tipos aninhados como parâmetro Não suportado Pelo mesmo motivo — o Dapper rejeita tipos complexos como valores de parâmetro
Tipos Geo como parâmetro Não suportado Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon
Dapper.Contrib.Insert<T>() Não suportado Gera sintaxe específica do SQL Server
Tipo Nothing Não suportado Sem representação significativa no .NET

Linq2db

Este driver é compatível com o linq2db, um ORM leve e provedor LINQ para .NET. Consulte o site do projeto para obter a documentação detalhada.

Exemplo de uso:

Crie uma DataConnection usando o provedor do ClickHouse:

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

Os mapeamentos de tabelas podem ser definidos usando atributos ou a API fluente. Se os nomes da sua classe e propriedade corresponderem exatamente aos nomes da tabela e da coluna, nenhuma configuração será necessária:

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

Consultando:

await using var db = new DataConnection(options);

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

Cópia em lote:

Use BulkCopyAsync para inserções em lote eficientes.

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

O provedor oficial do Entity Framework Core para ClickHouse. Mapeie classes C# para tabelas do ClickHouse, faça consultas com LINQ e insira dados via SaveChanges — tudo usando os padrões familiares do EF Core.

Instalação

dotnet add package ClickHouse.EntityFrameworkCore

Requer o .NET 10.0 e o EF Core 10.

Início rápido

Defina sua entidade e o DbContext e, em seguida, consulte com 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");
}

// Consulta
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();

Tipos com suporte

Categoria Tipos do ClickHouse Tipos CLR
Inteiros Int8Int64, UInt8UInt64 sbyte, short, int, long, byte, ushort, uint, ulong
Inteiros grandes Int128, Int256, UInt128, UInt256 BigInteger
Pontos flutuantes Float32, Float64, BFloat16 float, double
Decimais Decimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S) decimal ou ClickHouseDecimal
Bool Bool bool
Strings String, FixedString(N) string
Enums Enum8(...), Enum16(...) string ou enum de C#
Data/hora Date, Date32, DateTime, DateTime64(P, 'TZ') DateOnly, DateTime
Time Time, Time64(N) TimeSpan
UUID UUID Guid
Rede IPv4, IPv6 IPAddress
Arrays Array(T) T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
Maps Map(K, V) Dictionary<K,V>
Tuples Tuple(T1, ...) Tuple<...> ou ValueTuple<...>
Variant Variant(T1, T2, ...) object
Dinâmico Dynamic object
JSON Json JsonNode ou string
Geoespaciais Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon, Geometry Tuple<double,double> e arrays correspondentes; object para Geometry
Wrappers Nullable(T), LowCardinality(T) Desempacotados automaticamente

Use ClickHouseDecimal (de ClickHouse.Driver.Numerics) em vez de decimal quando precisar da precisão total de colunas Decimal128/Decimal256 — o decimal do .NET é limitado a 28–29 dígitos significativos.

Operações LINQ compatíveis

Consultas: Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking

GROUP BY e agregações: GroupBy com Count, LongCount, Sum, Average, Min, Max — incluindo HAVING (.Where() após .GroupBy()), várias agregações em uma única projeção e OrderBy com base nos resultados agregados.

JOINs: Join (INNER), padrões GroupJoin/SelectMany (LEFT e CROSS). LEFT JOIN retorna null de fato para linhas sem correspondência (veja semântica de null em LEFT JOIN abaixo).

Subconsultas: Contains / IN correlacionados, Any / EXISTS, All e subconsultas escalares em projeções.

Operações de conjunto: Concat (→ UNION ALL), Union (→ UNION DISTINCT), Intersect, Except.

Coleções locais inline: junções e Contains em coleções em memória (int[], List<T>, etc.) são convertidos em uma série de UNIONs.

Métodos de string: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat (e o operador +).

Funções matemáticas: métodos padrão de Math e MathF traduzidos para seus equivalentes no ClickHouse — funções aritméticas, logarítmicas, trigonométricas e utilitárias.

Semântica de NULL em LEFT JOIN

O provider injeta set_join_use_nulls=1 automaticamente em cada conexão para atender às expectativas do Entity Framework em relação ao comportamento de JOIN.

Se o seu servidor ClickHouse ou profile impedir a alteração dessa configuração (por exemplo, um profile readonly=1), desative isso com:

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

Com o opt-out ativado, o LEFT JOIN retorna os valores padrão das colunas do ClickHouse, e a detecção de navegação baseada em nulos do EF não funciona mais como esperado. Use comparações explícitas com 0 / "" em vez de == null.

Inserção de dados

SaveChanges usa a API nativa InsertBinaryAsync do driver — codificação RowBinary com compressão GZip, muito mais eficiente do que SQL parametrizado:

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();

As entidades passam de Added para Unchanged após salvar, assim como em qualquer outro provedor do EF Core.

O tamanho do lote é configurável (padrão: 1000):

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

Inserção em massa

Para cargas de alta taxa de transferência, use BulkInsertAsync em vez de SaveChanges. Esse é um método de extensão no DbContext que ignora completamente o rastreador de alterações, a resolução de identidade e o gerenciamento de estado do EF Core — ele chama diretamente o InsertBinaryAsync do driver com codificação RowBinary e compressão GZip.

Isso o torna ideal para carregar grandes conjuntos de dados quando você não precisa rastrear entidades após a inserção:

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

A entrada pode ser qualquer IEnumerable<T> — ela processa as entidades em fluxo, sem carregá-las todas na memória. O valor retornado é o número de linhas inseridas. As entidades não ficam vinculadas ao DbContext após a inserção, portanto não há transição de estado de AddedUnchanged.

Enums

As colunas Enum8/Enum16 do ClickHouse podem ser mapeadas como propriedades string ou como tipos enum em C#. Ao usar enums em C#, o provedor converte automaticamente entre o enum e sua representação textual:

public enum Status { Active, Inactive, Pending }

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

// Consulta com valores do enum
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();

Conversões de tipos personalizadas

O sistema ValueConverter do EF Core permite mapear tipos personalizados para tipos que o provedor já suporta. O provedor nunca vê seu tipo personalizado — o EF Core faz a conversão na interface entre os dois.

Conversão por propriedade:

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

// No 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");

Classe de conversor reutilizável:

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] };
    }
}

// Aplicar a uma única propriedade:
.HasConversion<MoneyConverter>()

// Ou a todas as propriedades de um tipo por convenção:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}

Anotações de tipo de coluna

Para tipos escalares como string, int, DateTime etc., o provedor deduz automaticamente o tipo do ClickHouse. Para tipos parametrizados e wrappers, é necessário especificar explicitamente o tipo do ClickHouse.

Usando anotações de dados (atributos):

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

Usando a API fluente no OnModelCreating:

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

Wrappers aninhados como Array(Nullable(Int32)) e LowCardinality(Nullable(String)) são suportados — o provedor desempacota Nullable e LowCardinality automaticamente em todos os níveis de aninhamento.

Colunas Variant e Dynamic

As colunas Variant(T1, T2, ...) e Dynamic do ClickHouse são mapeadas para object no .NET. Como object é genérico demais para a inferência automática de tipos, você deve declarar explicitamente o tipo de armazenamento por meio de .HasColumnType():

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

// No OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// ou:
entity.Property(e => e.Payload).HasColumnType("Dynamic");

Ao ler, o valor é desserializado automaticamente para o tipo .NET correspondente ao discriminador armazenado (por exemplo, string, ulong, ulong[]).

Colunas JSON

O provedor dá suporte ao tipo de coluna Json do ClickHouse, com mapeamento para System.Text.Json.Nodes.JsonNode (principal) ou string (via ValueConverter automático):

using System.Text.Json.Nodes;

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

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

A leitura e a escrita de JSON funcionam tanto com SaveChanges quanto com 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"

Se você preferir strings JSON brutas, mapeie a propriedade como string, com o tipo de coluna Json — o provedor aplica um ValueConverter automaticamente:

public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // JSON bruto como string
}

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

Motores de tabela

Configure os motores de tabela do ClickHouse e as cláusulas específicas de cada motor por meio da API fluente ToTable(name, t => ...). Quando nenhum motor é configurado, o provedor usa MergeTree, com ORDER BY derivado da chave primária da entidade.

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

Famílias de motores compatíveis:

Motor Método fluente Observações
MergeTree HasMergeTreeEngine() Padrão quando nada é configurado
ReplacingMergeTree HasReplacingMergeTreeEngine("Version", "IsDeleted") ou HasReplacingMergeTreeEngine<T>(e => e.Version) Colunas Version / IsDeleted opcionais
SummingMergeTree HasSummingMergeTreeEngine(…) ou HasSummingMergeTreeEngine<T>(e => new { … }) Colunas a somar opcionais
AggregatingMergeTree HasAggregatingMergeTreeEngine()
CollapsingMergeTree HasCollapsingMergeTreeEngine("Sign") ou HasCollapsingMergeTreeEngine<T>(e => e.Sign) A coluna Sign deve ser Int8
VersionedCollapsingMergeTree HasVersionedCollapsingMergeTreeEngine("Sign", "Version") ou <T>(e => e.Sign, e => e.Version)
GraphiteMergeTree HasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, Memory HasLogEngine(), HasTinyLogEngine(), HasStripeLogEngine(), HasMemoryEngine() Sem ORDER BY / PARTITION BY

Cláusulas do motor: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Todas são anexadas ao construtor de motor retornado por HasXxxEngine().

Recursos em nível de coluna: HasCodec, HasTtl, HasComment, HasDefault — todos participam das migrações.

Índices de data skipping — via HasIndex(...).HasSkippingIndexType(...):

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

// Índice com parâmetros (ex.: bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);

Índices padrão (sem skipping) são ignorados silenciosamente, já que não têm equivalente no ClickHouse. Índices únicos geram exceção, pois o ClickHouse não impõe unicidade.

Migrações

Fluxo de trabalho padrão das migrações do EF Core:

dotnet ef migrations add InitialCreate
dotnet ef database update

Operações suportadas:

Operação Emite
CREATE TABLE Inclui cláusula de motor, ORDER BY, PARTITION BY, SETTINGS, codecs/TTL/comentários/valores padrão de coluna
ALTER TABLE ADD COLUMN
ALTER TABLE DROP COLUMN
ALTER TABLE MODIFY COLUMN Lida com alteração de tipo e adição/remoção de anotações (CODEC, TTL, COMMENT, DEFAULT)
ALTER TABLE RENAME COLUMN
RENAME TABLE
ALTER TABLE ADD INDEX / DROP INDEX Somente índices de data skipping
CREATE DATABASE / DROP DATABASE Via EnsureCreated / EnsureDeleted e migrações

Limitações de migração

Recurso Motivo
Chaves estrangeiras O ClickHouse não aplica chaves estrangeiras. As migrações rejeitam AddForeignKey; o validador do modelo emite um aviso na compilação do modelo.
Restrições de unicidade / índices únicos O ClickHouse não aplica unicidade. Índices únicos geram erro no momento da migração.
Valores gerados pelo servidor (auto-incremento / IDENTITY) O ClickHouse não tem equivalente.
colunas Nested(…) Ainda não há suporte como tipo CLR mapeado.
Entidades de propriedade como JSON (.ToJson()) O mapeamento estrutural de JSON para entidades de propriedade ainda não foi implementado. Em vez disso, use JsonNode / string em uma coluna Json (consulte colunas JSON).

Além das migrações, o provedor também ainda não oferece suporte a:

  • UPDATE / DELETE
  • Transações: BeginTransaction é um no-op. Não há suporte a transações ACID no ClickHouse.
  • Tradução de consultas com caminho JSON: entity.Data["key"] em LINQ não é traduzido para a sintaxe SQL data.key do ClickHouse. Aplique filtros em colunas não JSON e inspecione o JSON na memória.

Limitações

Tuple com 8+ elementos e uma tupla aninhada na última posição

Tipos ValueTuple de C# com mais de 7 elementos usam um esquema de aninhamento gerado pelo compilador: o 8º argumento genérico (TRest) é, ele próprio, um ValueTuple que contém os elementos restantes. Por exemplo, (int, int, int, int, int, int, int, string, string) é compilado como ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.

Isso cria uma ambiguidade quando a coluna do ClickHouse é uma tupla de 8 elementos em que o último elemento também é uma tupla — por exemplo, Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). O driver não consegue distinguir entre:

  • Uma tupla plana de 9 elementos (aninhamento TRest gerado pelo compilador)
  • Uma tupla de 8 elementos em que o último elemento é um Tuple(String, String) aninhado

Ambos produzem o mesmo tipo .NET: ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.

O driver trata o 8º argumento como TRest (ou seja, o expande), o que significa que o caso de 8 elementos com tupla aninhada será serializado incorretamente.

Isso afeta tanto System.Tuple quanto ValueTuple, já que ambos usam aninhamento TRest para >7 elementos. Tuple com 7 ou menos elementos, ou Tuple em que o último elemento não é uma tupla, não são afetadas.

Solução alternativa: Envolva a tupla interna em uma camada extra para que o driver consiga distingui-la do aninhamento 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")))

Colunas do tipo AggregateFunction

Colunas do tipo AggregateFunction(...) não podem ser consultadas nem inseridas diretamente.

Para inserir:

INSERT INTO t VALUES (uniqState(1));

Para selecionar:

SELECT uniqMerge(c) FROM t;

Navigation