-
ClickHouseClient(recomendado): Cliente de alto nível, thread-safe, projetado para ser usado como singleton. Oferece uma API assíncrona simples para consultas e inserções em massa. É a melhor opção para a maioria das aplicações. -
ADO.NET (
ClickHouseDataSource,ClickHouseConnection,ClickHouseCommand): Abstrações padrão do .NET para bancos de dados. Necessárias para integração com ORMs (Dapper, Linq2db) e para casos que exigem compatibilidade com ADO.NET.ClickHouseBulkCopyé uma classe auxiliar para inserir dados de forma eficiente por meio de uma conexão ADO.NET.ClickHouseBulkCopyestá obsoleto e será removido em um lançamento futuro; useClickHouseClient.InsertBinaryAsyncem seu lugar.
Guia de migração
- Atualize o seu arquivo
.csprojcom o novo nome do pacote,ClickHouse.Driver, e com a versão mais recente disponível no NuGet. - Substitua todas as referências a
ClickHouse.ClientporClickHouse.Driverna sua base de código.
Versões do .NET compatíveis
ClickHouse.Driver é compatível com as seguintes versões do .NET:
- .NET 6.0
- .NET 8.0
- .NET 9.0
- .NET 10.0
Versões do ClickHouse com suporte
O cliente oferece suporte oficial aos três lançamentos mais recentes, além dos dois lançamentos LTS mais recentes.Instalação
Instale o pacote pelo NuGet:Início rápido
Configuração
Você pode configurar a conexão com o ClickHouse de duas maneiras:- String de conexão: Pares de chave e 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: 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 de dados e serialização
Gerenciamento de sessões
A opção
UseSession mantém a sessão no servidor, permitindo o uso de instruções SET e tabelas temporárias. As sessões são redefinidas após 60 segundos de inatividade (tempo limite padrão). É possível prolongar o ciclo de vida da sessão ajustando as configurações de sessão por meio de instruções do ClickHouse ou da configuração do servidor.A classe ClickHouseConnection normalmente permite a execução paralela (várias threads podem executar consultas ao mesmo tempo). No entanto, habilitar a opção UseSession limita a execução a uma consulta ativa por conexão de cada vez (essa é uma limitação do lado do servidor).Segurança
Configuração do cliente HTTP
Logging e depuração
Configurações personalizadas e funções
Ao definir configurações personalizadas por meio de uma string de conexão, 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 esta página.Exemplos de strings de conexão
Conexão básica
Com configurações personalizadas do ClickHouse
QueryOptions
QueryOptions permite substituir as configurações definidas para o cliente em cada consulta. Todas as propriedades são opcionais e, quando especificadas, substituem os valores padrão do cliente.
Exemplo:
InsertOptions
InsertOptions estende QueryOptions com configurações específicas para operações de inserção em massa por meio de InsertBinaryAsync.
Todas as propriedades de
QueryOptions também estão disponíveis em InsertOptions.
Exemplo:
Como evitar a consulta de verificação do schema
Por padrão,InsertBinaryAsync envia uma consulta SELECT ... WHERE 1=0 antes de cada inserção para identificar os tipos de coluna. Em cenários de alta vazão, você pode eliminar essa sobrecarga de duas maneiras:
Opção 1: Informar os tipos de coluna explicitamente
Se você conhece o schema da tabela no momento da compilação, informe os tipos de coluna diretamente por meio de ColumnTypes. Assim, nenhuma consulta para verificar o schema é enviada:
UseSchemaCache = true para consultar o schema apenas uma vez e reutilizá-lo nas próximas operações de insert na mesma instância de ClickHouseClient:
ColumnTypestem prioridade sobreUseSchemaCache. Se ambos estiverem definidos, os tipos explícitos serão usados.- O cache de schema não detecta alterações feitas por
ALTER TABLE. Se você alterar o schema da tabela, crie um novoClickHouseClientou não useUseSchemaCachepara essa tabela. - O cache é específico de cada instância de
ClickHouseCliente indexado por (database, table). Diferentes subconjuntos de colunas da mesma tabela compartilham o mesmo schema em cache.
ClickHouseClient
ClickHouseClient é a API recomendada para interagir com o ClickHouse. É thread-safe, foi projetada para uso como singleton e gerencia internamente o 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 dados de conexão com 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 dados de conexão serão exibidos abaixo.
Se você usa o ClickHouse autogerenciado, os dados de conexão são definidos pelo administrador do ClickHouse.
Usando uma string de conexão:
ClickHouseClientSettings:
IHttpClientFactory:
ClickHouseClient foi projetado para ser mantido por longos períodos e compartilhado em toda a aplicação. Crie uma única instância (geralmente como um singleton) e reutilize-a em todas as operações de banco de dados. O cliente gerencia internamente o pool de conexões HTTP.Executando consultas
UseExecuteNonQueryAsync para executar instruções que não retornam resultados:
ExecuteScalarAsync para obter um único valor:
Inserindo dados
Inserções parametrizadas
Insira dados por meio de consultas parametrizadas comExecuteNonQueryAsync. Os tipos dos parâmetros devem ser especificados no SQL com a sintaxe {name:Type}:
Inserções em massa
UseInsertBinaryAsync para inserir grandes quantidades de linhas com eficiência. O método transmite os dados em fluxo no formato binário nativo de linhas do ClickHouse, permite o envio paralelo de lotes e evita erros de “URL muito longa” que podem ocorrer com consultas parametrizadas.
InsertOptions:
- Antes de inserir dados, o cliente consulta automaticamente a estrutura da tabela por meio de
SELECT * FROM <table> WHERE 1=0. Os valores fornecidos devem corresponder aos tipos das colunas de destino. Para evitar 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ções em paralelo; desative-as ou definaMaxDegreeOfParallelism = 1. - Use
RowBinaryFormat.RowBinaryWithDefaultsemInsertOptions.Formatse quiser que o servidor aplique os valores DEFAULT às colunas não fornecidas.
Inserção com POCO
Em vez de criar arraysobject[], você pode inserir objetos POCO fortemente tipados diretamente. Registre o tipo uma única vez e depois passe IEnumerable<T>:
Quando todas as propriedades mapeadas especificam um
Type explícito, a consulta de verificação do schema é dispensada. Quando apenas algumas propriedades têm tipos explícitos, o driver recorre à consulta de verificação do schema para o conjunto completo de colunas.
InsertBinaryAsync<T> aceita as mesmas opções de InsertOptions (agrupamento em lote, paralelismo e cache do schema) que a sobrecarga de object[].
Diferentemente da sobrecarga de
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 serão inseridas, use [ClickHouseNotMapped] para excluir propriedades ou [ClickHouseColumn(Name = "...")] para renomeá-las.Se ColumnTypes estiver definido em InsertOptions, seus valores terão precedência sobre os atributos do POCO.Evolução do schema
As inserções com POCO continuam funcionando normalmente quando novas colunas são adicionadas à tabela de destino após o registro do tipo. Como o driver insere apenas as colunas mapeadas pelo POCO, o servidor preenche automaticamente as novas colunas comDEFAULT (ou outras expressões de valor padrão). Não é preciso alterar o código nem registrar o tipo novamente.
Posicionamento da consulta de inserção
Em um insert binário, a instruçãoINSERT INTO ... FORMAT ... é gravada na primeira linha do corpo da requisição, antes das linhas de dados. Como o corpo é comprimido por padrão, os mecanismos de roteamento e logging que inspecionam apenas a URL não conseguem ver a instrução. Defina InsertOptions.QueryPlacement como InsertQueryPlacement.Url para enviar a instrução como parâmetro query da URL, deixando o corpo reservado às linhas de dados:
query, ou quando você quer que a instrução apareça nos logs de acesso e nas ferramentas de observabilidade. Essa opção precisa ser ativada explicitamente porque a instrução passa a contar para o limite de comprimento da URL. O limite efetivo é o menor dos limites impostos pelo runtime do .NET, por um intermediário ou pelo servidor. Do .NET 6 ao .NET 9, System.Uri limita a URI completa e codificada da requisição a 65,519 caracteres; se esse limite for excedido, o driver lança uma InvalidOperationException que orienta você a usar InsertQueryPlacement.Body. Por padrão, o http_max_uri_size do ClickHouse é de 1 MiB, mas um intermediário pode impor um limite menor. No modo em que a instrução é enviada no corpo, ela e as linhas não estão sujeitas a esse limite de comprimento da URL; outras opções da requisição ainda podem aparecer na URL.
Essa configuração é independente de Compressor: o corpo é codificado da mesma forma em ambos os modos.
Leitura de dados
UseExecuteReaderAsync para executar consultas SELECT. O ClickHouseDataReader retornado permite acessar as colunas do resultado com tipos definidos, por meio de métodos como GetInt64(), GetString() e GetFieldValue<T>().
Chame Read() para avançar para a próxima linha. O método retorna false quando não há mais linhas. Acesse as colunas pelo índice (começando em 0) ou pelo nome.
Leitura de POCOs
Em vez de ler as colunas pelo índice ou pelo nome, você pode ler os resultados da consulta em fluxo diretamente nas suas classes. Registre o tipo no cliente uma única vez e, depois, useQueryAsync<T>:
RegisterPocoType<T>() configura os mapeamentos de inserção e leitura e valida os dois de antemão. RegisterBinaryInsertType<T>() permanece inalterado e continua restrito à inserção para manter a 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 aceitas.
InvalidOperationException. Portanto, uma propriedade do tipo object aceita qualquer coluna.
QueryAsync<T> lê cada uma dessas colunas diretamente para uma propriedade do tipo correspondente:
Cada linha também aceita a versão anulável do tipo de propriedade (
long?, DateOnly? e assim por diante),
independentemente de a coluna ser Nullable(...). Uma propriedade de tipo valor não anulável associada a uma coluna
Nullable(T) é aceita no registro, mas lança uma exceção quando recebe NULL.
Wrappers como LowCardinality(T), SimpleAggregateFunction(f, T) e Object(T) são mapeados exatamente como T.
Colunas compostas também são compatíveis e usam o tipo do framework indicado na
referência de tipos para leitura: Array(T) para T[], Tuple(...)
para System.Tuple<...>, Nested(...) para Tuple<...>[], JSON para JsonObject (ou string
com 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 sem boxing e preserva a ordem de transmissão e as chaves repetidas, em qualquer um dos modos
MapReadMode. Uma propriedade Dictionary<K, V> funciona apenas no modo
padrão. Os tipos da chave e do valor devem corresponder exatamente; portanto,
Map(String, Nullable(Int32)) exige KeyValuePair<string, int?>.
Quando uma coluna aceita mais de um tipo de propriedade (uma coluna DateTime como DateTime,
DateTimeOffset ou DateOnly, uma coluna String como string ou byte[]), o tipo declarado da propriedade determina a representação. Essas representações alternativas
estão disponíveis na leitura de POCOs: QueryAsync<T> oferece suporte a elas, mas MapTo<T> não.
Ao percorrer manualmente um leitor, use ClickHouseDataReader.MapTo<T>() para materializar a linha atual como um POCO registrado sem avançar o leitor:
MapTo<T> quando precisar controlar o loop de leitura — por exemplo, para combinar o acesso bruto às colunas
com a materialização de POCOs. Como ele lê a linha por meio dos valores com boxing do leitor, não oferece
os tipos alternativos de propriedade mencionados acima e faz mais alocações que QueryAsync<T>. Prefira
QueryAsync<T> quando precisar apenas das linhas; consulte
como escolher o caminho de materialização para comparar os números.
Um conversor de valores de leitura configurado 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 usando a sobrecarga correspondente à forma como
ela foi lida: ConvertValue<T>, tipada, para uma
coluna sem boxing, e ConvertValue, com boxing, para uma coluna composta. Implemente as duas sobrecargas
de forma consistente; caso contrário, a mesma coluna produzirá resultados diferentes dependendo do caminho usado.
Quando há um LoggerFactory configurado, RegisterPocoType<T>() e RegisterBinaryInsertType<T>() geram um log de nível Debug (categoria ClickHouse.Driver.Client) que indica quais propriedades foram mapeadas para cada coluna e quais foram ignoradas, com os respectivos motivos. Consulte Logging e diagnósticos.
Parâmetros SQL
No ClickHouse, os parâmetros em consultas SQL seguem o formato padrão{parameter_name:DataType}.
Exemplos:
Os parâmetros de vinculação (‘bind’) do SQL são enviados como parâmetros de consulta na URI HTTP. Por isso, usar muitos deles pode causar uma exceção de “URL muito longa”. Use
InsertBinaryAsync para inserir grandes volumes de dados e evitar essa limitação.Marcadores @name no estilo ADO
O driver também aceita marcadores @name, usados por ORMs como o Dapper. Eles são um
recurso do lado do cliente: antes do envio da requisição, cada um é substituído por
{name:ResolvedType}, de modo que o servidor nunca recebe um @. Consulte
resolução de tipos para saber como o tipo é determinado. Sempre que possível, use a forma explícita
{name:Type}.
Um @name sem parâmetro correspondente é mantido como está, para que o servidor o rejeite. A correspondência
diferencia maiúsculas de minúsculas; portanto, @ID não corresponde a um parâmetro chamado id.
Para desativar essa substituição, configure a opção AppContext
ClickHouse.Driver.DisableReplacingParameters
antes de usar o driver pela primeira vez. Somente a substituição no texto é desativada; os parâmetros continuam sendo enviados, portanto
as consultas escritas com a sintaxe nativa {name:Type} continuam funcionando.Parâmetros de identificador
O tipo de parâmetroIdentifier permite passar 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 defina ClickHouseDbParameter.ClickHouseType = "Identifier":
ID da consulta
Cada consulta recebe umquery_id único, que pode ser usado para buscar dados na tabela system.query_log ou cancelar consultas de longa duração. Você pode definir um ID de consulta personalizado usando QueryOptions:
Mapeamento personalizado de tipos de parâmetros
Ao usar parâmetros no estilo@ (por exemplo, WHERE id = @id), o driver infere automaticamente o tipo do ClickHouse a partir do tipo do valor .NET. Por exemplo, int é mapeado para Int32.
Para alterar esse comportamento padrão, defina ParameterTypeResolver em ClickHouseClientSettings. Isso é útil quando você quer que todos os parâmetros DateTime usem DateTime64(3) para obter precisão de milissegundos, ou que todos os valores decimais usem uma escala específica, sem precisar definir ClickHouseType em cada parâmetro.
Usando DictionaryParameterTypeResolver para mapeamentos simples de tipos:
IParameterTypeResolver personalizado para cenários avançados:
Para uma resolução que leve em conta o valor ou se baseie no nome, implemente diretamente a interface IParameterTypeResolver. Retorne null para que a inferência padrão seja usada:
QueryOptions.ParameterTypeResolver. Nesse caso, ele tem precedência sobre o resolvedor configurado no nível do cliente.
Ordem de precedência na resolução de tipos:
O resolvedor faz parte de uma cadeia de precedência. Da maior para a menor prioridade:
ClickHouseTypedefinido explicitamente no parâmetro- Anotação de tipo SQL usando a sintaxe
{name:Type}na consulta IParameterTypeResolver(deQueryOptions.ParameterTypeResolverou, se não estiver definido, deClickHouseClientSettings.ParameterTypeResolver)- Inferência de tipos integrada (
TypeConverter.ToClickHouseType)
ClickHouseConnection do ADO.NET — as conexões criadas a partir do cliente herdam suas configurações.
Formatação personalizada de valores de parâmetros
IParameterFormatter é um hook que determina como os valores dos parâmetros são serializados. Use-o quando a formatação integrada (por exemplo, a precisão de DateTime, as convenções de formatação de decimais, o escape de strings ou a representação de números) não atender ao que seu schema ou suas ferramentas posteriores esperam.
Defina ParameterFormatter em ClickHouseClientSettings para aplicar um formatador a todas as consultas parametrizadas. O formatador recebe o valor, o nome do tipo ClickHouse identificado e o nome do parâmetro, e retorna a representação em string enviada ao servidor. Retorne null para usar o formatador padrão.
Como usar DictionaryParameterFormatter para formatação simples por tipo CLR:
IParameterFormatter personalizado para cenários avançados:
QueryOptions.ParameterFormatter. Quando definido, ele tem precedência sobre o formatador configurado no nível do cliente.
Valores compostos:
O formatador é aplicado tanto aos parâmetros de coleção de nível superior quanto a cada elemento de valores compostos (Array, Tuple, Map, Nullable, LowCardinality, Variant). Por exemplo, um mapeamento de typeof(int) formata individualmente cada elemento Int32 de um Array(Int32).
Uso de aspas simples em contextos compostos:
Para tipos do ClickHouse semelhantes a strings (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) embutidos em um literal composto, o driver coloca a saída do formatador entre aspas simples, mas não aplica escape ao conteúdo. Se a string retornada contiver uma aspa simples ou uma 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 valor composto) são usados como estão, sem aspas adicionais; portanto, não é necessário aplicar escape nesse caso.
Precedência dos formatadores:
IParameterFormatter(deQueryOptions.ParameterFormatterou, se não estiver definido, deClickHouseClientSettings.ParameterFormatter). Se retornar um valor diferente denull, esse valor será usado.- Formatação integrada específica para cada tipo em
HttpParameterFormatter.
null ou DBNull; eles sempre são serializados como o marcador de valor nulo do ClickHouse (\N).
Conversão personalizada de valores na leitura
IReadValueConverter permite transformar os valores retornados pelo leitor de dados após a desserialização, sem alterar seu tipo CLR. Entre os usos comuns estão definir DateTime.Kind = Utc em uma coluna DateTime sem fuso horário, remover caracteres das extremidades de strings ou normalizá-las e processar uma coluna JSON antes que seus valores cheguem ao código da aplicação.
Defina ReadValueConverter em ClickHouseClientSettings para aplicar um conversor a todas as leituras. O conversor é chamado uma vez por coluna, para cada linha, tanto no caminho com boxing (GetValue) quanto no caminho genérico (GetFieldValue<T>). Se nenhum conversor estiver definido, não há sobrecarga — o leitor retorna os valores diretamente.
Usando DictionaryReadValueConverter para conversões simples por tipo CLR:
For<T> são transmitidos sem alterações. O encaminhamento é feito com base no tipo CLR exato; portanto, registre o tipo que o leitor realmente produz (por exemplo, For<JsonObject> para uma coluna JSON no modo JsonReadMode.Binary).
IReadValueConverter personalizado para cenários avançados:
Se precisar fazer o encaminhamento com base na string do tipo do ClickHouse (por exemplo, para distinguir DateTime de DateTime('UTC') — ambos são representados pelo mesmo tipo CLR), implemente IReadValueConverter diretamente:
GetFieldType, GetSchemaTable) não são redirecionados pelo conversor e devem permanecer consistentes com o valor retornado.
Você também pode definir um conversor para cada consulta por meio de QueryOptions.ReadValueConverter; nesse caso, ele tem precedência sobre o conversor no nível do cliente.
Limite do encaminhamento:
O conversor é invocado uma vez por coluna e recebe o valor completo da célula desserializada; ele não percorre recursivamente estruturas compostas. Para uma coluna Array(Int32), o valor recebido é um int[]; para Tuple(Int32, String), é um ITuple.
Qual sobrecarga é executada:
As duas sobrecargas devem produzir resultados consistentes, pois a sobrecarga chamada pelo driver depende de como o código chamador leu a
coluna:
ConvertValue<T>— os métodos de acesso tipadosGetByte,GetSByte,GetInt16/32/64,GetUInt16/32/64,GetFloat,GetDouble,GetGuid,GetDateTime,GetIPAddress,GetBigIntegereGetFieldValue<T>, além de todas as colunas sem encapsulamento em objeto no caminho de leitura de POCO.ConvertValue(com encapsulamento em objeto) —GetValue,GetValues, os indexadores,GetChar,GetTuplee os caminhos que fazem coerção de tipo emGetBoolean,GetDecimaleGetString.
IsDBNull não executa o conversor: ele lê diretamente o indicador de nulo, portanto um conversor nunca pode
alterar se um valor é considerado nulo. TryGetEnumOrdinal também não passa pelo conversor — consulte
como ler o ordinal de um enum.
O conversor funciona no caminho ADO.NET de ClickHouseConnection — as conexões criadas a partir do cliente herdam as configurações.
Streaming de dados brutos
UseExecuteRawResultAsync para transmitir os resultados de uma consulta diretamente no formato desejado, 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 de dados automaticamente.
Para exportações brutas (por exemplo, Parquet, Arrow, Native), pode ser útil negociar outro codec (por exemplo, zstd ou lz4) para economizar largura de banda à custa de mais CPU, sem alterar a configuração da conexão. QueryOptions.AcceptEncoding e ClickHouseCommand.AcceptEncoding definem o cabeçalho HTTP Accept-Encoding para uma única requisição, substituindo qualquer valor padrão já definido, e adicionam enable_http_compression=1 à URL (exigido pelo ClickHouse para que ele respeite Accept-Encoding).
Configuração do HttpClient
Não é necessário configurar nada: oHttpClient criado pelo driver mantém AutomaticDecompression em DecompressionMethods.None, e o próprio driver decodifica as respostas. Assim, Content-Encoding nunca é removido sem que você perceba, e o corpo bruto chega até você exatamente como o servidor o enviou.
Corpos de erro
Quando o servidor responde com um código 4xx/5xx eenable_http_compression=1 está definido, ele comprime o corpo do erro com o mesmo codec que usaria para uma resposta bem-sucedida. O driver decodifica esses corpos com todos os codecs compatíveis (lz4, zstd, gzip, deflate, br/brotli), para que a mensagem de ClickHouseServerException seja legível. Para os demais codecs (snappy, …), ele retorna um texto substituto que identifica o codec e indica system.query_log como o local onde encontrar o texto original do erro.
Descompressão da resposta
Accept-Encoding apenas solicita que o servidor comprima a resposta — ela ainda precisa ser decodificada. O driver faz isso automaticamente com base no Content-Encoding da resposta. Assim, todas as APIs de leitura habituais (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) funcionam com respostas comprimidas sem precisar de configuração adicional. O driver decodifica lz4, zstd, gzip, deflate e br; snappy não é compatível.
Por padrão, o driver informa que aceita zstd, lz4, gzip, deflate, e o ClickHouse responde usando zstd. Para escolher outro formato, defina Accept-Encoding manualmente — para todo o cliente:
ClickHouseClientSettings:
enable_http_compression=1 na URL, requisito para que o ClickHouse leve o cabeçalho em conta — inclusive quando UseCompression é false, pois especificar um codec é interpretado como um pedido de compressão. Quando nenhum valor é definido, UseCompression=false não envia o cabeçalho Accept-Encoding.
Accept-Encoding pode ser definido em quatro lugares. Prevalece o primeiro que especificar um codec:
QueryOptions.AcceptEncoding(ouClickHouseCommand.AcceptEncoding)CustomHeaders["Accept-Encoding"]na consultaCustomHeaders["Accept-Encoding"]no clienteClickHouseClientSettings.AcceptEncodingou a palavra-chaveAcceptEncodingna string de conexão
identity.
É o servidor, não o cliente, que escolhe o codec. O ClickHouse verifica os tokens de Accept-Encoding segundo 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 valores q. Assim, o cabeçalho indica os codecs aceitos, sem impor a escolha de um deles; a única forma de influenciar essa escolha é omitir tokens. A lista padrão inclui zstd, por isso uma consulta com a configuração padrão recebe uma resposta compactada com zstd; os demais tokens servem como alternativas. br pode ser decodificado, mas não é anunciado por padrão.
O tamanho da carga útil e o uso de CPU no servidor e no cliente variam entre os codecs conforme os dados, a conexão e o valor de http_zlib_compression_level no servidor (valor padrão: 3) — consulte Ajustar a compressão.
http_zlib_compression_level. Essa configuração se aplica a todos os codecs HTTP, e seu valor padrão é 3. Ajuste-a de acordo com os dados, a velocidade da conexão e o uso de CPU.- Cliente limitado pela CPU em uma conexão rápida. O driver decodifica o corpo da resposta na thread que fez a chamada; portanto, quando a rede não é o gargalo, a velocidade de decodificação no cliente pode ser o fator limitante.
Content-Encoding assim indicar, independentemente do que foi solicitado: se o cabeçalho estiver ausente ou tiver o valor identity, o corpo passa sem alterações; se indicar um codec compatível, o corpo é decodificado; 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 Content-Encoding. Assim, o driver recebe o conteúdo sem compressão e não o altera.
Resultados brutos não anunciam nenhum codec. ExecuteRawResultAsync (assim como os métodos públicos PostStreamAsync / InsertRawStreamAsync) entregam o corpo sem alterações. Por isso, a menos que você especifique um codec, eles não solicitam nenhum — como o driver não decodifica esse corpo, anunciar um codec nesse caso transformaria silenciosamente uma exportação em um arquivo comprimido. A regra é simples e independe da configuração de qualquer HttpClient: o corpo chega exatamente como o servidor o enviou, e o servidor envia os dados sem compressão, a menos que você solicite um codec. Para exportar bytes comprimidos intencionalmente, solicite um codec para todo o cliente ou por consulta.
Um AcceptEncoding explícito (em qualquer um dos níveis) continua valendo para requisições brutas, e ClickHouseRawResult.ReadDecompressedStreamAsync() decodifica o resultado quando necessário; ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync e CopyToAsync sempre retornam os bytes exatamente como chegaram.
leaveOpen, de modo que descartá-lo preserva a resposta; quando ela não está comprimida, você recebe o próprio stream do conteúdo HTTP, e descartá-lo encerra o corpo da resposta. Em ambos os casos, o ClickHouseRawResult é responsável pela resposta — não use seus outros membros de leitura após descartar o stream. É sempre necessário descartar o ClickHouseRawResult, e isso, por si só, é suficiente: ele libera a resposta e qualquer decodificador criado aqui (os decodificadores mantêm buffers de um pool). Assim, o await using acima é opcional, mas pode ser mantido com segurança. Chamadas sequenciais repetidas retornam o mesmo stream; não é seguro usar esse tipo simultaneamente.
Consulte Select_007_ResponseCompression.cs para ver um exemplo executável.
Compressão de inserts (requisições)
Zstd é o codec padrão para inserts:InsertOptions.Compressor é inicializado com ZstdCompressor.Default,
que usa zstd no nível 3. Defina outro compressor para alterar o codec ou use null para enviar o
corpo sem compressão.
Default e um construtor que recebe um nível
e o tamanho do buffer de escrita:
Compartilhe as instâncias dos compressores. Cada
Default é uma instância compartilhada, e os quatro compressores
podem ser usados com segurança por várias threads ao mesmo tempo — como acontece quando
InsertOptions.MaxDegreeOfParallelism é maior que 1, pois cada inserção usa um compressor por
lote. Nenhum deles implementa IDisposable. Crie uma instância própria e reutilize-a, assim
como se faz com Default.IClickHouseCompressor é público, e sua implementação precisa fornecer apenas dois membros:
Content-Encoding especificado. Os demais membros —
Decompress, MethodByte, MaxEncodedLength, Encode e Decode — têm implementações
padrão que lançam NotSupportedException; portanto, sobrescreva apenas os membros de que seu codec precisa.
Além de comprimir as requisições, implemente Decompress para decodificar os corpos das respostas. O stream retornado deve lançar
InvalidDataException quando o corpo estiver corrompido ou em um formato incorreto.
InsertOptions.Compressor controla apenas o insert binário. Os demais corpos das requisições do driver seguem outras regras de compressão e não passam por esse componente:
- Toda requisição com SQL em texto (
ExecuteReaderAsync,ExecuteScalarAsync,ExecuteNonQueryAsync,QueryAsync<T>,ExecuteRawResultAsync, a camada ADO.NET) envia a instrução comContent-Encoding: gzipsempre queUseCompressionétrue— ou seja, por padrão. O codec não é configurável:AcceptEncodingcontrola apenas a resposta, então, para a requisição, as opções são gzip ou nenhuma compressão.Compression=falseenvia a instrução sem compressão. Como as instruções são pequenas, isso raramente exige atenção — mas é útil saber ao monitorar requisições em um proxy ou em uma captura de pacotes. - Um corpo multipart — de uma consulta cujos parâmetros são enviados como dados de formulário (
UseFormDataParameters=true) — é sempre enviado sem compressão, independentemente do valor deUseCompression. - Um upload de dados brutos (
InsertRawStreamAsync,PostStreamAsync) usa um sinalizador próprio para cada chamada e não depende deUseCompressionnem deInsertOptions.Compressor: usa gzip quando o sinalizador está ativado e não usa compressão caso contrário. Observe que o parâmetrouseCompressiondeInsertRawStreamAsynctemtruecomo valor padrão. Portanto, o upload é comprimido com gzip, a menos que você passefalse— mesmo comCompression=falseno cliente.
Ajustando a compressão
A compressão reduz o volume de dados transmitidos, mas exige mais processamento da CPU. Se vale a pena depende quase inteiramente da velocidade da conexão em comparação com a velocidade de execução do codec. Não existe uma configuração ideal para todos os casos.O número que faz a diferença
Vale a pena comprimir os dados quando o codec é mais rápido que a rede. No caminho de leitura, esse limiar é mais baixo do que muitos imaginam, porque o ClickHouse comprime as respostas HTTP em uma única thread no buffer de saída. Em medições feitas em um serviço ClickHouse Cloud com 16 vCPUs (hits, RowBinary, nível 3), o servidor gera saída comprimida a cerca de 100 a 200 MB/s.
Assim, para um resultado grande, supondo que apenas uma consulta seja processada por vez, a compressão deixa de compensar por volta de 100 MB/s. Um único fluxo HTTPS
dentro da mesma região de nuvem costuma superar essa velocidade, enquanto conexões pela internet pública, por uma VPN ou entre regiões geralmente ficam abaixo dela.
No caminho de inserção, a compressão continua valendo a pena em velocidades de conexão mais altas, porque o cliente comprime os dados em um núcleo de CPU próprio e costuma ser mais rápido que o servidor ao comprimir respostas.
Guia geral por ambiente de implantação
Há três fatores que esta tabela não considera:
- Custo do tráfego de saída: se você paga pela transferência de dados, cada byte tem um custo além do impacto na latência, o que favorece uma compressão mais alta independentemente da velocidade da conexão.
- Resultados pequenos: as recomendações acima se referem a cargas úteis grandes. Para respostas pequenas, o codec quase não faz diferença, e a sobrecarga por requisição predomina.
- Inserts paralelos elevam os limiares de inserção. Todos os valores de vazão acima consideram uma única thread. O valor padrão de
InsertOptions.MaxDegreeOfParallelismé1, mas aumentá-lo permite comprimir lotes em paralelo. Assim, a taxa total de codificação do cliente cresce aproximadamente conforme o número de núcleos alocados. Por isso, em uma conexão rápida, ainda pode valer a pena comprimir uma inserção paralela mesmo em velocidades nas quais a compressão de uma inserção de thread única já não compensa. Considere os valores da coluna de insert como um limite inferior e, se você já processa lotes em paralelo, teste novamente antes de concluir que sua conexão é rápida demais para a compressão.
Escolhendo um codec
Níveis
A compressão da resposta é controlada por uma única configuração do servidor,http_zlib_compression_level, que se aplica a todos os codecs HTTP, não apenas ao zlib. O valor padrão é 3.
Não altere essa configuração a menos que as medições justifiquem a mudança. Acima do valor padrão, a redução no tamanho é pequena em comparação com o aumento no uso de CPU (para zstd, passar de 3 para 6 praticamente dobra o uso de CPU do servidor para reduzir o volume de dados em cerca de 14%), e o desempenho de br piora drasticamente. Abaixo do valor padrão, no nível 1, o cenário muda de fato: lz4 passa a consumir muito menos CPU, e zstd perde a vantagem que tinha sobre ele nesse aspecto. Se necessário, defina o valor por consulta:
Medindo o ponto de equilíbrio no seu ambiente
A maneira mais rápida de escolher o codec e o nível de compressão ideais é medir o tempo de execução da mesma consulta com diferentes codecs e comparar os resultados.ProfileEvents em system.query_log — defina
QueryOptions.QueryId para localizar a linha:
LIMIT n sem ORDER BY retorna linhas diferentes
a cada execução. Assim, cada repetição comprime dados diferentes, e as razões deixam de ser significativas. Faça a comparação
com um conjunto de resultados fixo.
Inserção a partir de um fluxo de dados brutos
UseInsertRawStreamAsync para inserir dados diretamente de fluxos de arquivos ou da memória em formatos como CSV, JSON, Parquet ou qualquer formato compatível com o ClickHouse.
Inserir dados de um arquivo CSV:
Consulte a documentação das configurações de formato para conhecer as opções de controle do comportamento da ingestão de dados.
Mais exemplos
Para ver mais exemplos práticos de uso, consulte o diretório de exemplos 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 para o uso das abstrações padrão de banco de dados do .NET.
Gerenciamento do ciclo de vida com ClickHouseDataSource
Sempre crie conexões por meio de umClickHouseDataSource para garantir o gerenciamento adequado do ciclo de vida e o reaproveitamento das conexões. O DataSource gerencia internamente um único ClickHouseClient, e todas as conexões compartilham o pool de conexões HTTP desse cliente.
Usando ClickHouseCommand
Crie comandos usando uma conexão para executar SQL:ExecuteNonQueryAsync()- Para instruções INSERT, UPDATE, DELETE e DDLExecuteScalarAsync()- Retorna o valor da primeira coluna da primeira linhaExecuteReaderAsync()- Retorna umClickHouseDataReaderpara percorrer os resultados
Usando ClickHouseDataReader
OClickHouseDataReader oferece acesso tipado aos resultados das consultas:
Lendo o valor ordinal de um enum
Uma colunaEnum8 ou Enum16 é materializada como seu rótulo: GetFieldType informa string, e
GetString, GetValue e GetFieldValue<string> retornam o rótulo. Os acessores numéricos
lançam InvalidCastException ao acessar uma coluna enum, pois o valor armazenado é uma string.
Use TryGetEnumOrdinal para obter o número associado ao rótulo:
true e atribui um valor a value para uma coluna Enum8/Enum16 e para uma coluna Nullable(Enum...)
cuja célula não seja NULL. Retorna false, com value definido como 0, para uma célula NULL ou para
qualquer coluna que não seja do tipo enum. O ordinal é o valor com sinal recebido no formato
wire; por isso, pode ser negativo, e o ordinal de um Enum16 pode ser maior que um
byte.
Boas práticas
Ciclo de vida das conexões e pool de conexões
ClickHouse.Driver usa System.Net.Http.HttpClient internamente. HttpClient mantém um pool de conexões para cada endpoint. Por isso:
- As sessões do banco de dados compartilham as conexões HTTP gerenciadas pelo pool.
- As conexões HTTP são recicladas automaticamente pelo pool.
- As conexões podem permanecer ativas mesmo após o descarte dos objetos
ClickHouseClientouClickHouseConnection.
Tratamento de DateTime
-
Use UTC sempre que possível. Armazene os timestamps em colunas
DateTime('UTC')e useDateTimeKind.Utcno código. Isso elimina ambiguidades de fuso horário. -
Use
DateTimeOffsetpara lidar explicitamente com fusos horários. Ele sempre representa um instante específico e inclui a informação de deslocamento em relação ao UTC. -
Especifique o fuso horário nas indicações de tipo SQL. Ao usar parâmetros com valores DateTime
Unspecifiedpara 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 lote. Em vez de exigir que o cliente agrupe os dados em lotes, o servidor armazena temporariamente os dados recebidos e os grava no armazenamento quando atingem limiares configuráveis. Isso é útil em cenários de alta concorrência, como cargas de trabalho de observabilidade em que muitos agentes enviam pequenas cargas úteis. Ative as inserções assíncronas por meio deCustomSettings ou da string de conexão:
wait_for_async_insert):
Principais configurações:
Sessões
Habilite as sessões apenas quando precisar de funcionalidades que mantêm estado no lado do servidor, como:- Tabelas temporárias (
CREATE TEMPORARY TABLE) - Manutenção do contexto das consultas entre várias instruções
- Configurações no nível da sessão (
SET max_threads = 4)
Tipos de dados compatíveis
ClickHouse.Driver oferece suporte a todos os tipos de dados do ClickHouse. As tabelas abaixo mostram a correspondência entre os tipos do ClickHouse e os tipos nativos do .NET na leitura de dados do banco de dados.
Mapeamento de tipos: leitura de dados do ClickHouse
Tipos de números inteiros
Tipos de ponto flutuante
Tipos decimais
A conversão de tipos decimais é controlada pela configuração UseCustomDecimals.
Tipo booleano
Tipos de 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 para armazenar dados binários que talvez não sejam UTF-8 válido.A configuração também se aplica a strings aninhadas em outros tipos: Array(String) é lido como byte[][]
e Map(String, String) como Dictionary<byte[], byte[]> — inclusive as chaves. A única exceção é uma
coluna JSON, cujos valores de string são sempre tratados como texto; consulte 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 decorridos desde a epoch). Embora o armazenamento seja sempre em UTC, as colunas podem ter um fuso horário associado, que afeta a exibição e a interpretação dos valores.
Ao ler valores DateTime, a propriedade DateTime.Kind é definida de acordo com o fuso horário da coluna:
Para colunas com fuso horário diferente de UTC, o
DateTime retornado representa a hora local nesse fuso horário. Use ClickHouseDataReader.GetDateTimeOffset() para obter um DateTimeOffset com o deslocamento correto:
DateTime em vez de DateTime('Europe/Amsterdam')), o driver retorna um DateTime com Kind=Unspecified. Isso preserva a hora local exatamente como foi armazenada, sem presumir um fuso horário.
Se você precisar considerar o fuso horário em colunas sem um fuso horário explícito, escolha uma das opções:
- Especifique o fuso horário na definição da coluna:
DateTime('UTC')ouDateTime('Europe/Amsterdam') - Aplique o fuso horário após a leitura.
Tipo JSON
O tipo de retorno das colunas JSON é determinado pela configuração
JsonReadMode:
-
Binary(padrão): RetornaSystem.Text.Json.Nodes.JsonObject. Permite acessar os dados JSON de forma estruturada, mas tipos específicos do ClickHouse (como endereços IP, UUIDs e números decimais grandes) são convertidos em representações em string dentro da estrutura JSON. -
String: Retorna o JSON bruto comostring. Preserva a representação exata do JSON fornecida pelo ClickHouse, o que é útil para repassar o JSON sem fazer o parsing ou para cuidar da desserialização por conta própria.
None é um terceiro modo. Ele lê os dados exatamente como Binary, mas não envia nenhuma configuração do servidor junto com a
consulta — use esse modo em uma conexão sem permissão para definir configurações do servidor.
Um caminho declarado no tipo da coluna é um caminho tipado; qualquer outro caminho no documento é um
caminho dinâmico. A diferença entre eles aparece quando um valor é nulo.
Um caminho tipado sempre aparece no JsonObject. Quando declarado como Nullable(T) ou Dynamic, ele é retornado
como null em JSON tanto se o valor armazenado for nulo quanto se o documento não contiver esse caminho — não é possível
distinguir os dois casos:
JSON(x String)
produz {"x":""} e JSON(x Int64) produz {"x":0}.
Um caminho dinâmico com valor nulo é removido por completo do objeto; por isso, ContainsKey retorna
false para esse caminho. Ao ler {"x":null} de uma coluna JSON comum, o resultado é {}.
Caminhos tipados aninhados criam os níveis superiores, de modo que JSON(a.b Nullable(Int64)) produz {"a":{"b":null}}
mesmo para um documento vazio.
É assim que o próprio servidor renderiza o resultado; portanto, os modos
Binary e String agora se comportam da mesma forma. Antes da versão 1.4.0, um
caminho tipado com valor nulo era removido do JsonObject, fazendo com que {"x":null} fosse lido como
{} — e, para um caminho aninhado como JSON(a.b Nullable(Int64)), toda a subárvore a desaparecia.JSON são sempre retornados como texto, seja qual for
o valor de ReadStringsAsByteArrays — JsonValue não tem representação como array de bytes; por isso, um byte[] seria
representado em Base64. Isso vale para String, FixedString e para esses tipos quando encapsulados em
LowCardinality, Nullable ou SimpleAggregateFunction, além de strings dentro de Array e Map,
inclusive as chaves de map.
Um array de bytes cujo tipo o leitor de JSON não consegue identificar ainda é representado em Base64: um
caminho tipado como
Variant ou Dynamic contém um valor cujo tipo só é conhecido em cada linha. Por isso, uma string
em Variant(Array(UInt8), String) é retornada codificada em Base64. Isso vale para 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, então o servidor
gera a linha com uma chave duplicada: {"a":0,"a":{"b":7}}. Um JsonObject não pode conter dois valores
para a mesma chave. Por isso, JsonReadMode.Binary lança uma SerializationException que identifica os dois caminhos. O
mesmo acontece quando o valor é um Map, como em JSON(a Map(String, Int64)), lido de uma linha que
também contém um a.b dinâmico.
Isso só se aplica quando ambos os caminhos contêm um valor na linha. Se um deles não contiver nada — um valor nulo,
um objeto vazio ou uma subárvore cujos valores são todos nulos —, prevalece o caminho que contém dados,
independentemente de qual dos dois o servidor envia primeiro. Assim, quando a sobreposição é declarada com tipos Nullable, apenas um dos caminhos é preenchido por linha, e a leitura ocorre sem
erro: JSON(a Nullable(Int64), a.b Nullable(Int64)) produz {"a":5} e {"a":{"b":7}}, como
esperado.
Leia essa coluna com JsonReadMode.String para obter o texto JSON enviado pelo servidor sem alterações, inclusive a
chave duplicada.
Defina AllowDuplicateJsonKeys para continuar lendo a coluna como um JsonObject sem lançar uma exceção. O
driver mantém o último dos dois valores na linha e descarta o outro. Portanto, há
perda de dados: JSON(a Int64, a.b Int64) contendo {"a.b":7} é lido como {"a":0}. Se um caminho
contiver um valor e seu pai contiver um escalar ou um array, uma exceção ainda será lançada, pois uma subárvore não pode ser
inserida em nenhum dos dois.
Tipo Map
Um
Map(K, V) do ClickHouse é armazenado fisicamente como um Array(Tuple(K, V)) e pode conter vários pares com a mesma chave. Um Dictionary não permite isso; portanto, no modo padrão, apenas o último valor de cada chave repetida é mantido, e os pares anteriores são descartados. A configuração MapReadMode determina 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 os que têm a mesma chave.
Map. Por isso, também afeta GetFieldValue<T>, os tipos de schema informados pelo driver e o mapeamento de propriedades de POCO. Isso vale sempre que um map aparecer na hierarquia de tipos de uma coluna — inclusive em Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) e Dynamic.
Ambas as representações são aceitas para gravação em qualquer um dos modos — consulte gravação de maps.
Outros tipos
Os tipos Dynamic e Variant são convertidos, em cada linha, para o tipo correspondente ao valor subjacente.
Tipos geométricos
O tipo Geometry é um tipo Variant que pode armazenar qualquer um dos tipos geométricos. O valor será convertido para o tipo correspondente.
Mapeamento de tipos: gravação no ClickHouse
Ao inserir dados, o driver converte os tipos .NET para os 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 de string
Tipos de data e hora
Valores fora do intervaloNa gravação binária, valores de
Date, Date32, DateTime e DateTime32 fora dos respectivos intervalos aceitos geram ArgumentOutOfRangeException no momento da chamada a Write. A exceção informa o tipo da coluna e o intervalo aceito. Antes, valores fora do intervalo podiam ser truncados silenciosamente ao passar por um inteiro de 32 bits e reinterpretados pelo servidor, produzindo timestamps válidos, mas incorretos.DateTime.Kind ao gravar valores:
Os valores de
DateTimeOffset sempre preservam o instante exato.
Exemplo: DateTime em UTC (instante preservado)
DateTimeKind.Utc ou DateTimeOffset em todas as operações com DateTime. Assim, seu código funcionará de forma consistente, independentemente dos fusos horários do servidor, do cliente e da coluna.
Parâmetros HTTP vs cópia em massa
Há uma diferença importante entre a vinculação de parâmetros HTTP e a cópia em massa ao gravar valores DateTimeUnspecified:
A cópia em massa conhece o fuso horário da coluna de destino e interpreta corretamente os valores Unspecified nesse fuso horário.
Os parâmetros HTTP não identificam automaticamente o fuso horário da coluna. Você precisa especificá-lo na anotação de tipo SQL:
Tipos decimais
Tipo JSON
O comportamento da gravação de 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 parsing no lado do servidor. É o modo mais flexível e funciona sem registro de tipos. -
Binary: Aceita apenas tipos POCO registrados. Os dados são convertidos no lado do cliente para o formato JSON binário do ClickHouse, com suporte completo a dicas de tipo. É necessário chamarconnection.RegisterJsonSerializationType<T>()antes de usar esse modo. A gravação de valoresstringouJsonNodenesse modo lançaArgumentException.
JSON(id UInt64, price Decimal128(2))), o driver as utiliza para serializar os valores sem perder informações de tipo. Isso evita a perda de precisão em tipos como UInt64, Decimal, UUID e DateTime64, que ocorreria se fossem serializados como JSON genérico.
Os POCOs podem ser gravados em colunas JSON de duas maneiras, dependendo do JsonWriteMode:
Modo String (padrão): os POCOs são serializados por meio de System.Text.Json.JsonSerializer. Não é necessário registrar os tipos. Essa é a abordagem mais simples e funciona com objetos anônimos.
Modo Binary: os POCOs são serializados no formato JSON binário do driver, com suporte completo a indicações de tipo. Os tipos devem ser registrados com connection.RegisterJsonSerializationType<T>() antes do uso. Esse modo permite mapear caminhos JSON personalizados por meio de atributos:
-
[ClickHouseJsonPath("path")]: mapeia uma propriedade para um caminho JSON personalizado. Útil para estruturas aninhadas ou quando o nome da propriedade é diferente da chave JSON desejada. Funciona apenas no modo Binary. -
[ClickHouseJsonIgnore]: exclui uma propriedade da serialização. Funciona apenas no modo Binary.
UserId só corresponde a uma indicação definida como UserId, não como userid. Esse é o comportamento do ClickHouse, que permite que caminhos como userName e UserName coexistam como campos distintos.
Limitações (apenas no modo Binary):
- Os tipos POCO devem ser registrados na conexão com
connection.RegisterJsonSerializationType<T>()antes da serialização. A tentativa de serializar um tipo não registrado lançaClickHouseJsonSerializationException. - Propriedades do tipo dicionário ou array/lista precisam de indicações de tipo na definição da coluna para que sejam serializadas corretamente. Sem essas indicações, use o modo String.
- Valores nulos em propriedades POCO só são gravados quando o caminho tem uma indicação de tipo
Nullable(T)na definição da coluna. O ClickHouse não permite tiposNullableem caminhos JSON dinâmicos; por isso, propriedades nulas sem indicação de tipo são ignoradas. - Os atributos
ClickHouseJsonPatheClickHouseJsonIgnoresão ignorados no modo String (funcionam apenas no modo Binary).
Outros tipos
Tipos geométricos
Sem suporte para gravação
Tratamento de tipos aninhados
Os tipos aninhados do ClickHouse (Nested(...)) podem ser lidos e gravados com a semântica de arrays.
Logging e diagnóstico
O cliente .NET do ClickHouse usa as abstrações deMicrosoft.Extensions.Logging para oferecer logging leve, ativado apenas quando configurado. Quando habilitado, o driver emite mensagens estruturadas sobre eventos do ciclo de vida da conexão, execução de comandos, operações de transporte e operações de insert em massa. O logging é totalmente opcional: aplicações que não configuram um logger continuam funcionando sem sobrecarga adicional.
Início rápido
Usando appsettings.json
Você pode configurar os níveis de logging com a configuração padrão do .NET:Usando uma configuração em memória
Você também pode definir no código o nível de detalhamento do logging para cada categoria:Categorias e emissores
O driver usa categorias específicas para que você possa ajustar os níveis de log de cada componente:Exemplo: Como diagnosticar problemas de conexão
- Seleção da fábrica de clientes HTTP (pool padrão ou conexão única)
- Configuração do handler HTTP (SocketsHttpHandler ou HttpClientHandler)
- Configurações do pool de conexões (MaxConnectionsPerServer, PooledConnectionLifetime etc.)
- Configurações de tempo limite (ConnectTimeout, Expect100ContinueTimeout etc.)
- Configuração de SSL/TLS
- Eventos de abertura e fechamento de conexões
- Rastreamento do ID da sessão
Modo de depuração: rastreamento de rede e diagnóstico
Para facilitar o diagnóstico de problemas de rede, a biblioteca do driver inclui um processo auxiliar que permite rastrear, em baixo nível, os componentes internos de rede do .NET. Para habilitar esse recurso, você deve passar uma LoggerFactory com o nível definido como Trace e definir EnableDebugMode como true (ou habilitá-lo manualmente por meio da 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 produção.
OpenTelemetry
O driver oferece suporte nativo ao rastreamento distribuído com OpenTelemetry por meio da APISystem.Diagnostics.Activity do .NET. Quando habilitado, o driver gera spans para operações de banco de dados, que podem ser exportados para plataformas de observabilidade como o 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 dos spans
Cada span inclui atributos padrão do OpenTelemetry para bancos de dados, além de estatísticas de consulta específicas do ClickHouse que podem ajudar na depuração.Opções de configuração
Configure o comportamento do rastreamento comClickHouseDiagnosticsOptions:
Configuração de TLS
Ao se conectar ao ClickHouse via HTTPS, você pode configurar o funcionamento do TLS/SSL de diferentes maneiras.Validação personalizada de certificados
Em ambientes de produção que exigem uma lógica personalizada para validar certificados, forneça uma instância própria deHttpClient com o handler ServerCertificateCustomValidationCallback configurado:
Considerações importantes ao fornecer um HttpClient personalizado
- Descompressão automática: mantenha
AutomaticDecompressiondesativado. O driver descomprime as respostas por conta própria, portanto essa opção não é necessária — e ativá-la pode causar problemas no envio de requisições: ao enviar uma requisição, o handler também adiciona todos os algoritmos de sua máscara aoAccept-Encodingde saída, ampliando a lista anunciada pelo driver. Com isso, o ClickHouse pode responder usando um codec que você não solicitou. Consulte Descompressão de respostas. - Tempo limite de inatividade: configure
PooledConnectionIdleTimeoutcom um valor menor que okeep_alive_timeoutdo servidor (10 segundos no ClickHouse Cloud) para evitar erros causados por conexões semiabertas.
Ajuste de desempenho
Esta seção explica como usar o cliente para obter o melhor desempenho possível e quais opções ajustar para otimizar seu desempenho no seu caso de uso.Visão geral
| Se você | Faça isto | |---|---|---| | Lê linhas como POCOs | UseQueryAsync<T>, não MapTo<T> |
| Insere grandes volumes de dados | Aumente InsertOptions.BatchSize |
| Executa um aplicativo de console ou worker que faz muitas inserções | Ative o Server GC |
| Lê grandes volumes de resultados pela rede | Mantenha a compressão da resposta ativada (como é o padrão) |
| Insere dados por uma conexão rápida | Experimente InsertOptions.Compressor = null |
| Insere dados repetidamente na mesma tabela | Use UseSchemaCache ou ColumnTypes |
| Lê volumes muito grandes de resultados | Aumente ReadBufferSize |
Leitura: escolha o caminho de materialização
Há três maneiras de obter uma linha de um resultado, e elas têm custos diferentes. Alguns caminhos encapsulam os valores em objetos, aumentando as alocações e reduzindo o desempenho.
Para a leitura de 1,000,000 linhas e 105 colunas do conjunto de dados hits:
Os ORMs usam o caminho otimizado quando empregam acessores tipados. O linq2db registra
GetInt64,
GetDouble e GetDateTime para cada coluna, permitindo a leitura sem boxing. O código que lê por meio de
GetValue (inclusive resultados dynamic do Dapper) faz boxing de cada valor. Se uma consulta de ORM for executada com frequência
e usar GetValue para leitura, use QueryAsync<T> para essa consulta específica.Inserção: tamanho do lote e paralelismo
O tamanho do lote é o fator que mais afeta a vazão de inserção. O valor padrão deInsertOptions.BatchSize é
100.000 linhas.
Use lotes grandes. Em uma inserção de 1.000.000 de linhas, aumentar o tamanho de cada lote de 10.000 para 100.000 linhas
produziu os seguintes resultados:
Se não for possível controlar o tamanho do lote (por exemplo, quando vários produtores enviam pequenas quantidades de linhas de forma independente), use insert assíncrono e deixe que o servidor agrupe as linhas em lotes.
Envios em paralelo. O valor padrão de
InsertOptions.MaxDegreeOfParallelism é 1. Aumente-o para enviar
lotes simultaneamente. Isso é especialmente útil quando a compressão está ativada, pois cada lote é comprimido
em uma thread própria. As sessões não são compatíveis com inserts paralelos: desative-as ou mantenha
MaxDegreeOfParallelism = 1.
Elimine a consulta preliminar ao schema. Cada chamada a InsertBinaryAsync envia primeiro uma consulta SELECT ... WHERE 1=0
para identificar os tipos de coluna. Consulte Como dispensar a consulta preliminar ao schema para evitar essa
ida e volta com ColumnTypes ou UseSchemaCache.
O caminho de inserção sem boxing se aplica ao formato padrão
RowBinary. RowBinaryWithDefaults precisa
examinar cada valor para encontrar o marcador DBDefault e, por isso, usa o caminho mais lento.Compressão: os resultados variam conforme a direção
A compressão reduz a quantidade de bytes transferidos em troca de maior uso de CPU. Se vale a pena depende da direção da transferência, da largura de banda da conexão com o servidor ClickHouse, do desempenho do algoritmo de compressão escolhido com seus dados e de você pagar ou não por byte transferido. Leituras: mantenha a compressão ativada, a menos que o servidor esteja na mesma máquina. Essa é a configuração padrão. Em comparação com a transferência sem compressão,zstd no nível 1
apresentou:
Inserts: meça o desempenho antes de ativar a compressão. A economia pode não ser suficiente para justificar seu uso. Lembre-se também de que a descompressão aumenta a carga no servidor; esse aumento é moderado com Zstd e LZ4, mas pode ser elevado com outros algoritmos (por exemplo, Brotli).
Para desativar a compressão na inserção:
Buffers
ReadBufferSize define o tamanho do buffer usado para ler respostas HTTP. O valor padrão é 64 KiB.
O driver obtém esse buffer de um pool compartilhado e o devolve ao liberar o leitor. Assim, não há
uma alocação para cada consulta. Aumente esse valor para reduzir o número de vezes que o buffer precisa
ser reabastecido ao processar resultados grandes. O driver mantém um buffer para cada leitor aberto
simultaneamente; por isso, o uso de memória aumenta com o tamanho do buffer e o número de leitores simultâneos.
Ambiente de execução e coleta de lixo
Ative o Server GC para aplicações que realizam muitas operações de insert. Com o mesmo código e a mesma quantidade de bytes alocados, o Workstation GC foi até 97% mais lento nas operações de insert do que o Server GC.O Server GC é uma configuração voltada à vazão, não à latência. Nas mesmas medições, o Server GC passou
menos da metade do tempo total em pausas, mas cada pausa durou mais
(114,6 ms no 95º percentil, contra 61,9 ms). Se o seu serviço for sensível à latência das requisições mais lentas, avalie
os dois modos 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 consultas.- Não crie um cliente para cada requisição. Cada novo cliente com seu próprio
HttpClientcria um novo pool de conexões e precisa realizar o handshake novamente. Use um únicoClickHouseClientdurante todo o ciclo de vida da aplicação. Ele é thread-safe e foi projetado para ser usado como singleton. - Para ADO.NET e ORMs, use
ClickHouseDataSourcepara que todas as conexões compartilhem o mesmo pool.
Meça o desempenho por conta própria
Em muitos casos, o desempenho dependerá da estrutura dos seus dados, da velocidade da conexão com o servidor, de como você distribui o uso de CPU entre o cliente e o servidor, das limitações do seu hardware etc. Por isso, recomendamos que você meça o desempenho no seu ambiente, usando seus próprios dados. Para verificar a parte do trabalho realizada pelo servidor, definaQueryOptions.QueryId e consulte os contadores:
Suporte a ORMs
Os ORMs exigem a API ADO.NET (ClickHouseConnection). Para gerenciar corretamente o ciclo de vida das conexões, crie-as a partir de um ClickHouseDataSource:
Dapper
ClickHouse.Driver é compatível com o Dapper. O driver converte automaticamente a sintaxe @parameter do Dapper para a sintaxe nativa {parameter:Type} do ClickHouse, inferindo os tipos a partir dos valores do .NET.
Use ClickHouseDataSource para gerenciar corretamente o ciclo de vida das conexões:
Formas de passar parâmetros
O Dapper oferece suporte a todas as formas padrão de passar parâmetros: Objetos anônimos:DynamicParameters (de um dicionário ou de um objeto anônimo):
Consultas com resultados em POCOs
O Dapper mapeia as colunas para as propriedades pelo nome (sem diferenciar maiúsculas de minúsculas):Sintaxe nativa de parâmetros do ClickHouse
Quando precisar controlar explicitamente os tipos, use a sintaxe{param:Type} do ClickHouse diretamente no SQL, com um Dictionary<string, object> para os valores dos parâmetros. Não combine as sintaxes @param e {param:Type} para o mesmo parâmetro.
WHERE IN
A expansão nativa de IN do Dapper funciona:WHERE id IN (@Ids1, @Ids2, @Ids3), e o driver converte cada parâmetro expandido.
A função has() do ClickHouse também funciona com um parâmetro Array:
Handlers de tipo personalizados
Alguns tipos do ClickHouse, comoITuple, BigInteger e ClickHouseDecimal, precisam ter seus handlers registrados na inicialização:
Dapper.Contrib
GetAll<T>() e Get<T>(id) funcionam. Já Insert<T>() não funciona: gera sintaxe específica do SQL Server (SCOPE_IDENTITY, []). Em vez dele, recomenda-se usar o método nativo InsertBinaryAsync de 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 completa. Exemplo de uso: Crie umaDataConnection com o provedor do ClickHouse:
BulkCopyAsync para realizar inserções em massa com eficiência.
Entity Framework Core
O provedor oficial de Entity Framework Core para ClickHouse. Mapeie classes C# para tabelas do ClickHouse, faça consultas com LINQ e insira dados usandoSaveChanges — tudo com os recursos 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, migrações com DDL completo (CREATE / ALTER / DROP) e configuração de motores de tabela específicos do ClickHouse. UPDATE / DELETE não têm suporte.Instalação
Início rápido
Defina sua entidade e oDbContext e faça consultas com LINQ:
Tipos compatíveis
Use
ClickHouseDecimal (de ClickHouse.Driver.Numerics) em vez de decimal quando precisar da precisão total das colunas Decimal128/Decimal256 — o tipo decimal do .NET comporta apenas 28 a 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 aplicado aos resultados das agregações.
Junções: Join (INNER), padrões GroupJoin/SelectMany (LEFT e CROSS). LEFT JOIN retorna null de fato para linhas sem correspondência (veja semântica de valores nulos 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 embutidas: junções e Contains com coleções em memória (int[], List<T> etc.) são convertidos em uma sequência de operações UNION.
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 convertidos para funções equivalentes no ClickHouse — aritméticas, logarítmicas, trigonométricas e utilitárias.
O provedor inclui automaticamente set_join_use_nulls=1 em todos os caminhos de conexão para que o comportamento de JOIN corresponda ao esperado pelo Entity Framework.
Se o servidor ClickHouse ou o perfil não permitir alterar essa configuração (por exemplo, um perfil readonly=1), desative esse comportamento com:
0 / "" em vez de == null.
Inserção de dados
SaveChanges usa a API nativa InsertBinaryAsync do driver, com codificação RowBinary e corpo da requisição comprimido — uma abordagem muito mais eficiente do que SQL parametrizado:
Added para Unchanged após serem salvas, como ocorre com qualquer outro provedor do EF Core.
O tamanho do lote é configurável (o padrão é 1000):
Inserção em lote
Para cargas de alta vazão, useBulkInsertAsync em vez de SaveChanges. Esse método de extensão de DbContext dispensa totalmente o rastreamento de alterações, a resolução de identidade e o gerenciamento de estado do EF Core — ele chama diretamente o InsertBinaryAsync do driver, usando a codificação RowBinary e um corpo da requisição comprimido.
Por isso, ele é indicado para carregar grandes conjuntos de dados quando não é necessário rastrear as entidades após a inserção:
IEnumerable<T> — as entidades são processadas uma a uma, sem que seja necessário carregar todas na memória. O valor retornado é o número de linhas inseridas. Após a inserção, as entidades não ficam vinculadas ao DbContext, portanto não há transição de estado de Added → Unchanged.
Enums
As colunasEnum8/Enum16 do ClickHouse podem ser mapeadas como propriedades do tipo string ou como tipos enum do C#. Ao usar enums do C#, o provedor converte automaticamente os valores entre o enum e sua representação em string:
Conversões de tipos personalizados
O sistemaValueConverter do EF Core permite mapear tipos personalizados para tipos já aceitos pelo provedor. O provedor nunca recebe o tipo personalizado — o EF Core faz a conversão antes que ele chegue ao provedor.
Conversão por propriedade:
Anotações de tipo de coluna
Para tipos escalares comostring, int, DateTime etc., o provedor infere automaticamente o tipo do ClickHouse. Para tipos parametrizados e wrappers, é preciso especificar o tipo do ClickHouse explicitamente.
Usando anotações de dados (atributos):
OnModelCreating:
Array(Nullable(Int32)) e LowCardinality(Nullable(String)), são compatíveis — o provedor remove automaticamente as camadas Nullable e LowCardinality 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, é necessário declarar explicitamente o tipo de armazenamento com .HasColumnType():
string, ulong, ulong[]).
Colunas JSON
O provedor oferece suporte ao tipo de colunaJson do ClickHouse, que é mapeado para System.Text.Json.Nodes.JsonNode (principal) ou string (por meio de um ValueConverter automático):
SaveChanges e BulkInsertAsync:
string com uma coluna do tipo Json — o provedor aplicará um ValueConverter automaticamente:
- Sem tradução de caminhos JSON —
entity.Data["name"]em LINQ não é convertido para a sintaxe SQLdata.namedo ClickHouse. Filtre por colunas que não sejam JSON e examine o JSON na memória. - Semântica de NULL — O tipo JSON do ClickHouse retorna
{}(um objeto vazio) para valores NULL, em vez de retornar NULL do SQL. - Precisão dos inteiros — O ClickHouse armazena todos os inteiros em JSON 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 => ...). Se nenhum motor for configurado, o provedor usará MergeTree por padrão, com ORDER BY definido a partir da chave primária da entidade.
Cláusulas do motor:
WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Todas são aplicadas ao construtor do motor retornado por HasXxxEngine().
Recursos em nível de coluna: HasCodec, HasTtl, HasComment, HasDefault — todos são incluídos nas migrações.
Índices de salto de dados — por meio de HasIndex(...).HasSkippingIndexType(...):
Migrações
Fluxo de trabalho padrão das migrações do EF Core:Limitações das migrações
Além das migrações, o provedor ainda não oferece suporte a:
UPDATE/DELETE- Transações:
BeginTransactionnão tem efeito. 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. Filtre por colunas que não sejam JSON e examine o JSON na memória.
Limitações
Tuplas com 8 ou mais elementos e uma tupla aninhada na última posição
Os tiposValueTuple de C# com mais de 7 elementos usam uma estrutura de aninhamento gerada pelo compilador: o 8º argumento genérico (TRest) é 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 gera uma ambiguidade quando a coluna do ClickHouse é uma tupla de 8 elementos cujo ú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 (com aninhamento de
TRestgerado pelo compilador) - Uma tupla de 8 elementos cujo último elemento é uma
Tuple(String, String)aninhada
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
O driver interpreta o 8º argumento como TRest (ou seja, desfaz o aninhamento), fazendo com que a tupla de 8 elementos com uma tupla aninhada seja serializada incorretamente.
Isso afeta tanto System.Tuple quanto ValueTuple, pois ambos usam o aninhamento de TRest para tuplas com mais de 7 elementos. Tuplas com 7 elementos ou menos, ou cujo último elemento não é uma tupla, não são afetadas.
Solução alternativa: Envolva a tupla interna em uma camada adicional para que o driver consiga distingui-la do aninhamento de TRest:
Colunas do tipo AggregateFunction
Não é possível consultar colunas do tipoAggregateFunction(...) nem inserir dados nelas diretamente.
Para inserir dados: