-
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.
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:Início rápido
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.
Configurações de conexão
Formato e serialização de dados
Gerenciamento de sessão
O sinalizador
UseSession habilita a persistência da sessão do servidor, permitindo usar instruções SET e tabelas temporárias. As sessões serão redefinidas após 60 segundos de inatividade (timeout padrão). A duração da sessão pode ser estendida definindo configurações de sessão por meio de instruções do ClickHouse ou da configuração do servidor.A classe ClickHouseConnection normalmente permite operação paralela (várias threads podem executar consultas concorrentemente). No entanto, habilitar o sinalizador UseSession limitará isso a uma consulta ativa por conexão a qualquer momento (esta é uma limitação do servidor).Segurança
Configuração do cliente HTTP
Logging e depuração
Configurações personalizadas e roles
Ao usar uma string de conexão para definir configurações personalizadas, use o prefixo
set_, por exemplo, “set_max_threads=4”. Ao usar um objeto ClickHouseClientSettings, não use o prefixo set_.Para ver a lista completa de configurações disponíveis, consulte aqui.Exemplos de string de conexão
Conexão básica
Com configurações personalizadas do ClickHouse
QueryOptions
QueryOptions permite substituir configurações do cliente individualmente para cada consulta. Todas as propriedades são opcionais e só substituem os padrões do cliente quando especificadas.
Exemplo:
InsertOptions
InsertOptions estende QueryOptions com configurações específicas para operações de inserção em massa via InsertBinaryAsync.
Todas as propriedades de
QueryOptions também estão disponíveis em InsertOptions.
Exemplo:
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:
UseSchemaCache = true para consultar o esquema uma única vez e reutilizá-lo nas inserções subsequentes na mesma instância do ClickHouseClient:
ColumnTypestem prioridade sobreUseSchemaCache. Se ambos estiverem definidos, os tipos explícitos serão usados.- O cache de esquema não detecta alterações feitas com
ALTER TABLE. Se você modificar o esquema da tabela, crie um novoClickHouseClientou evite usarUseSchemaCachepara essa tabela. - O cache tem escopo na instância de
ClickHouseCliente é indexado por (banco de dados, tabela). Diferentes subconjuntos de colunas da mesma tabela compartilham um único esquema em cache.
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 umClickHouseClient 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:
ClickHouseClientSettings:
IHttpClientFactory:
ClickHouseClient foi projetado para ter longa vida útil e ser compartilhado em toda a aplicação. Crie-o uma única vez (normalmente como um singleton) e reutilize-o em todas as operações do banco de dados. O cliente gerencia internamente o pool de conexões HTTP.Executando consultas
UseExecuteNonQueryAsync para instruções que não retornam resultados:
ExecuteScalarAsync para obter um único valor:
Inserção de dados
Inserções parametrizadas
Insira dados por meio de consultas parametrizadas comExecuteNonQueryAsync. Os tipos dos parâmetros devem ser especificados no SQL usando a sintaxe {name:Type}:
Inserção em massa
UseInsertBinaryAsync 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.
InsertOptions:
- O cliente obtém automaticamente a estrutura da tabela por meio de
SELECT * FROM <table> WHERE 1=0antes da inserção. Os valores fornecidos devem corresponder aos tipos das colunas de destino. Para ignorar essa consulta, useInsertOptions.ColumnTypesouInsertOptions.UseSchemaCache. - Quando
MaxDegreeOfParallelism > 1, os lotes são enviados em paralelo. As sessões não são compatíveis com inserção em paralelo; desative as sessões ou definaMaxDegreeOfParallelism = 1. - Use
RowBinaryFormat.RowBinaryWithDefaultsemInsertOptions.Formatse quiser que o servidor aplique valores DEFAULT às colunas não fornecidas.
Inserções com POCO
Em vez de construir arraysobject[], você pode inserir diretamente objetos POCO com tipagem forte. Registre o tipo uma vez e, em seguida, passe IEnumerable<T>:
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[].
Diferentemente da sobrecarga
object[], InsertBinaryAsync<T> não aceita uma lista explícita de colunas. As colunas são determinadas pelas propriedades mapeadas do tipo registrado. Para controlar quais colunas são inseridas, use [ClickHouseNotMapped] para excluir propriedades ou [ClickHouseColumn(Name = "...")] para renomeá-las.Se ColumnTypes estiver definido em InsertOptions, eles substituirão os atributos do POCO.Evolução do esquema
As inserções com 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 comDEFAULT (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.
Posicionamento da consulta de inserção
Um insert binário escreve sua instruçãoINSERT INTO ... FORMAT ... na primeira linha do corpo da requisição, antes das linhas de dados. O corpo é comprimido por padrão, de modo que mecanismos de roteamento e logging que inspecionam apenas a URL não enxergam a instrução. Defina InsertOptions.QueryPlacement como InsertQueryPlacement.Url para enviar a instrução no parâmetro de URL query, deixando o corpo somente para as linhas:
query, ou quando você quiser a instrução nos logs de acesso e em ferramentas de observabilidade. É opt-in porque, nesse caso, a instrução passa a contar para o comprimento da URL. O limite efetivo é o menor entre os impostos pelo runtime do .NET, por um intermediário e pelo servidor. Do .NET 6 ao .NET 9, System.Uri limita a URI de requisição completa e codificada a 65.519 caracteres; o driver lança uma InvalidOperationException que o direciona de volta para InsertQueryPlacement.Body quando esse limite é excedido. O http_max_uri_size do ClickHouse é de 1 MiB por padrão, mas um intermediário pode impor um limite menor. No modo body, a instrução e as linhas não têm esse limite de comprimento de URL; outras opções da requisição ainda podem aparecer na URL.
A configuração é independente de Compressor: o corpo é codificado da mesma forma nos dois modos.
Lendo dados
UseExecuteReaderAsync 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.
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, useQueryAsync<T>:
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.
InvalidOperationException. Portanto, uma propriedade object aceita qualquer coluna.
QueryAsync<T> lê cada uma dessas colunas diretamente em uma propriedade correspondente:
Cada linha também aceita a forma anulável do seu tipo de propriedade (
long?, DateOnly? e assim por diante),
independentemente de a coluna ser Nullable(...) ou não. Uma propriedade de tipo por valor não anulável em uma coluna
Nullable(T) é aceita no registro, mas lança uma exceção quando chega um NULL.
Wrappers como LowCardinality(T), SimpleAggregateFunction(f, T) e Object(T) são mapeados exatamente como T.
Colunas compostas também são suportadas e assumem o tipo do framework indicado na
referência de tipos de leitura: Array(T) para T[], Tuple(...)
para System.Tuple<...>, Nested(...) para Tuple<...>[], JSON para JsonObject (ou string
sob JsonReadMode=String) e Variant/Dynamic para object.
Uma coluna Map(K, V) é um caso especial: uma propriedade List<KeyValuePair<K, V>> ou KeyValuePair<K, V>[]
é lida pelo caminho sem boxing e preserva a ordem em wire e quaisquer chaves repetidas, em qualquer
MapReadMode. Já uma propriedade Dictionary<K, V> funciona apenas no modo padrão.
Os tipos de chave e de valor devem corresponder exatamente, portanto
Map(String, Nullable(Int32)) requer KeyValuePair<string, int?>.
Quando uma coluna oferece mais de um tipo de propriedade (uma coluna DateTime como DateTime,
DateTimeOffset ou DateOnly; uma coluna String como string ou byte[]), o tipo de propriedade declarado define a representação. Essas representações alternativas
pertencem ao caminho POCO, portanto estão disponíveis em QueryAsync<T>, mas não em MapTo<T>.
Ao iterar manualmente sobre um leitor, use ClickHouseDataReader.MapTo<T>() para materializar a linha atual em um POCO registrado sem avançar o leitor:
MapTo<T> quando você mesmo precisar controlar o laço do leitor — por exemplo, para combinar acesso bruto às colunas
com materialização de POCO. Ele lê a linha por meio dos valores boxed do leitor, portanto não oferece
os tipos de propriedade alternativos citados acima, e aloca mais do que QueryAsync<T>. Prefira
QueryAsync<T> quando você precisar apenas das linhas; consulte
escolha o caminho de materialização para ver os números.
Um conversor de valores de leitura definido no nível do cliente ou por consulta se aplica a ambos os caminhos e
não desativa a leitura sem boxing. O driver converte cada coluna na sobrecarga correspondente à forma como
a coluna foi lida: o ConvertValue<T> tipado para uma coluna
sem boxing e o ConvertValue com boxing para uma coluna composta. Implemente as duas sobrecargas
de forma consistente; caso contrário, a mesma coluna produzirá resultados diferentes em caminhos diferentes.
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:
Os parâmetros SQL de ‘bind’ são passados como parâmetros de consulta do URI HTTP, portanto o uso excessivo deles pode resultar em uma exceção de “URL too long”. Use
InsertBinaryAsync para inserção de dados em massa e evitar essa limitação.Placeholders @name no estilo ADO
O driver também aceita placeholders @name, emitidos por ORMs como o Dapper. Trata-se de uma
conveniência do lado do cliente: antes do envio da requisição, cada um é reescrito como
{name:ResolvedType}, de modo que o servidor nunca vê um @. Consulte
resolução de tipos para saber como o tipo é escolhido. Sempre que possível,
use a forma explícita {name:Type}.
Um @name sem parâmetro correspondente é mantido intacto, para que o servidor o rejeite. A
correspondência diferencia maiúsculas de minúsculas, portanto @ID não faz bind de um parâmetro chamado id.
Para desativar essa reescrita, defina o switch de AppContext
ClickHouse.Driver.DisableReplacingParameters
antes do primeiro uso do driver. Apenas a reescrita do texto é interrompida; os parâmetros continuam
sendo enviados, de modo que consultas escritas com a sintaxe nativa {name:Type} continuam funcionando.Parâmetros do tipo Identifier
O tipo de parâmetroIdentifier 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":
ID da consulta
Cada consulta recebe umquery_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:
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:
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:
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)
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:
IParameterFormatter personalizado para casos avançados:
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.
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:
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:
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.
Qual sobrecarga é executada:
Ambas as sobrecargas devem ser consistentes entre si, porque a que o driver chama depende de como o chamador leu a
coluna:
ConvertValue<T>— os acessadores tipadosGetByte,GetSByte,GetInt16/32/64,GetUInt16/32/64,GetFloat,GetDouble,GetGuid,GetDateTime,GetIPAddress,GetBigIntegereGetFieldValue<T>, além de todas as colunas sem boxing no caminho de leitura POCO.ConvertValue(com boxing) —GetValue,GetValues, os indexadores,GetChar,GetTuplee os caminhos de coerção emGetBoolean,GetDecimaleGetString.
IsDBNull não executa nenhum conversor: ele lê o indicador de nulo diretamente, portanto um conversor nunca pode
alterar se um valor conta como nulo. TryGetEnumOrdinal também o ignora — veja
lendo o ordinal de um enum.
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
UseExecuteRawResultAsync 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:
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 negociazstd, lz4, gzip, deflate quando Compression=true (o padrão da string de conexão) e decodifica o fluxo por conta própria, 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).
Configuração do HttpClient
Não há nada a configurar: oHttpClient construído pelo driver mantém AutomaticDecompression em DecompressionMethods.None e o próprio driver decodifica as respostas, de modo que o Content-Encoding nunca é removido sem o seu conhecimento e o corpo bruto chega até você exatamente como o servidor o enviou.
Corpos de erro
Quando o servidor responde com um 4xx/5xx eenable_http_compression=1 foi definido, ele compacta o corpo do erro com o mesmo codec que usaria em uma resposta bem-sucedida. O driver decodifica esses corpos para todos os codecs que suporta (lz4, zstd, gzip, deflate, br/brotli), para que a mensagem em ClickHouseServerException seja legível. Para qualquer outro caso (snappy, …), ele retorna uma mensagem substituta que informa o codec e aponta para system.query_log, onde está o texto original do erro.
Descompressão da resposta
Accept-Encoding apenas pede ao servidor que comprima a resposta — algo ainda precisa decodificá-la. O próprio driver faz isso, com base no Content-Encoding da resposta, de modo que todas as APIs normais de leitura (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) funcionam com uma resposta comprimida sem nenhuma configuração adicional. Ele decodifica lz4, zstd, gzip, deflate e br; snappy não é suportado.
Por padrão, o driver anuncia zstd, lz4, gzip, deflate, e o ClickHouse responde com zstd. Para escolher outra opção, defina o Accept-Encoding manualmente — para todo o cliente:
ClickHouseClientSettings:
enable_http_compression=1 na URL, o que o ClickHouse exige antes de sequer respeitar o cabeçalho — inclusive quando UseCompression é false, já que nomear um codec explicitamente é interpretado como um pedido de compressão. Sem nenhum valor definido, UseCompression=false não envia nenhum Accept-Encoding.
Accept-Encoding pode ser definido em quatro lugares. Prevalece o primeiro deles que nomear um codec:
QueryOptions.AcceptEncoding(ouClickHouseCommand.AcceptEncoding)CustomHeaders["Accept-Encoding"]na consultaCustomHeaders["Accept-Encoding"]no clienteClickHouseClientSettings.AcceptEncoding, ou a palavra-chave de string de conexãoAcceptEncoding
identity.
Quem escolhe o codec é o servidor, não o cliente. O ClickHouse examina o Accept-Encoding em busca de tokens seguindo sua própria ordem fixa de preferência — zstd > br > lz4 > snappy > gzip > deflate — e ignora tanto a ordem em que você os lista quanto quaisquer q-values. Portanto, o cabeçalho é um anúncio de capacidades, não uma exigência, e a única forma de influenciar a escolha é decidir quais tokens deixar de fora. O padrão inclui zstd, então uma consulta padrão é respondida com zstd; os tokens restantes funcionam como fallback. br é decodificável, mas não é anunciado por padrão.
A comparação entre os codecs em tamanho de payload, CPU do servidor e CPU do cliente depende dos seus dados, do seu link e do http_zlib_compression_level do servidor (padrão de fábrica: 3) — veja Ajuste da compressão.
http_zlib_compression_level. Essa configuração se aplica a todos os codecs HTTP, e o valor padrão é 3. Esse valor deve ser ajustado conforme seus dados, a velocidade do link e o uso de CPU.- Um cliente CPU-bound em um link rápido. O driver decodifica o corpo da resposta na thread chamadora, portanto, quando a rede não é o gargalo, a velocidade de decodificação no lado do cliente pode se tornar o fator limitante.
Content-Encoding assim indicar, independentemente do que foi solicitado: se estiver ausente ou for identity, o conteúdo passa intacto; se for um codec suportado, é decodificado; e qualquer outro valor gera um erro que o identifica. Não há risco de decodificação dupla — se o AutomaticDecompression de um handler fornecido pelo chamador já tiver decodificado o corpo, ele também remove o Content-Encoding, de modo que o driver vê o conteúdo em texto simples e não o altera.
Resultados brutos não anunciam nenhum codec. ExecuteRawResultAsync (e os públicos PostStreamAsync / InsertRawStreamAsync) entregam o corpo a você tal como veio, portanto, a menos que você mesmo indique um codec, eles não solicitam nenhum — nada no driver decodifica um corpo desse tipo, de modo que oferecer um codec ali transformaria silenciosamente uma exportação em um arquivo comprimido. A regra, portanto, é simples e independe de como o HttpClient esteja configurado: um corpo sem processamento chega exatamente como o servidor o enviou, e o servidor envia texto simples a menos que você peça um codec. Pedir um (para todo o client ou por consulta) é a forma de exportar bytes comprimidos de propósito.
Um AcceptEncoding explícito (em qualquer um dos níveis) continua valendo para requisições brutas, e ClickHouseRawResult.ReadDecompressedStreamAsync() decodifica o resultado quando você quiser isso; ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync e CopyToAsync sempre retornam os bytes exatamente como chegaram.
leaveOpen, de modo que descartá-lo mantém a resposta intacta; quando ela não está comprimida, você recebe o próprio stream de conteúdo HTTP, e descartá-lo encerra o corpo. Em qualquer um dos casos, o ClickHouseRawResult é o dono da resposta — não chame seus outros membros de leitura depois que o stream tiver sido descartado. Descartar o ClickHouseRawResult é sempre obrigatório e, por si só, suficiente: isso libera tanto a resposta quanto qualquer decoder inserido aqui (decoders mantêm buffers do pool). Portanto, o await using acima é opcional, mas é seguro mantê-lo. Chamadas sequenciais repetidas devolvem o mesmo stream; o tipo não é seguro para uso concorrente.
Veja Select_007_ResponseCompression.cs para um exemplo executável.
Compressão de insert (requisição)
Zstd é o codec padrão para inserts:InsertOptions.Compressor tem como valor inicial ZstdCompressor.Default,
que corresponde ao zstd no nível 3. Defina outro compressor para alterar o codec, ou null para enviar o
corpo sem compressão.
Default e um construtor que recebe um nível
e o tamanho do write buffer:
Compartilhe instâncias de compressor. Cada
Default é uma única instância compartilhada, e os quatro compressores
podem ser usados com segurança por várias threads ao mesmo tempo — que é justamente o que acontece quando
InsertOptions.MaxDegreeOfParallelism é maior que 1, já que cada insert usa um compressor por
batch. Nenhum deles implementa IDisposable. Crie sua própria instância uma única vez e reutilize-a, da
mesma forma que Default é usado.IClickHouseCompressor é público, e uma implementação precisa fornecer apenas dois membros:
Content-Encoding que você indicar. Os demais membros —
Decompress, MethodByte, MaxEncodedLength, Encode e Decode — têm implementações
padrão que lançam NotSupportedException, portanto sobrescreva apenas os que seu codec precisar.
Implemente Decompress para decodificar corpos de resposta além de comprimir requisições, e lance
InvalidDataException a partir do stream que ele retorna quando um corpo estiver corrompido ou em formato incorreto.
InsertOptions.Compressor rege apenas o insert binário. Os demais corpos de requisição do driver são comprimidos por regras diferentes, e nenhum deles passa por ele:
- Toda requisição de texto SQL (
ExecuteReaderAsync,ExecuteScalarAsync,ExecuteNonQueryAsync,QueryAsync<T>,ExecuteRawResultAsync, a camada ADO.NET) envia sua instrução comContent-Encoding: gzipsempre queUseCompressionfortrue— ou seja, por padrão. O codec não é configurável:AcceptEncodingcontrola apenas a resposta, então a escolha é gzip ou nada. ComCompression=false, a instrução é enviada sem compressão. As instruções são pequenas, então isso raramente merece atenção — mas é bom saber quando você estiver observando requisições em um proxy ou em uma captura de pacotes. - Um corpo multipart — uma consulta cujos parâmetros são enviados como form data (
UseFormDataParameters=true) — é sempre enviado sem compressão, independentemente do que digaUseCompression. - Um upload bruto (
InsertRawStreamAsync,PostStreamAsync) usa sua própria flag por chamada e não consulta nemUseCompressionnemInsertOptions.Compressor: gzip quando a flag está definida, sem compressão caso contrário. Observe que o parâmetrouseCompressiondeInsertRawStreamAsynctem valor padrãotrue, então um upload bruto é comprimido com gzip a menos que você passefalse— mesmo comCompression=falseno client.
Ajustando a compressão
A compressão troca CPU por bytes. Se essa troca compensa depende quase inteiramente da velocidade do seu link em relação à velocidade de execução do codec. Não existe uma configuração adequada para todos.O único número que decide
Comprimir vale a pena desde que o codec seja mais rápido que a rede. Esse limite é mais baixo do que a maioria das pessoas imagina no caminho de leitura, porque o ClickHouse comprime as respostas HTTP em thread única no buffer de saída. Medido em um service do ClickHouse Cloud com 16 vCPUs (hits, RowBinary, nível 3), o servidor produz saída comprimida a aproximadamente 100-200MB/s.
Portanto, para um resultado grande, e supondo que apenas uma consulta seja processada por vez, a compressão deixa de compensar por volta de 100 MB/s. Um único stream HTTPS
dentro de uma mesma região de nuvem costuma superar esse valor, enquanto qualquer tráfego que atravesse a internet pública, uma VPN ou a fronteira entre regiões normalmente fica abaixo dele.
O caminho de insert tolera compressão em links mais rápidos, porque o client comprime em um core próprio e costuma ser mais rápido que a compressão de resposta do servidor.
Guia aproximado por tipo de implantação
Três aspectos que esta tabela não contempla:
- Custo de egress: se você é cobrado pela transferência de dados, os bytes têm um preço que vai além da latência, e isso favorece uma compressão mais alta independentemente da velocidade do link.
- Resultados pequenos: tudo o que foi dito acima vale para payloads grandes. Em respostas pequenas, o codec quase não importa e a sobrecarga por requisição é o que predomina.
- Inserts em paralelo elevam os limiares de insert. Todos os números de throughput acima se referem a uma única thread.
InsertOptions.MaxDegreeOfParallelismtem1como valor padrão, mas aumentá-lo faz com que os batches sejam comprimidos de forma concorrente, de modo que a taxa agregada de codificação do cliente escala aproximadamente com os núcleos que você disponibilizar. Ou seja, em um link rápido, ainda pode valer a pena comprimir um insert paralelo bem depois do ponto em que um insert de thread única deixa de compensar. Trate as linhas de insert da tabela como um piso e, se você já faz batches em paralelo, refaça os testes antes de concluir que seu link é rápido demais para compressão.
Escolhendo um codec
Níveis
A compressão da resposta é controlada por uma única configuração de servidor,http_zlib_compression_level, que se aplica a todos os codecs HTTP, não apenas ao zlib. O padrão é 3.
Não mexa nela a menos que tenha medições que justifiquem. Acima do padrão, ganha-se muito pouco em tamanho ao custo de muita CPU (para zstd, 3 → 6 praticamente dobra a CPU do servidor em troca de ~14% menos bytes), e o br se torna patológico. Abaixo dele, no nível 1, o cenário muda de verdade: o lz4 fica muito mais barato e o zstd perde sua vantagem de CPU sobre ele. Defina o valor por consulta, se necessário:
Medindo seu próprio ponto de cruzamento
A maneira mais rápida de otimizar a escolha do codec e do nível de compressão é medir o tempo da mesma consulta com alguns codecs e comparar os resultados.ProfileEvents a partir de system.query_log — defina
QueryOptions.QueryId para conseguir localizar a linha:
LIMIT n isolado, sem ORDER BY, retorna linhas diferentes
a cada execução, de modo que cada repetição comprime dados diferentes e as razões viram ruído. Compare
sempre contra um result set fixo.
Inserção via raw stream
UseInsertRawStreamAsync para inserir dados diretamente de arquivos ou de streams em memória em formatos como CSV, JSON, Parquet ou qualquer formato suportado pelo ClickHouse.
Inserir a partir de um arquivo CSV:
Consulte a documentação de configurações de formato para ver as opções que controlam o comportamento da ingestão de dados.
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 deClickHouseConnection, 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 umClickHouseDataSource 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.
Usando o ClickHouseCommand
Crie comandos usando uma conexão para executar SQL:ExecuteNonQueryAsync()- Para instruções INSERT, UPDATE, DELETE e DDLExecuteScalarAsync()- Retorna a primeira coluna da primeira linhaExecuteReaderAsync()- Retorna umClickHouseDataReaderpara percorrer os resultados
Usando ClickHouseDataReader
OClickHouseDataReader fornece acesso tipado aos resultados da consulta:
Lendo o ordinal de um enum
Uma colunaEnum8 ou Enum16 é materializada como seu label: GetFieldType informa string, e GetString, GetValue e GetFieldValue<string> retornam o label. Os accessors numéricos lançam InvalidCastException em uma coluna enum, porque o valor armazenado é uma string.
Use TryGetEnumOrdinal para obter o número por trás do label:
true e define value para colunas Enum8/Enum16 e para colunas
Nullable(Enum...) cuja célula não seja NULL. Retorna false, com value definido como 0, para células NULL ou para
qualquer coluna que não seja um enum. O ordinal é o
valor com sinal obtido do wire, portanto pode ser negativo, e um ordinal Enum16 pode ser maior que um
byte.
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.
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:
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 viaCustomSettings ou pela connection string:
wait_for_async_insert):
Configurações principais:
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)
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
Tipos de ponto flutuante
Tipos decimais
A conversão de tipos decimais é controlada pela configuração UseCustomDecimals.
Tipo booleano
Tipos String
Por padrão, as colunas
String e FixedString(N) são retornadas como string. Defina ReadStringsAsByteArrays=true na string de conexão para lê-las como byte[]. Isso é útil ao armazenar dados binários que podem não estar em UTF-8 válido.A configuração também se aplica a strings aninhadas dentro de outros tipos, de modo que Array(String) é lido como byte[][]
e Map(String, String) como Dictionary<byte[], byte[]> — inclusive as chaves. A única exceção é uma
coluna JSON, cujas folhas de string são sempre texto; veja Tipo JSON.Tipos de data e hora
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:
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:
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
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.
None é um terceiro modo. Ele faz a leitura exatamente como o Binary, mas não envia nenhuma server setting junto com a
consulta — use-o em uma connection que não tem permissão para definir uma.
Um path declarado no column type é um typed path; qualquer outro path do documento é um
dynamic path. Os dois se diferenciam quando o valor é nulo.
Um typed path sempre aparece no JsonObject. Declarado como Nullable(T) ou Dynamic, ele é retornado
como um JSON null tanto quando o valor armazenado é nulo quanto quando o documento não possui esse path — os dois
casos são indistinguíveis:
JSON(x String)
resulta em {"x":""} e JSON(x Int64) resulta em {"x":0}.
Um caminho dinâmico cujo valor é nulo é removido por completo do objeto, de modo que ContainsKey retorna
false para ele. Ler {"x":null} de uma coluna JSON simples resulta em {}.
Caminhos tipados aninhados criam seus parents, portanto JSON(a.b Nullable(Int64)) produz {"a":{"b":null}}
mesmo para um documento vazio.
É isso que o próprio servidor renderiza, portanto os modes
Binary e String agora coincidem. Antes da 1.4.0, um
caminho tipado contendo null era removido do JsonObject, o que fazia {"x":null} ser lido como
{} — e, para um caminho aninhado como JSON(a.b Nullable(Int64)), toda a subárvore a desaparecia.ReadStringsAsByteArrays — JsonValue não possui uma forma de array de bytes, portanto um byte[] seria
renderizado como base64. Isso vale para String, FixedString e para os tipos encapsulados em
LowCardinality, Nullable ou SimpleAggregateFunction, além das strings dentro de Array e Map,
incluindo as chaves do map.
Um array de bytes cujo tipo o leitor JSON não consegue identificar continua sendo renderizado como base64: um
typed path
Variant ou Dynamic guarda um valor cujo tipo só é conhecido linha a linha, então uma string
sob Variant(Array(UInt8), String) retorna codificada em Base64. Isso ocorre da mesma forma em ambas as configurações.Um tipo de chave de map JSON que não seja exatamente String — Map(LowCardinality(String), String), por
exemplo — lança NotSupportedException.JSON(a Int64, a.b Int64). Ambos os caminhos estão presentes em todas as linhas, portanto o servidor
renderiza a linha com uma chave duplicada: {"a":0,"a":{"b":7}}. Um JsonObject não pode conter dois valores
para uma mesma chave, então JsonReadMode.Binary lança uma SerializationException indicando os dois caminhos. O
mesmo vale quando o valor é um Map, como em JSON(a Map(String, Int64)) lido de uma linha que
também tem um a.b dinâmico.
Isso se aplica apenas quando ambos os lados contêm um valor naquela linha. O lado que não contém nada — um null,
um objeto vazio ou uma subárvore cujos valores são todos null — cede lugar ao lado que tem os dados,
qualquer que seja o caminho enviado primeiro pelo servidor. Portanto, uma sobreposição declarada com tipos Nullable preenche um lado por linha e é lida sem
erro: JSON(a Nullable(Int64), a.b Nullable(Int64)) produz {"a":5} e {"a":{"b":7}}, conforme
esperado.
Leia essa coluna com JsonReadMode.String para obter o texto JSON do servidor inalterado, incluindo a chave
duplicada.
Defina AllowDuplicateJsonKeys para continuar lendo a coluna como um JsonObject em vez de lançar uma exceção. Nesse caso, o
driver mantém, entre os dois, o valor que vier por último na linha e descarta o outro, de modo que o
resultado é com perda: JSON(a Int64, a.b Int64) contendo {"a.b":7} é lido como {"a":0}. Um caminho que
contém um valor e cujo pai contém um scalar ou um array continua lançando exceção, porque não há como
colocar uma subárvore sob nenhum dos dois.
Map type
Um
Map(K, V) do ClickHouse é fisicamente um Array(Tuple(K, V)) e pode conter várias entradas com a mesma chave. Um Dictionary não pode; por isso, no modo padrão, uma chave repetida mantém apenas o último valor e os pares anteriores são descartados. A configuração MapReadMode define a representação:
-
Dictionary(padrão): retornaDictionary<K, V>. -
KeyValuePairs: retornaList<KeyValuePair<K, V>>na ordem em que o servidor enviou os pares, preservando todos eles, inclusive as entradas que repetem uma chave.
Map, portanto também se aplica a GetFieldValue<T>, aos tipos de schema que o driver informa e ao mapeamento de propriedades POCO. Ele vale onde quer que um map apareça na árvore de tipos de uma coluna — incluindo Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) e Dynamic.
Ambas as representações são aceitas no caminho de gravação em qualquer um dos modes — consulte gravação de maps.
Outros tipos
Os tipos Dynamic e Variant serão convertidos para o tipo correspondente ao tipo subjacente real de cada linha.
Tipos de geometria
O tipo Geometry é um Variant que pode conter qualquer um dos tipos de geometria. Ele será convertido para o tipo correspondente.
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
Tipos de ponto flutuante
Tipo booleano
Tipos String
Tipos de data e hora
Valores fora do intervaloNo caminho de gravação binária, valores de
Date, Date32, DateTime e DateTime32 fora do intervalo suportado lançam ArgumentOutOfRangeException no momento de Write, indicando o tipo da coluna e o intervalo suportado. Anteriormente, valores fora do intervalo podiam ser truncados silenciosamente por meio de um inteiro de 32 bits e reinterpretados pelo servidor, produzindo timestamps reais, mas incorretos.DateTime.Kind ao gravar valores:
Os valores de
DateTimeOffset sempre preservam o instante exato.
Exemplo: DateTime UTC (instante preservado)
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 valoresUnspecified 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:
Tipos Decimal
Tipo JSON
O comportamento ao escrever JSON é controlado pela configuração
JsonWriteMode:
-
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.
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.
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.
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
Tipos de geometria
Não suportado para escrita
Tratamento do tipo Nested
Os tipos aninhados do ClickHouse (Nested(...)) podem ser lidos e gravados usando semântica de arrays.
Logging e diagnósticos
O cliente .NET do ClickHouse se integra às abstraçõesMicrosoft.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
Usando appsettings.json
Você pode configurar os níveis de log usando a configuração padrão do .NET:Usando configuração em memória
Você também pode configurar o nível de verbosidade do logging por categoria no código:Categorias e emissores
O driver usa categorias específicas para que você possa ajustar com precisão os níveis de log por componente:Exemplo: Diagnóstico de problemas de conexão
- 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 classeClickHouse.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.
OpenTelemetry
O driver oferece suporte nativo ao rastreamento distribuído com OpenTelemetry por meio da API .NETSystem.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 oActivitySource do driver do ClickHouse à configuração do OpenTelemetry:
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.Opções de configuração
Controle o comportamento do rastreamento por meio deClickHouseDiagnosticsOptions:
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óprioHttpClient com um handler ServerCertificateCustomValidationCallback configurado:
Considerações importantes ao fornecer um HttpClient personalizado
- Descompressão automática: deixe
AutomaticDecompressiondesativado. O próprio driver decodifica as respostas comprimidas, portanto ele não é necessário — e habilitá-lo trabalha contra você no lado da requisição: no momento do envio, o handler também adiciona todos os algoritmos de sua máscara aoAccept-Encodingde saída, ampliando o que o driver havia anunciado, de modo que o ClickHouse pode responder com um codec que você não solicitou. Consulte Descompressão de respostas. - Tempo limite de inatividade: Defina
PooledConnectionIdleTimeoutcom um valor menor que okeep_alive_timeoutdo servidor (10 segundos para ClickHouse Cloud) para evitar erros de conexão causados por conexões semiabertas.
Tuning de desempenho
Esta seção descreve como usar o client para obter o melhor desempenho possível, além das diversas opções que você pode ajustar para deixar o client mais performático no seu caso de uso específico.Visão geral
| Se você | Faça isto | |---|---|---| | Lê linhas para POCOs | UseQueryAsync<T>, e não MapTo<T> |
| Faz inserções grandes | Aumente o InsertOptions.BatchSize |
| Executa um aplicativo de console ou worker com muitas inserções | Ative o Server GC |
| Lê resultados grandes pela rede | Mantenha a compressão da resposta ativada (padrão) |
| Insere por uma conexão rápida | Experimente InsertOptions.Compressor = null |
| Insere na mesma tabela muitas vezes | Use UseSchemaCache ou ColumnTypes |
| Lê resultados muito grandes | Aumente o ReadBufferSize |
Leitura: escolha o caminho de materialização
Há três maneiras de obter uma linha de um resultado, e elas não têm o mesmo custo. Alguns desses caminhos fazem boxing dos resultados, o que aumenta as alocações e reduz o desempenho.
Para uma leitura de 1.000.000 de linhas em 105 colunas do dataset hits:
ORMs seguem o caminho rápido quando usam accessors tipados. O linq2db registra
GetInt64,
GetDouble e GetDateTime para cada coluna, portanto faz a leitura sem boxing. Código que lê por meio de
GetValue (incluindo um resultado dynamic do Dapper) aplica boxing a cada valor. Se uma consulta de ORM for muito frequente
e ler por meio de GetValue, use QueryAsync<T> para essa consulta específica.Inserção: tamanho do lote e paralelismo
O tamanho do lote é o principal fator de controle sobre o throughput de inserção.InsertOptions.BatchSize tem
valor padrão de 100.000 linhas.
Use lotes grandes. Em uma inserção de 1.000.000 de linhas, aumentar de 10.000 para 100.000 linhas por
lote resultou em:
Se você não puder controlar o tamanho do lote (por exemplo, quando muitos produtores pequenos enviam linhas de forma independente), use async inserts e deixe o servidor fazer o batching.
Uploads paralelos.
InsertOptions.MaxDegreeOfParallelism tem valor padrão 1. Aumente-o para enviar
lotes simultaneamente. O ganho é maior quando a compressão está ativada, pois cada lote é comprimido
em sua própria thread. Sessions não funcionam com inserções paralelas: desative as sessions ou mantenha
MaxDegreeOfParallelism = 1.
Remova o schema probe. Cada chamada InsertBinaryAsync envia primeiro uma consulta SELECT ... WHERE 1=0
para descobrir os column types. Consulte Ignorando a schema probe query para eliminar esse
round trip com ColumnTypes ou UseSchemaCache.
O caminho de inserção sem boxing se aplica ao format padrão
RowBinary. O RowBinaryWithDefaults precisa
examinar cada value para encontrar o marker DBDefault, portanto mantém o caminho mais lento.Compressão: as duas direções discordam
A compressão troca CPU por bytes. Se essa troca vale a pena depende da direção da transferência, da largura de banda da sua conexão com o servidor ClickHouse, de como seus dados interagem com o algoritmo de compressão escolhido e de você pagar ou não por cada byte transferido. Leituras: mantenha a compressão ativada, a menos que o servidor esteja em execução na mesma máquina. Esse é o padrão. Em comparação com a ausência de compressão, ozstd no nível 1
resultou em:
Inserções: meça antes de comprimir. A economia pode não ser suficiente para justificar a ativação. Lembre-se também de que a descompressão gera carga adicional no servidor; essa carga é modesta para Zstd e LZ4, mas pode ser alta para outros algoritmos (por exemplo, Brotli).
Para desativar a compressão de inserções:
Buffers
ReadBufferSize define o tamanho do buffer que lê as respostas HTTP. O padrão é 64 KiB.
O driver toma emprestado esse buffer de um pool compartilhado e o devolve ao descartar o leitor, ou seja, não há uma alocação a cada consulta. Aumente esse valor para reduzir o número de recargas do buffer em resultados grandes. O driver mantém um buffer para cada leitor aberto simultaneamente, portanto o uso de memória cresce conforme o tamanho do buffer e o número de leitores concorrentes.
Runtime e GC
Ative o Server GC em aplicações com alto volume de inserções. Com o mesmo código e a mesma quantidade de bytes alocados, o Workstation GC foi até 97% mais lento nas inserções do que o Server GC.O Server GC é uma configuração de throughput, não de latência. Nas mesmas medições, o Server GC passou
menos da metade do tempo total pausado, mas suas pausas individuais foram mais longas
(percentil 95 de 114,6 ms contra 61,9 ms). Se o seu service for sensível a latência de cauda, meça
os dois modes antes de escolher.
Latência: reutilize conexões
Estabelecer uma nova conexão TCP e realizar o handshake TLS leva um tempo considerável. Reutilizar conexões reduz significativamente a latência das suas consultas.- Não crie um client para cada requisição. Cada novo client, com seu próprio
HttpClient, cria um novo pool de conexões e paga novamente pelo handshake. Use um únicoClickHouseClientdurante todo o ciclo de vida da aplicação. Ele é thread-safe e foi projetado para uso como singleton. - Para ADO.NET e ORMs, use
ClickHouseDataSource, de modo que todas as conexões compartilhem um único pool.
Meça você mesmo
Em muitos casos, o desempenho dependerá do formato dos seus dados, da velocidade da sua conexão com o servidor, de você querer ou não trocar CPU do cliente por CPU do servidor (ou vice-versa), das limitações do seu hardware, etc. Por isso, recomenda-se medir o desempenho você mesmo, com base nos seus dados e no seu ambiente. Para ver a parcela de trabalho do servidor, definaQueryOptions.QueryId e leia os counters:
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:
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:
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:DynamicParameters (a partir de um dicionário ou de um objeto anônimo):
Consultas com POCOs
O Dapper mapeia colunas para propriedades pelo nome (sem diferenciar maiúsculas de minúsculas):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.
WHERE IN
A expansão nativa do IN no Dapper funciona: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:
Manipuladores de tipo personalizados
Alguns tipos do ClickHouse, comoITuple, BigInteger e ClickHouseDecimal, precisam ter manipuladores registrados na inicialização:
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.
Limitações
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 umaDataConnection usando o provedor do ClickHouse:
BulkCopyAsync para inserções em lote eficientes.
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 viaSaveChanges — tudo usando os padrões familiares do EF Core.
- NuGet:
ClickHouse.EntityFrameworkCore - Código-fonte: GitHub
Este provedor está em desenvolvimento ativo. O lançamento atual oferece suporte a consultas LINQ (incluindo junções, subconsultas e operações de conjunto),
INSERT via SaveChanges / BulkInsertAsync, migrations com DDL completo (CREATE / ALTER / DROP) e configuração específica do motor de tabela do ClickHouse. UPDATE / DELETE não são suportados.Instalação
Início rápido
Defina sua entidade e oDbContext e, em seguida, consulte com LINQ:
Tipos com suporte
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.
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:
0 / "" em vez de == null.
Inserção de dados
SaveChanges usa a API nativa InsertBinaryAsync do driver — codificação RowBinary com corpo da requisição comprimido, muito mais eficiente do que SQL parametrizado:
Added para Unchanged após salvar, assim como em qualquer outro provedor do EF Core.
O tamanho do lote é configurável (padrão: 1000):
Inserção em massa
Para cargas de alta taxa de transferência, useBulkInsertAsync 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 um corpo da requisição comprimido.
Isso o torna ideal para carregar grandes conjuntos de dados quando você não precisa rastrear entidades após a inserção:
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 colunasEnum8/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:
Conversões de tipos personalizadas
O sistemaValueConverter 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:
Anotações de tipo de coluna
Para tipos escalares comostring, 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):
OnModelCreating:
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 colunasVariant(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():
string, ulong, ulong[]).
Colunas JSON
O provedor dá suporte ao tipo de colunaJson do ClickHouse, com mapeamento para System.Text.Json.Nodes.JsonNode (principal) ou string (via ValueConverter automático):
SaveChanges quanto com BulkInsertAsync:
string, com o tipo de coluna Json — o provedor aplica um ValueConverter automaticamente:
- Sem tradução de caminhos JSON —
entity.Data["name"]no LINQ não é convertido para a sintaxe SQLdata.namedo ClickHouse. Filtre colunas não JSON e inspecione o JSON na memória. - Semântica de NULL — o tipo JSON do ClickHouse retorna
{}(objeto vazio) para valores NULL, em vez de SQL NULL. - Precisão de inteiros — o JSON do ClickHouse armazena todos os inteiros como
Int64. Ao ler comJsonNode, useGetValue<long>()em vez deGetValue<int>().
Motores de tabela
Configure os motores de tabela do ClickHouse e as cláusulas específicas de cada motor por meio da API fluenteToTable(name, t => ...). Quando nenhum motor é configurado, o provedor usa MergeTree, com ORDER BY derivado da chave primária da entidade.
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(...):
Migrações
Fluxo de trabalho padrão das migrações do EF Core:Limitações de migração
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
TiposValueTuple 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
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:
Colunas do tipo AggregateFunction
Colunas do tipoAggregateFunction(...) não podem ser consultadas nem inseridas diretamente.
Para inserir: