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.ClickHouseBulkCopyfoi descontinuado e será removido em um lançamento futuro; useClickHouseClient.InsertBinaryAsyncno 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
- Atualize o arquivo
.csprojcom o novo nome do pacoteClickHouse.Drivere a versão mais recente no NuGet. - Atualize todas as referências a
ClickHouse.ClientparaClickHouse.Driverno 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.DriverOu use o Gerenciador de Pacotes NuGet:
Install-Package ClickHouse.DriverIní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=mydbCom configurações personalizadas do ClickHouse
Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000QueryOptions
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:

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

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. Propriedadesrequiredsã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 aSELECT * 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:
ClickHouseTypeexplícito definido no parâmetro- Type hint de SQL da sintaxe
{name:Type}na consulta IParameterTypeResolver(deQueryOptions.ParameterTypeResolver, com fallback paraClickHouseClientSettings.ParameterTypeResolver)- 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:
IParameterFormatter(deQueryOptions.ParameterFormatter, com fallback paraClickHouseClientSettings.ParameterFormatter). Se ele retornar um valor não nulo, esse valor será usado.- 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 DDLExecuteScalarAsync()- Retorna a primeira coluna da primeira linhaExecuteReaderAsync()- Retorna umClickHouseDataReaderpara 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
ClickHouseClientouClickHouseConnectionsã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
-
Use UTC sempre que possível. Armazene timestamps como colunas
DateTime('UTC')e useDateTimeKind.Utcno seu código. Isso elimina ambiguidades de fuso horário. -
Use
DateTimeOffsetpara lidar explicitamente com o fuso horário. Ele sempre representa um instante específico e inclui a informação de offset. -
Especifique o fuso horário nas type hints de SQL. Ao usar parâmetros com valores
DateTimeUnspecifieddestinados 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:
- Use fusos horários explícitos nas definições das colunas:
DateTime('UTC')ouDateTime('Europe/Amsterdam') - 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): RetornaSystem.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 comostring. 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 UTCExemplo: 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:30Recomendaçã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): Aceitastring,JsonObject,JsonNodeou qualquer objeto. Todas as entradas são serializadas comSystem.Text.Json.JsonSerializere 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 chamarconnection.RegisterJsonSerializationType<T>()antes do uso. Escrever valoresstringouJsonNodenesse modo lançaArgumentException.
// 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 idusing 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çaClickHouseJsonSerializationException. - 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 tiposNullableem caminhos JSON dinâmicos, portanto propriedades nulas sem dica são ignoradas. - Os atributos
ClickHouseJsonPatheClickHouseJsonIgnoresã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 = Memoryvar 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.
- NuGet:
ClickHouse.EntityFrameworkCore - Código-fonte: GitHub
Instalação
dotnet add package ClickHouse.EntityFrameworkCoreRequer 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 | Int8–Int64, UInt8–UInt64 |
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 Added → Unchanged.
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 updateOperaçõ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 SQLdata.keydo 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;