Skip to main content
O cliente oficial em C# para se conectar ao ClickHouse. O código-fonte do cliente está disponível no repositório do GitHub. Desenvolvido originalmente por Oleg V. Kozlyuk. A biblioteca oferece duas APIs principais:
  • 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. ClickHouseBulkCopy está obsoleto e será removido em um lançamento futuro; use ClickHouseClient.InsertBinaryAsync em seu lugar.
As duas APIs compartilham o mesmo pool de conexões HTTP e podem ser usadas juntas na mesma aplicação.

Guia de migração

  1. Atualize o seu arquivo .csproj com o novo nome do pacote, ClickHouse.Driver, e com a versão mais recente disponível no NuGet.
  2. Substitua todas as referências a ClickHouse.Client por ClickHouse.Driver na 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:
Ou usando o Gerenciador de Pacotes 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.
A seguir, você encontra a lista completa das configurações, seus valores default e seus efeitos.

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:
Opção 2: Armazenar o schema em cache Se você insere dados repetidamente na mesma tabela, defina 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:
  • ColumnTypes tem prioridade sobre UseSchemaCache. 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 novo ClickHouseClient ou não use UseSchemaCache para essa tabela.
  • O cache é específico de cada instância de ClickHouseClient e 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 um ClickHouseClient com uma string de conexão ou um objeto ClickHouseClientSettings. Consulte a seção Configuração para conhecer as opções disponíveis. Os 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:
Ou com ClickHouseClientSettings:
Em cenários de injeção de dependência, use 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

Use ExecuteNonQueryAsync para executar instruções que não retornam resultados:
Use ExecuteScalarAsync para obter um único valor:

Inserindo dados

Inserções parametrizadas

Insira dados por meio de consultas parametrizadas com ExecuteNonQueryAsync. Os tipos dos parâmetros devem ser especificados no SQL com a sintaxe {name:Type}:

Inserções em massa

Use InsertBinaryAsync 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.
Para grandes volumes de dados, configure o agrupamento em lote e o paralelismo usando 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, use InsertOptions.ColumnTypes ou InsertOptions.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 defina MaxDegreeOfParallelism = 1.
  • Use RowBinaryFormat.RowBinaryWithDefaults em InsertOptions.Format se quiser que o servidor aplique os valores DEFAULT às colunas não fornecidas.

Inserção com POCO

Em vez de criar arrays object[], você pode inserir objetos POCO fortemente tipados diretamente. Registre o tipo uma única vez e depois passe IEnumerable<T>:
Por padrão, todas as propriedades públicas que podem ser lidas são mapeadas para colunas por meio da correspondência exata de nomes para diferenciar maiúsculas de minúsculas. Você pode personalizar esse mapeamento usando atributos:
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 com DEFAULT (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ção INSERT 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:
Use essa opção quando um proxy, balanceador de carga ou gateway encaminha ou inspeciona requisições com base no parâmetro 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

Use ExecuteReaderAsync 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, use QueryAsync<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. Propriedades required são aceitas.
A correspondência de colunas diferencia maiúsculas de minúsculas. Se uma coluna estiver ausente no resultado, a propriedade correspondente mantém seu valor default; colunas adicionais no resultado são ignoradas. O driver não converte valores para tipos mais amplos ou mais restritos. Exceto pelas representações alternativas listadas abaixo, o tipo da coluna no framework deve ser atribuível ao tipo da propriedade; caso contrário, é lançada uma 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:
Use 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âmetro Identifier 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":
O valor é enviado exatamente como foi fornecido, e o servidor o insere como um identificador SQL sem delimitação, fazendo por conta própria a delimitação com backticks e o escape de caracteres. Identificadores com caracteres especiais (inclusive backticks) são preservados com segurança na ida e volta.

ID da consulta

Cada consulta recebe um query_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:
Se você especificar um QueryId personalizado, use um valor único para cada chamada. Um GUID aleatório é uma boa opção.

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.
Comportamento dos parâmetros DateTime inferidosPara parâmetros no estilo @ sem uma indicação {name:Type} no SQL e sem ClickHouseType definido, valores que representam instantes são inferidos como DateTime('UTC'), e não como um DateTime sem fuso horário. Valores DateTime com Kind igual a Utc ou Local, assim como todos os valores DateTimeOffset, são enviados como DateTime('UTC'), preservando o instante independentemente do fuso horário do servidor.Indicações explícitas ({name:DateTime}) têm precedência sobre a inferência e são a forma recomendada de construir consultas.
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:
Você também pode definir um resolvedor para uma única consulta por meio de 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:
  1. ClickHouseType definido explicitamente no parâmetro
  2. Anotação de tipo SQL usando a sintaxe {name:Type} na consulta
  3. IParameterTypeResolver (de QueryOptions.ParameterTypeResolver ou, se não estiver definido, de ClickHouseClientSettings.ParameterTypeResolver)
  4. Inferência de tipos integrada (TypeConverter.ToClickHouseType)
O resolvedor também funciona ao usar 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:
Você também pode definir um formatador para cada consulta por meio de 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:
  1. IParameterFormatter (de QueryOptions.ParameterFormatter ou, se não estiver definido, de ClickHouseClientSettings.ParameterFormatter). Se retornar um valor diferente de null, esse valor será usado.
  2. Formatação integrada específica para cada tipo em HttpParameterFormatter.
O formatador não é chamado para valores 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:
Os valores cujo tipo CLR em tempo de execução não foi registrado com 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:
O conversor deve preservar o tipo CLR em tempo de execução; os metadados da coluna (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 tipados GetByte, GetSByte, GetInt16/32/64, GetUInt16/32/64, GetFloat, GetDouble, GetGuid, GetDateTime, GetIPAddress, GetBigInteger e GetFieldValue<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, GetTuple e os caminhos que fazem coerção de tipo em GetBoolean, GetDecimal e GetString.
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

Use ExecuteRawResultAsync 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:
Formatos comuns: JSONEachRow, CSV, TSV, Parquet, Native. Consulte a documentação sobre formatos para ver todas as opções.

Compressão de transporte por consulta

Por padrão, o cliente negocia zstd, 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: o HttpClient 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.
Se você fornecer seu próprio HttpClient, também deixe AutomaticDecompression desativado. Essa configuração não afeta apenas as respostas: no momento do envio, o handler adiciona ao Accept-Encoding de saída todos os algoritmos presentes em sua máscara que ainda não estejam no cabeçalho. Assim, um handler com GZip | Deflate transforma um AcceptEncoding = "lz4" explícito em lz4, gzip, deflate e um "identity" explícito em identity, gzip, deflate na transmissão. Como o ClickHouse interpreta o cabeçalho de acordo com sua própria ordem fixa de preferência por codecs (ignorando a ordem e os valores de q), ele pode responder com um codec que você nunca solicitou. O handler então decodifica a resposta e remove o cabeçalho, sem que você sequer perceba o que aconteceu. Deixar a máscara desativada mantém a oferta exatamente como você a definiu.
Se AcceptEncoding solicitar um codec que o driver não consegue decodificar (snappy), apenas ExecuteRawResultAsync é seguro. ExecuteReaderAsync, ExecuteScalarAsync e ExecuteNonQueryAsync falham com uma NotSupportedException que identifica o codec (antes, interpretavam os bytes comprimidos como o formato do resultado e produziam dados sem sentido).

Corpos de erro

Quando o servidor responde com um código 4xx/5xx e enable_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:
por consulta, que tem precedência:
ou na string de conexão, para quem usa ORM e nunca precisa acessar ClickHouseClientSettings:
Definir esse valor também força 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:
  1. QueryOptions.AcceptEncoding (ou ClickHouseCommand.AcceptEncoding)
  2. CustomHeaders["Accept-Encoding"] na consulta
  3. CustomHeaders["Accept-Encoding"] no cliente
  4. ClickHouseClientSettings.AcceptEncoding ou a palavra-chave AcceptEncoding na string de conexão
Se nenhum deles especificar um codec, o driver envia sua lista padrão. Um valor que não especifica nenhum codec (nulo, vazio, composto apenas por espaços em branco ou por vírgulas) é tratado como não definido, e o próximo lugar é verificado. Para desativar a compressão, use 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.
Solicite um codec diferente por consulta ou para todo o cliente sempre que uma dessas situações se aplicar:
Como a decisão se baseia na resposta, o corpo é decodificado sempre que 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.
Leia o stream retornado até o fim antes que ele saia do escopo, como no exemplo acima. Quando a resposta está comprimida, você recebe um decodificador criado com 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.
O driver vem com quatro codecs. Cada um tem uma instância 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:
O servidor deve aceitar o 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 com Content-Encoding: gzip sempre que UseCompression é true — ou seja, por padrão. O codec não é configurável: AcceptEncoding controla apenas a resposta, então, para a requisição, as opções são gzip ou nenhuma compressão. Compression=false envia 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 de UseCompression.
  • Um upload de dados brutos (InsertRawStreamAsync, PostStreamAsync) usa um sinalizador próprio para cada chamada e não depende de UseCompression nem de InsertOptions.Compressor: usa gzip quando o sinalizador está ativado e não usa compressão caso contrário. Observe que o parâmetro useCompression de InsertRawStreamAsync tem true como valor padrão. Portanto, o upload é comprimido com gzip, a menos que você passe false — mesmo com Compression=false no 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.
O caminho de leitura só é paralelizado quando há várias consultas.

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.
Para ver o mesmo cenário do ponto de vista do servidor, consulte ProfileEvents em system.query_log — defina QueryOptions.QueryId para localizar a linha:
Uma armadilha ao fazer esse benchmark por conta própria: um simples 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

Use InsertRawStreamAsync 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:
O driver assume a posse do stream. InsertRawStreamAsync e PostStreamAsync descartam o stream fornecido quando a requisição termina, com ou sem sucesso. Não o descarte por conta própria nem o reutilize depois — por isso, o exemplo acima não coloca o FileStream em um bloco using.Um bloco using criado por você é executado depois que o driver já descartou o stream. Para um FileStream ou MemoryStream, essa segunda chamada é inofensiva. Mas, se o Dispose do stream devolver um buffer ao pool ou reduzir uma contagem de referências, o recurso será liberado duas vezes.A posse só é transferida depois que os argumentos são aceitos: se a chamada lançar ArgumentException ou ArgumentNullException por falta de tabela, stream ou formato, o stream ainda é seu.
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 de ClickHouseConnection, 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 um ClickHouseDataSource 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.
Para injeção de dependências:
Não crie ClickHouseConnection diretamente no código de produção. Cada instanciação direta cria um novo cliente HTTP e um novo pool de conexões, o que pode esgotar os sockets sob carga:
Em vez disso, use sempre ClickHouseDataSource ou compartilhe uma única instância de ClickHouseClient.

Usando ClickHouseCommand

Crie comandos usando uma conexão para executar SQL:
Métodos de comando:
  • ExecuteNonQueryAsync() - Para instruções INSERT, UPDATE, DELETE e DDL
  • ExecuteScalarAsync() - Retorna o valor da primeira coluna da primeira linha
  • ExecuteReaderAsync() - Retorna um ClickHouseDataReader para percorrer os resultados

Usando ClickHouseDataReader

O ClickHouseDataReader oferece acesso tipado aos resultados das consultas:

Lendo o valor ordinal de um enum

Uma coluna Enum8 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:
Retorna 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 ClickHouseClient ou ClickHouseConnection.
Práticas recomendadas:
Ao usar um HttpClient ou HttpClientFactory personalizado, configure PooledConnectionIdleTimeout com um valor menor que o keep_alive_timeout do servidor para evitar erros causados por conexões encerradas apenas de um lado. O valor padrão de keep_alive_timeout nas implantações no Cloud é de 10 segundos.
Evite criar várias instâncias de ClickHouseClient ou instâncias independentes de ClickHouseConnection sem um HttpClient compartilhado. Cada instância cria seu próprio pool de conexões.

Tratamento de DateTime

  1. Use UTC sempre que possível. Armazene os timestamps em colunas DateTime('UTC') e use DateTimeKind.Utc no código. Isso elimina ambiguidades de fuso horário.
  2. Use DateTimeOffset para 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.
  3. Especifique o fuso horário nas indicações de tipo SQL. Ao usar parâmetros com valores DateTime Unspecified para 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 de CustomSettings ou da string de conexão:
Dois modos (controlados por wait_for_async_insert):
Com wait_for_async_insert=0, os erros só aparecem durante a gravação dos dados do buffer e não podem ser associados à inserção original. O cliente também não aplica contrapressão, o que pode sobrecarregar o servidor.
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)
Quando as sessões estão habilitadas, as requisições são processadas em sequência para impedir o uso simultâneo da mesma sessão. Isso gera sobrecarga para cargas de trabalho que não precisam manter o estado da sessão.
Uso do ADO.NET (para compatibilidade com ORMs):

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:
Para colunas sem um fuso horário explícito (ou seja, DateTime em vez de DateTime('Europe/Amsterdam')), o driver retorna um DateTime com Kind=Unspecified. Isso preserva 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:
  1. Especifique o fuso horário na definição da coluna: DateTime('UTC') ou DateTime('Europe/Amsterdam')
  2. 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): Retorna System.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 como string. 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:
Quando declarado com um tipo que não aceita valores nulos, um caminho ausente assume o valor padrão desse tipo — 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.
Os valores de string nas folhas de uma coluna 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.
O ClickHouse aceita uma coluna que declara um caminho tanto como valor quanto como pai de outro caminho, por exemplo, 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): Retorna Dictionary<K, V>.
  • KeyValuePairs: Retorna List<KeyValuePair<K, V>> na ordem em que o servidor enviou os pares, preservando todos eles, inclusive os que têm a mesma chave.
O modo determina o tipo usado pelo framework para uma coluna 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.
O driver respeita DateTime.Kind ao gravar valores: Os valores de DateTimeOffset sempre preservam o instante exato. Exemplo: DateTime em UTC (instante preservado)
Exemplo: DateTime não especificado (hora local)
Recomendação: para ter um comportamento mais simples e previsível, use 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 DateTime Unspecified: 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): Aceita string, JsonObject, JsonNode ou qualquer objeto. Todas as entradas são serializadas com System.Text.Json.JsonSerializer e 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 chamar connection.RegisterJsonSerializationType<T>() antes de usar esse modo. A gravação de valores string ou JsonNode nesse modo lança ArgumentException.
Quando uma coluna JSON tem dicas de tipo (por exemplo, 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.
A correspondência entre nomes de propriedades e indicações de tipo da coluna diferencia maiúsculas de minúsculas. A propriedade 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ça ClickHouseJsonSerializationException.
  • 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 tipos Nullable em caminhos JSON dinâmicos; por isso, propriedades nulas sem indicação de tipo são ignoradas.
  • Os atributos ClickHouseJsonPath e ClickHouseJsonIgnore sã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 de Microsoft.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

Isso será registrado nos logs:
  • 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 classe ClickHouse.Driver.Diagnostic.TraceHelper). Os eventos serão registrados na categoria ClickHouse.Driver.NetTrace. Aviso: isso gerará logs extremamente detalhados e afetará o desempenho. Não é recomendável habilitar o modo de depuração em produção.

OpenTelemetry

O driver oferece suporte nativo ao rastreamento distribuído com OpenTelemetry por meio da API System.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 o ActivitySource do driver do ClickHouse à configuração do OpenTelemetry:
Para aplicativos de console, testes ou configuração manual:

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 com ClickHouseDiagnosticsOptions:
Ativar IncludeSqlInActivityTags pode expor dados confidenciais em seus traces. Use essa opção com cautela em ambientes de produção.

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 de HttpClient com o handler ServerCertificateCustomValidationCallback configurado:
Considerações importantes ao fornecer um HttpClient personalizado
  • Descompressão automática: mantenha AutomaticDecompression desativado. 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 ao Accept-Encoding de 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 PooledConnectionIdleTimeout com um valor menor que o keep_alive_timeout do 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 | Use QueryAsync<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 de InsertOptions.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:
Para saber mais sobre a escolha do codec, os níveis de compressão e como identificar o ponto de virada no seu caso, consulte Ajuste da compressã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.
Sempre libere os leitores. Ao liberar um leitor, ele devolve o buffer ao pool e libera a conexão HTTP. Abandonar um leitor não devolve o buffer ao pool e pode deixar a conexão HTTP indisponível; a coleta de lixo convencional não substitui a liberação do leitor.

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.
Os projetos ASP.NET Core já usam essa configuração. Aplicativos de console, serviços worker e a maioria das imagens de contêiner não a usam. A causa está no tamanho do orçamento da geração 0. O Workstation GC usa um orçamento pequeno, por isso os buffers de curta duração criados por uma inserção não são liberados na geração 0. Em vez disso, passam para a geração 1, o que aumenta as promoções e gera muito mais trabalho na geração 2. Em um teste de inserção, houve 4.000 coletas da geração 2 a cada 1.000 operações com o Server GC e 73.000 com o Workstation 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 HttpClient cria um novo pool de conexões e precisa realizar o handshake novamente. Use um único ClickHouseClient durante 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 ClickHouseDataSource para que todas as conexões compartilhem o mesmo pool.
Para conhecer todos os padrões, consulte Ciclo de vida das conexões e pooling.

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, defina QueryOptions.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:
Classes POCO:
Dicionário:
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:
O Dapper reescreve isso como 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, como ITuple, BigInteger e ClickHouseDecimal, precisam ter seus handlers registrados na inicialização:
Consulte o exemplo do Dapper para ver como implementar um handler de tipo.

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.
Os nomes das propriedades devem corresponder exatamente aos nomes das colunas do ClickHouse (inclusive quanto ao uso de maiúsculas e minúsculas).

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 uma DataConnection com o provedor do ClickHouse:
Os mapeamentos de tabelas podem ser definidos por meio de atributos ou de uma configuração fluente. Se os nomes das classes e propriedades corresponderem exatamente aos nomes das tabelas e colunas, não será necessária nenhuma configuração:
Consultas:
Cópia em massa: Use 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 usando SaveChanges — tudo com os recursos familiares do EF Core.
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

Requer .NET 10.0 e EF Core 10.

Início rápido

Defina sua entidade e o DbContext 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:
Com a opção de desativação habilitada, LEFT JOIN retorna os valores padrão das colunas do ClickHouse, e a detecção de navegação do EF baseada em valores nulos deixa de funcionar como esperado. Use comparações explícitas com 0 / "" em vez de == null.

Inserção de dados

SaveChanges usa a API nativa InsertBinaryAsync do driver, com codificação RowBinary e corpo da requisição comprimido — uma abordagem muito mais eficiente do que SQL parametrizado:
As entidades passam de 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, use BulkInsertAsync 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:
A entrada pode ser qualquer 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 colunas Enum8/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 sistema ValueConverter 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:
Classe de conversor reutilizável:

Anotações de tipo de coluna

Para tipos escalares como string, 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):
Usando a API fluente em OnModelCreating:
Encapsulamentos aninhados, como 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 colunas Variant(T1, T2, ...) e Dynamic do ClickHouse são mapeadas para object no .NET. Como object é genérico demais para a inferência automática de tipos, é necessário declarar explicitamente o tipo de armazenamento com .HasColumnType():
Ao ler o valor, ele é desserializado automaticamente para o tipo .NET correspondente ao discriminador armazenado (por exemplo, string, ulong, ulong[]).

Colunas JSON

O provedor oferece suporte ao tipo de coluna Json do ClickHouse, que é mapeado para System.Text.Json.Nodes.JsonNode (principal) ou string (por meio de um ValueConverter automático):
A leitura e a gravação de JSON funcionam com SaveChanges e BulkInsertAsync:
Se preferir strings JSON sem processamento, mapeie a propriedade como 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 SQL data.name do 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 com JsonNode, use GetValue<long>() em vez de GetValue<int>().

Motores de tabela

Configure os motores de tabela do ClickHouse e as cláusulas específicas de cada motor por meio da API fluente ToTable(name, t => ...). Se nenhum motor for configurado, o provedor usará MergeTree por padrão, com ORDER BY definido a partir da chave primária da entidade.
Famílias de motores compatíveis: 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(...):
Os índices padrão (que não são do tipo skipping) são ignorados sem aviso, pois não há equivalente no ClickHouse. Os índices únicos geram uma exceção, pois o ClickHouse não exige unicidade.

Migrações

Fluxo de trabalho padrão das migrações do EF Core:
Operações compatíveis:

Limitações das migrações

Além das migrações, o provedor ainda não oferece suporte a:
  • UPDATE / DELETE
  • Transações: BeginTransaction nã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 SQL data.key do 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 tipos ValueTuple 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 TRest gerado pelo compilador)
  • Uma tupla de 8 elementos cujo último elemento é uma Tuple(String, String) aninhada
Ambas produzem o mesmo tipo .NET: 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 tipo AggregateFunction(...) nem inserir dados nelas diretamente. Para inserir dados:
Para selecionar:

Última modificação em 14 de outubro de 2026