Skip to main content
Recomendamos que os usuários sempre criem seu próprio schema para logs e traces pelos seguintes motivos:
  • Escolhendo uma chave primária - Os schemas padrão usam um ORDER BY otimizado para padrões de acesso específicos. É improvável que os seus padrões de acesso estejam alinhados com isso.
  • Extraindo estrutura - Você pode querer extrair novas colunas de colunas existentes, por exemplo, da coluna Body. Isso pode ser feito usando colunas materializadas (e visões materializadas em casos mais complexos). Isso exige alterações no schema.
  • Otimizando Maps - Os schemas padrão usam o tipo Map para armazenar atributos. Essas colunas permitem armazenar metadados arbitrários. Embora isso seja uma capacidade essencial, já que os metadados de eventos muitas vezes não são definidos antecipadamente e, portanto, não podem ser armazenados de outra forma em um banco de dados fortemente tipado como o ClickHouse, o acesso às chaves do map e aos seus valores não é tão eficiente quanto o acesso a uma coluna normal. Resolvemos isso modificando o schema e garantindo que as chaves de map acessadas com mais frequência sejam colunas de nível superior - veja “Extracting structure with SQL”. Isso exige uma alteração no schema.
  • Simplificando o acesso a chaves de map - Acessar chaves em maps exige uma sintaxe mais verbosa. Você pode mitigar isso com aliases. Veja “Using Aliases” para simplificar consultas.
  • Índices secundários - O schema padrão usa índices secundários para acelerar o acesso a Maps e consultas de texto. Normalmente, eles não são necessários e consomem espaço adicional em disco. Eles podem ser usados, mas devem ser testados para garantir que sejam realmente necessários. Veja “Secondary / Data Skipping indices”.
  • Usando codecs - Você pode querer personalizar codecs de colunas se conhecer os dados esperados e tiver evidências de que isso melhora a compressão.
Descrevemos cada um dos casos de uso acima em detalhes abaixo. Importante: Embora os usuários sejam incentivados a estender e modificar seu schema para obter compressão ideal e melhor desempenho de consulta, eles devem seguir, sempre que possível, a nomenclatura do schema OTel para colunas principais. O plugin do Grafana para ClickHouse pressupõe a existência de algumas colunas OTel básicas para ajudar na criação de consultas, por exemplo, Timestamp e SeverityText. As colunas obrigatórias para logs e traces estão documentadas aqui [1][2] e aqui, respectivamente. Você pode optar por alterar esses nomes de colunas, substituindo os padrões na configuração do plugin.
ClickStack inclui um schema padrão otimizadoClickStack fornece schemas prontos para uso para logs, traces e métricas que incorporam os recursos mais recentes do ClickHouse (índices de texto para pesquisa de texto completo e de chaves de map, colunas materializadas e arrays ALIAS para filtragem com leitura direta, pesquisas de linhas por número de bloco) e foram submetidos a benchmark para oferecer um forte desempenho pronto para uso para workloads de logging e trace. Use-os como referência para o seu próprio design.

Extraindo a estrutura com SQL

Seja na ingestão de logs estruturados ou não estruturados, os usuários frequentemente precisam ser capazes de:
  • Extrair colunas de blobs de string. Consultá-las será mais rápido do que usar operações de string no momento da consulta.
  • Extrair chaves de maps. O schema padrão coloca atributos arbitrários em colunas do tipo Map. Esse tipo oferece uma capacidade sem schema, com a vantagem de que os usuários não precisam predefinir colunas para atributos ao definir logs e traces — muitas vezes, isso é impossível ao coletar logs do Kubernetes e querer garantir que os labels do pod sejam preservados para pesquisas posteriores. Acessar chaves de map e seus valores é mais lento do que consultar colunas normais do ClickHouse. Portanto, muitas vezes é desejável extrair chaves de maps para colunas de nível raiz da tabela.
Considere as consultas a seguir: Suponha que queiramos contar quais caminhos de URL recebem mais requisições POST usando os logs estruturados. O blob JSON é armazenado na coluna Body como uma String. Além disso, ele também pode ser armazenado na coluna LogAttributes como um Map(String, String) se o usuário tiver habilitado o json_parser no collector.
Supondo que LogAttributes esteja disponível, a consulta para contar quais caminhos de URL do site recebem mais solicitações POST:
Observe o uso da sintaxe de map aqui, por exemplo LogAttributes['request_path'], e da função path para remover os parâmetros de consulta da URL. Se o usuário não tiver habilitado o parsing de JSON no collector, LogAttributes ficará vazio, o que nos obriga a usar funções JSON para extrair as colunas da String Body.
Prefira o ClickHouse para fazer o parsingEm geral, recomendamos que os usuários façam o parsing de JSON de logs estruturados no ClickHouse. Estamos confiantes de que o ClickHouse oferece a implementação de parsing de JSON mais rápida. No entanto, reconhecemos que talvez você queira enviar logs para outras sources e não queira que essa lógica fique no SQL.
Agora veja o mesmo para logs não estruturados:
Uma consulta semelhante para logs não estruturados requer o uso de expressões regulares por meio da função extractAllGroupsVertical.
A maior complexidade e o custo das consultas para fazer parsing de logs não estruturados (observe a diferença de desempenho) são o motivo pelo qual recomendamos que os usuários sempre usem logs estruturados, quando possível.
Considere DictionariesA consulta acima poderia ser otimizada para aproveitar Dictionaries de expressões regulares. Veja Usando Dictionaries para mais detalhes.
Ambos os casos de uso podem ser atendidos com o ClickHouse ao mover a lógica da consulta acima para o momento da inserção. Exploramos várias abordagens abaixo, destacando quando cada uma é mais apropriada.
OTel ou ClickHouse para processamento?Você também pode realizar o processamento usando processadores e operadores do OTel collector, conforme descrito aqui. Na maioria dos casos, você verá que o ClickHouse é significativamente mais eficiente em termos de recursos e mais rápido do que os processadores do OTel collector. A principal desvantagem de realizar todo o processamento de eventos em SQL é o acoplamento da sua solução ao ClickHouse. Por exemplo, você pode querer enviar logs processados para destinos alternativos a partir do OTel collector, como o S3.

Colunas materializadas

As colunas materializadas oferecem a maneira mais simples de extrair estrutura de outras colunas. Os valores dessas colunas são sempre calculados no momento da inserção e não podem ser especificados em consultas INSERT.
SobrecargaAs colunas materializadas geram uma sobrecarga adicional de armazenamento, pois os valores são extraídos para novas colunas em disco no momento da inserção.
As colunas materializadas oferecem suporte a qualquer expressão do ClickHouse e podem aproveitar qualquer função analítica para processamento de strings (incluindo regex e pesquisa) e URLs, realizando conversões de tipo, extração de valores de JSON ou operações matemáticas. Recomendamos colunas materializadas para processamento básico. Elas são especialmente úteis para extrair valores de map, promovê-los a colunas de nível superior e realizar conversões de tipo. Em geral, são mais úteis quando usadas em schemas bem simples ou em conjunto com visões materializadas. Considere o schema a seguir para logs cujo JSON foi extraído para a coluna LogAttributes pelo collector:
O schema equivalente para extrair dados usando funções JSON de um Body do tipo String pode ser encontrado aqui. Nossas três colunas materializadas extraem a página da requisição, o tipo de requisição e o domínio de referência. Elas acessam as chaves do map e aplicam funções aos seus valores. Nossa consulta seguinte é significativamente mais rápida:
Por padrão, colunas materializadas não são retornadas em um SELECT *. Isso preserva a propriedade de que o resultado de um SELECT * sempre pode ser inserido de volta na tabela usando INSERT. Esse comportamento pode ser desativado definindo asterisk_include_materialized_columns=1 e pode ser ativado no Grafana (consulte Additional Settings -> Custom Settings na configuração da fonte de dados).

Visões materializadas

visão materializada oferecem uma forma mais poderosa de aplicar filtragem e transformações SQL a logs e traces. As visões materializadas permitem transferir o custo de computação do tempo de consulta para o momento da inserção. Uma visão materializada no ClickHouse é apenas um gatilho que executa uma consulta em blocos de dados à medida que eles são inseridos em uma tabela. Os resultados dessa consulta são inseridos em uma segunda tabela de “destino”.
Atualizações em tempo realAs visões materializadas no ClickHouse são atualizadas em tempo real à medida que os dados fluem para a tabela em que se baseiam, funcionando mais como índices atualizados continuamente. Em contraste, em outros bancos de dados, visões materializadas normalmente são snapshots estáticos de uma consulta que precisam ser atualizados (semelhante às Refreshable Materialized Views do ClickHouse).
A consulta associada à visão materializada pode, teoricamente, ser qualquer consulta, incluindo uma agregação, embora existam limitações com junções. Para as cargas de trabalho de transformações e filtragem necessárias para logs e traces, você pode considerar qualquer instrução SELECT como possível. É importante lembrar que a consulta é apenas um gatilho executado sobre as linhas que estão sendo inseridas em uma tabela (a tabela de origem), com os resultados enviados para uma nova tabela (a tabela de destino). Para garantir que não persistamos os dados duas vezes (nas tabelas de origem e de destino), podemos alterar a tabela de origem para usar um motor de tabela Null, preservando o schema original. Nossos OTel collectors continuarão enviando dados para essa tabela. Por exemplo, para logs, a tabela otel_logs se torna:
O engine de tabela Null é uma otimização poderosa - pense nele como /dev/null. Esta tabela não armazenará nenhum dado, mas quaisquer visões materializadas anexadas ainda serão executadas sobre as linhas inseridas antes de serem descartadas. Considere a seguinte consulta. Ela transforma nossas linhas em um formato que queremos preservar, extraindo todas as colunas de LogAttributes (pressupomos que isso tenha sido definido pelo collector usando o operador json_parser), definindo SeverityText e SeverityNumber (com base em algumas condições simples e na definição dessas colunas). Neste caso, também selecionamos apenas as colunas que sabemos que serão preenchidas, ignorando colunas como TraceId, SpanId e TraceFlags.
Também extraímos a coluna Body acima — caso atributos adicionais sejam incluídos posteriormente e não sejam extraídos pelo nosso SQL. Essa coluna deve comprimir bem no ClickHouse e raramente será acessada, sem impacto no desempenho das consultas. Por fim, reduzimos o Timestamp para um DateTime (para economizar espaço — consulte “Optimizing Types”) com um cast.
CondicionaisObserve o uso de condicionais acima para extrair SeverityText e SeverityNumber. São extremamente úteis para formular condições complexas e verificar se há valores definidos em maps — partimos ingenuamente do pressuposto de que todas as chaves existem em LogAttributes. Recomendamos que os usuários se familiarizem com elas — são grandes aliadas no parsing de logs, além das funções para lidar com valores nulos!
Precisamos de uma tabela para receber esses resultados. A tabela de destino abaixo corresponde à consulta acima:
Os tipos selecionados aqui são baseados nas otimizações discutidas em “Otimizando tipos”.
Observe como alteramos drasticamente nosso schema. Na prática, você provavelmente também terá colunas de trace que vai querer preservar, assim como a coluna ResourceAttributes (ela geralmente contém metadados do Kubernetes). O Grafana pode usar colunas de trace para criar links entre logs e traces - veja “Usando o Grafana”.
Abaixo, criamos uma visão materializada otel_logs_mv, que executa a consulta acima para a tabela otel_logs e envia os resultados para otel_logs_v2.
O que foi mostrado acima é exibido abaixo: Se agora reiniciarmos a configuração do collector usada em “Exportando para o ClickHouse”, os dados aparecerão em otel_logs_v2 no formato desejado. Observe o uso de funções tipadas de extração de JSON.
Uma visão materializada equivalente, que se baseia na extração de colunas da coluna Body com funções JSON, é mostrada abaixo:

Atenção aos tipos

As visões materializadas acima dependem de conversão implícita de tipos, especialmente ao usar o map LogAttributes. O ClickHouse frequentemente converte de forma transparente o valor extraído para o tipo da tabela de destino, reduzindo a sintaxe necessária. No entanto, recomendamos que os usuários sempre testem suas visões usando a instrução SELECT da visão com uma instrução INSERT INTO em uma tabela de destino com o mesmo schema. Isso deve confirmar que os tipos estão sendo tratados corretamente. Dê atenção especial aos seguintes casos:
  • Se uma chave não existir em um map, uma string vazia será retornada. No caso de valores numéricos, você precisará mapeá-los para um valor apropriado. Isso pode ser feito com condicionais, por exemplo, if(LogAttributes['status'] = ", 200, LogAttributes['status']), ou com funções de conversão, se valores padrão forem aceitáveis, por exemplo, toUInt8OrDefault(LogAttributes['status'] )
  • Alguns tipos nem sempre serão convertidos; por exemplo, representações em string de valores numéricos não serão convertidas em valores enum.
  • As funções de extração de JSON retornam valores padrão para seu tipo quando um valor não é encontrado. Verifique se esses valores fazem sentido!
Evite NullableEvite usar Nullable no ClickHouse para dados de observabilidade. Raramente é necessário, em logs e traces, distinguir entre vazio e nulo. Esse recurso gera uma sobrecarga adicional de armazenamento e impacta negativamente o desempenho das consultas. Veja aqui para mais detalhes.

Escolhendo uma chave primária (de ordenação)

Depois de extrair as colunas desejadas, você pode começar a otimizar sua chave primária/de ordenação. Algumas regras simples podem ajudar na escolha de uma chave de ordenação. Os pontos a seguir às vezes podem entrar em conflito, então considere-os nesta ordem. Você pode identificar várias chaves a partir desse processo, mas, em geral, 4 a 5 costumam ser suficientes:
  1. Selecione colunas alinhadas aos filtros e padrões de acesso mais comuns. Se você normalmente inicia investigações de observabilidade filtrando por uma coluna específica, por exemplo, o nome do pod, essa coluna será usada com frequência em cláusulas WHERE. Priorize incluí-las na chave em vez de colunas usadas com menos frequência.
  2. Prefira colunas que ajudem a excluir uma grande porcentagem do total de linhas quando filtradas, reduzindo assim a quantidade de dados que precisa ser lida. Nomes de serviço e códigos de status costumam ser bons candidatos — neste último caso, porém, apenas se você filtrar por valores que excluam a maior parte das linhas. Por exemplo, filtrar por códigos 200 corresponderá, na maioria dos sistemas, à maior parte das linhas, enquanto erros 500 corresponderão a um pequeno subconjunto.
  3. Prefira colunas com alta correlação com outras colunas da tabela. Isso ajuda a garantir que esses valores também sejam armazenados de forma contígua, melhorando a compressão.
  4. Operações GROUP BY e ORDER BY em colunas da chave de ordenação podem se tornar mais eficientes em termos de memória.

Após identificar o subconjunto de colunas para a chave de ordenação, elas devem ser declaradas em uma ordem específica. Essa ordem pode influenciar significativamente tanto a eficiência da filtragem em colunas secundárias da chave nas consultas quanto a taxa de compressão dos arquivos de dados da tabela. Em geral, é melhor ordenar as chaves em ordem crescente de cardinalidade. Isso deve ser equilibrado com o fato de que a filtragem em colunas que aparecem mais tarde na chave de ordenação será menos eficiente do que a filtragem naquelas que aparecem antes na tupla. Equilibre esses fatores e leve em conta seus padrões de acesso. Mais importante ainda, teste variantes. Para entender melhor as chaves de ordenação e como otimizá-las, recomendamos este artigo.
Estrutura primeiroRecomendamos definir suas chaves de ordenação depois de estruturar seus logs. Não use chaves em maps de atributos na chave de ordenação nem expressões de extração de JSON. Certifique-se de que suas chaves de ordenação estejam como colunas raiz na tabela.

Usando maps

Exemplos anteriores mostram o uso da sintaxe de map map['key'] para acessar valores nas colunas Map(String, String). Além de usar a notação de map para acessar chaves aninhadas, há funções de map especializadas do ClickHouse disponíveis para filtrar ou selecionar essas colunas. Por exemplo, a consulta a seguir identifica todas as chaves exclusivas disponíveis na coluna LogAttributes usando a função mapKeys, seguida da função groupArrayDistinctArray (um combinador).
Evite pontosNão recomendamos usar pontos em nomes de colunas do tipo Map e poderemos descontinuar esse uso. Use um _.

Usando aliases

Fazer consultas em tipos map é mais lento do que consultar colunas normais - veja “Acelerando consultas”. Além disso, a sintaxe é mais complexa e pode ser trabalhosa de escrever. Para contornar esse último problema, recomendamos usar colunas Alias. As colunas ALIAS são calculadas em tempo de consulta e não são armazenadas na tabela. Portanto, é impossível fazer INSERT de um valor em uma coluna desse tipo. Com aliases, podemos referenciar chaves de map, simplificar a sintaxe e expor entradas de map de forma transparente como uma coluna normal. Considere o exemplo a seguir:
Temos várias colunas materializadas e uma coluna ALIAS, RemoteAddr, que acessa o map LogAttributes. Agora podemos consultar os valores de LogAttributes['remote_addr'] por essa coluna, simplificando nossa consulta, ou seja:
Além disso, adicionar ALIAS é simples usando o comando ALTER TABLE. Essas colunas ficam disponíveis imediatamente, por exemplo.
Alias excluído por padrãoPor padrão, SELECT * exclui colunas ALIAS. Esse comportamento pode ser desativado definindo asterisk_include_alias_columns=1.

Otimizando tipos

As práticas recomendadas gerais do ClickHouse para otimização de tipos se aplicam ao caso de uso do ClickHouse.

Usando codecs

Além das otimizações de tipo, você pode seguir as práticas recomendadas gerais para codecs ao tentar otimizar a compressão em schemas de observabilidade do ClickHouse. Em geral, o codec ZSTD costuma ser bastante adequado para conjuntos de dados de logs e traces. Aumentar o nível de compressão em relação ao valor padrão, 1, pode melhorar a compressão. No entanto, isso deve ser testado, pois valores mais altos aumentam a sobrecarga de CPU no momento da inserção. Normalmente, observamos pouco ganho ao aumentar esse valor. Além disso, embora os timestamps se beneficiem da codificação delta em termos de compressão, eles podem piorar o desempenho das consultas se essa coluna for usada na chave primária/de ordenação. Recomendamos que os usuários avaliem os trade-offs entre compressão e desempenho de consulta.

Usando dicionários

Dicionários são um recurso essencial do ClickHouse, fornecendo uma representação chave-valor em memória de dados de várias fontes internas e externas, otimizada para consultas de busca com latência ultrabaixa. Isso é útil em vários cenários, desde enriquecer dados ingeridos em tempo real sem desacelerar o processo de ingestão até melhorar o desempenho das consultas em geral, com benefício especial para as JOINs. Embora junções raramente sejam necessárias em casos de uso de observabilidade, os dicionários ainda podem ser úteis para fins de enriquecimento, tanto no tempo de inserção quanto no tempo de consulta. Fornecemos exemplos de ambos abaixo.
Acelerando junçõesUsuários interessados em acelerar junções com dicionários podem encontrar mais detalhes aqui.

Tempo de inserção vs tempo de consulta

Dicionários podem ser usados para enriquecer conjuntos de dados no tempo de consulta ou no tempo de inserção. Cada uma dessas abordagens tem seus próprios prós e contras. Em resumo:
  • Tempo de inserção - Em geral, isso é apropriado quando o valor de enriquecimento não muda e existe em uma fonte externa que pode ser usada para preencher o dicionário. Nesse caso, enriquecer a linha no tempo de inserção evita a consulta ao dicionário no tempo de consulta. Isso tem um custo em termos de desempenho de inserção, além de uma sobrecarga adicional de armazenamento, já que os valores enriquecidos serão armazenados como colunas.
  • Tempo de consulta - Se os valores em um dicionário mudam com frequência, as consultas no tempo de consulta costumam ser mais adequadas. Isso evita a necessidade de atualizar colunas (e reescrever dados) se os valores mapeados mudarem. Essa flexibilidade vem com o custo de uma consulta no tempo de consulta. Esse custo normalmente só é perceptível quando a consulta é necessária para muitas linhas, por exemplo, ao usar uma consulta ao dicionário em uma cláusula de filtro. Para enriquecimento de resultados, ou seja, no SELECT, essa sobrecarga normalmente não é perceptível.
Recomendamos que os usuários se familiarizem com os conceitos básicos de dicionários. Dicionários fornecem uma tabela de consulta em memória da qual os valores podem ser recuperados usando funções especializadas. Para exemplos simples de enriquecimento, veja o guia sobre Dicionários aqui. Abaixo, focamos em tarefas comuns de enriquecimento de observabilidade.

Usando dicionários de IP

Enriquecer logs e traces com dados geográficos, como latitude e longitude, usando endereços IP é um requisito comum de observabilidade. Podemos fazer isso usando o dicionário estruturado ip_trie. Usamos o dataset do DB-IP em nível de cidade, disponível publicamente e fornecido pela DB-IP.com, nos termos da licença CC BY 4.0. No readme, podemos ver que os dados estão estruturados da seguinte forma:
Dada essa estrutura, vamos começar dando uma olhada nos dados com a função de tabela url():
Para facilitar, vamos usar o mecanismo de tabela URL() para criar um objeto de tabela no ClickHouse com os nomes dos nossos campos e confirmar o número total de linhas:
Como nosso dicionário ip_trie exige que os intervalos de endereços IP sejam expressos em notação CIDR, precisaremos transformar ip_range_start e ip_range_end. O CIDR de cada intervalo pode ser calculado de forma concisa com a seguinte consulta:
Há muita coisa acontecendo na consulta acima. Para quem tiver interesse, leia esta excelente explicação. Caso contrário, basta entender que o trecho acima calcula um CIDR para um intervalo de IP.
Para os nossos propósitos, precisaremos apenas do intervalo de IP, do código do país e das coordenadas, então vamos criar uma nova tabela e inserir nossos dados de Geo IP:
Para realizar consultas de IP de baixa latência no ClickHouse, vamos usar dicionários para armazenar em memória o mapeamento de chave -> atributos dos nossos dados de GeoIP. O ClickHouse fornece uma ip_trie estrutura de dicionário para mapear nossos prefixos de rede (blocos CIDR) para coordenadas e códigos de país. A consulta a seguir especifica um dicionário usando esse layout e a tabela acima como origem.
Podemos selecionar linhas do dicionário e confirmar que esse conjunto de dados está disponível para buscas:
Atualização periódicaOs dicionários no ClickHouse são atualizados periodicamente com base nos dados da tabela subjacente e na cláusula lifetime usada acima. Para atualizar nosso dicionário Geo IP e refletir as alterações mais recentes no dataset DB-IP, basta reinserir os dados da tabela remota geoip_url na nossa tabela geoip, com as transformações aplicadas.
Agora que carregamos os dados de Geo IP no nosso dicionário ip_trie (também chamado, convenientemente, de ip_trie), podemos usá-lo para geolocalização de IP. Isso pode ser feito usando a função dictGet(), como mostrado a seguir:
Observe a velocidade de recuperação aqui. Isso nos permite enriquecer os logs. Neste caso, escolhemos fazer o enriquecimento em tempo de consulta. Voltando ao nosso conjunto original de logs, podemos usar o que foi mostrado acima para agregar os logs por país. O que segue pressupõe que usamos o esquema resultante da nossa visão materializada anterior, que possui uma coluna RemoteAddress extraída.
Como o mapeamento entre IP e localização geográfica pode mudar, é provável que os usuários queiram saber de onde a solicitação se originou no momento em que foi feita — e não qual é a localização geográfica atual desse mesmo endereço. Por esse motivo, o enriquecimento no momento da indexação tende a ser a melhor opção aqui. Isso pode ser feito usando colunas materializadas, como mostrado abaixo, ou na cláusula select de uma visão materializada:
Atualize periodicamenteÉ provável que os usuários queiram que o dicionário de enriquecimento de IP seja atualizado periodicamente com base em novos dados. Isso pode ser feito usando a cláusula LIFETIME do dicionário, o que fará com que ele seja recarregado periodicamente a partir da tabela subjacente. Para atualizar a tabela subjacente, consulte “Views materializadas atualizáveis”.
Os países e as coordenadas acima oferecem recursos de visualização que vão além do agrupamento e da filtragem por país. Para se inspirar, consulte “Visualizando dados geográficos”.

Usando dicionários regex (parsing de user agent)

O parsing de strings de user agent é um problema clássico de expressão regular e um requisito comum em datasets baseados em logs e traces. O ClickHouse oferece parsing eficiente de user agents usando Dicionários em Árvore de Expressões Regulares. Os dicionários em árvore de expressões regulares são definidos no ClickHouse open-source usando o tipo de dictionary source YAMLRegExpTree, que fornece o path para um arquivo YAML que contém a árvore de expressões regulares. Caso você queira fornecer seu próprio dicionário de expressões regulares, os detalhes sobre a estrutura necessária podem ser encontrados aqui. Abaixo, focamos no parsing de user-agent usando uap-core e carregamos nosso dicionário no formato CSV compatível. Essa abordagem é compatível com OSS e ClickHouse Cloud.
Nos exemplos abaixo, usamos snapshots das expressões regulares mais recentes do uap-core para parsing de user-agent de junho de 2024. O arquivo mais recente, que é atualizado ocasionalmente, pode ser encontrado aqui. Você pode seguir os passos aqui para carregar os dados no arquivo CSV usado abaixo.
Crie as tabelas Memory a seguir. Elas armazenam nossas expressões regulares para fazer o parsing de dispositivos, navegadores e sistemas operacionais.
Essas tabelas podem ser populadas a partir dos seguintes arquivos CSV hospedados publicamente, usando a função de tabela url:
Com nossas tabelas em memória preenchidas, podemos carregar nossos dicionários de expressões regulares. Observe que precisamos especificar os valores-chave como colunas - esses serão os atributos que poderemos extrair do user agent.
Com esses dicionários carregados, podemos fornecer um user-agent de exemplo e testar nossos novos recursos de extração com dicionários:
Dado que as regras relacionadas a user agents raramente mudam, e que o dicionário só precisa ser atualizado em resposta a novos navegadores, sistemas operacionais e dispositivos, faz sentido realizar essa extração no momento da inserção. Podemos realizar esse trabalho usando uma coluna materializada ou uma visão materializada. Abaixo, modificamos a visão materializada usada anteriormente:
Isso exige que modifiquemos o esquema da tabela de destino otel_logs_v2:
Após reiniciar o collector e realizar a ingestão de logs estruturados, com base nas etapas documentadas anteriormente, podemos consultar as colunas Device, Browser e Os extraídas recentemente.
Tuplas para estruturas complexasObserve o uso de Tuplas nessas colunas de user agent. Tuplas são recomendadas para estruturas complexas em que a hierarquia é conhecida de antemão. As subcolunas oferecem o mesmo desempenho que as colunas comuns (ao contrário das chaves de map), permitindo ao mesmo tempo tipos heterogêneos.

Leitura complementar

Para ver mais exemplos e detalhes sobre dicionários, recomendamos os seguintes artigos:

Acelerando consultas

O ClickHouse oferece várias técnicas para acelerar o desempenho das consultas. O conteúdo a seguir deve ser considerado somente depois de escolher uma chave primária/de ordenação adequada para otimizar os padrões de acesso mais comuns e maximizar a compressão. Em geral, isso terá o maior impacto no desempenho com o menor esforço.

Usando visões materializadas (incrementais) para agregações

Nas seções anteriores, exploramos o uso de visões materializadas para transformação e filtragem de dados. No entanto, visões materializadas também podem ser usadas para pré-calcular agregações no momento da inserção e armazenar o resultado. Esse resultado pode ser atualizado com os resultados de inserções subsequentes, permitindo, na prática, que uma agregação seja efetivamente pré-calculada no momento da inserção. A ideia principal aqui é que os resultados muitas vezes serão uma representação menor dos dados originais (um resumo parcial, no caso de agregações). Quando combinados com uma consulta mais simples para ler os resultados da tabela de destino, os tempos de consulta serão menores do que se a mesma computação fosse realizada sobre os dados originais. Considere a seguinte consulta, na qual calculamos o tráfego total por hora usando nossos logs estruturados:
Podemos imaginar que este seja um gráfico de linhas comum que os usuários criam no Grafana. É verdade que esta consulta é muito rápida — o conjunto de dados tem apenas 10 milhões de linhas, e o ClickHouse é rápido! No entanto, se escalarmos isso para bilhões ou trilhões de linhas, o ideal é manter esse desempenho de consulta.
Esta consulta seria 10x mais rápida se usássemos a tabela otel_logs_v2, que resulta da nossa visão materializada anterior e extrai a chave size do map LogAttributes. Usamos os dados brutos aqui apenas para fins ilustrativos e recomendamos usar a visão anterior se esta for uma consulta comum.
Precisamos de uma tabela para receber os resultados se quisermos calcular isso no momento da inserção usando uma visão materializada. Essa tabela deve manter apenas 1 linha por hora. Se uma atualização for recebida para uma hora já existente, as outras colunas deverão ser mescladas à linha dessa hora. Para que essa mesclagem de estados incrementais ocorra, estados parciais precisam ser armazenados para as outras colunas. Isso exige um tipo especial de engine no ClickHouse: o SummingMergeTree. Ele substitui todas as linhas com a mesma chave de ordenação por uma única linha que contém valores somados para as colunas numéricas. A tabela a seguir mesclará todas as linhas com a mesma data, somando todas as colunas numéricas.
Para demonstrar nossa visão materializada, suponha que a tabela bytes_per_hour esteja vazia e ainda não tenha recebido nenhum dado. Nossa visão materializada executa o SELECT acima sobre os dados inseridos em otel_logs (isso será feito em blocos de tamanho configurado), com os resultados enviados para bytes_per_hour. A sintaxe é mostrada abaixo:
A cláusula TO aqui é fundamental, pois indica para onde os resultados serão enviados, ou seja, bytes_per_hour. Se reiniciarmos nosso OTel collector e reenviarmos os logs, a tabela bytes_per_hour será preenchida incrementalmente com o resultado da consulta acima. Ao final, podemos confirmar o tamanho da bytes_per_hour — devemos ter 1 linha por hora:
Reduzimos efetivamente o número de linhas aqui de 10m (em otel_logs) para 113 ao armazenar o resultado da nossa consulta. O ponto principal é que, se novos logs forem inseridos na tabela otel_logs, novos valores serão enviados para bytes_per_hour na hora correspondente, onde serão automaticamente mesclados de forma assíncrona em segundo plano — mantendo apenas uma linha por hora, bytes_per_hour será sempre pequena e atualizada. Como a mesclagem das linhas é assíncrona, pode haver mais de uma linha por hora quando um usuário fizer uma consulta. Para garantir que todas as linhas pendentes sejam mescladas no momento da consulta, temos duas opções:
  • Usar o modificador FINAL no nome da tabela (o que fizemos na consulta de contagem acima).
  • Agregar pela chave de ordenação usada na nossa tabela final, ou seja, Timestamp, e somar as métricas.
Em geral, a segunda opção é mais eficiente e flexível (a tabela pode ser usada para outras finalidades), mas a primeira pode ser mais simples para algumas consultas. Mostramos ambas abaixo:
Isso reduziu o tempo da nossa consulta de 0,6s para 0,008s — mais de 75 vezes!
Esses ganhos podem ser ainda maiores em conjuntos de dados maiores e com consultas mais complexas. Veja aqui para ver exemplos.

Um exemplo mais complexo

O exemplo acima agrega uma contagem simples por hora usando o SummingMergeTree. Estatísticas além de somas simples exigem um mecanismo de tabela de destino diferente: o AggregatingMergeTree. Suponha que queiramos calcular o número de endereços IP exclusivos (ou de usuários exclusivos) por dia. A consulta para isso:
Para persistir uma contagem de cardinalidade para atualização incremental, é necessário usar o AggregatingMergeTree.
Para garantir que o ClickHouse saiba que estados de agregação serão armazenados, definimos a coluna UniqueUsers como do tipo AggregateFunction, especificando a função de origem dos estados parciais (uniq) e o tipo da coluna de origem (IPv4). Assim como no SummingMergeTree, linhas com o mesmo valor da chave ORDER BY serão mescladas (Hour no exemplo acima). A visão materializada associada usa a consulta anterior:
Observe como acrescentamos o sufixo State ao final das nossas funções de agregação. Isso garante que o estado de agregação da função seja retornado em vez do resultado final. Ele conterá informações adicionais que permitem mesclar esse estado parcial com outros estados. Depois que os dados forem recarregados, por meio de uma reinicialização do Collector, poderemos confirmar que 113 linhas estão disponíveis na tabela unique_visitors_per_hour.
Nossa consulta final precisa usar o sufixo Merge nas funções (já que as colunas armazenam estados de agregação parciais):
Observe que usamos GROUP BY aqui em vez de FINAL.

Usando visões materializadas (incrementais) para consultas rápidas

Você deve considerar os padrões de acesso ao escolher a chave de ordenação do ClickHouse, levando em conta as colunas usadas com frequência em cláusulas de filtro e agregação. Isso pode ser restritivo em casos de uso de observabilidade, nos quais os usuários têm padrões de acesso mais diversos que não podem ser encapsulados em um único conjunto de colunas. Isso fica mais claro em um exemplo presente nos esquemas padrão do OTel. Considere o esquema padrão para os traces:
Este esquema é otimizado para filtrar por ServiceName, SpanName e Timestamp. Em tracing, os usuários também precisam conseguir fazer consultas por um TraceId específico e recuperar os spans associados a esse trace. Embora isso esteja presente na chave de ordenação, sua posição no final significa que a filtragem não será tão eficiente e provavelmente fará com que seja necessário varrer volumes significativos de dados ao recuperar um único trace. O OTel collector também instala uma visão materializada e a tabela associada para resolver esse desafio. A tabela e a visão são mostradas abaixo:
A view garante, na prática, que a tabela otel_traces_trace_id_ts tenha o timestamp mínimo e máximo do trace. Essa tabela, ordenada por TraceId, permite recuperar esses timestamps com eficiência. Esses intervalos de timestamp podem, por sua vez, ser usados ao consultar a tabela principal otel_traces. Mais especificamente, ao recuperar um trace pelo ID, o Grafana usa a seguinte consulta:
A CTE aqui identifica o timestamp mínimo e máximo para o ID do trace ae9226c78d1d360601e6383928e4d22d, antes de usá-los para filtrar a tabela principal otel_traces pelos spans associados. Essa mesma abordagem pode ser aplicada a padrões de acesso semelhantes. Exploramos um exemplo parecido em Modelagem de dados aqui.

Usando projeções

As projeções do ClickHouse permitem especificar múltiplas cláusulas ORDER BY para uma tabela. Nas seções anteriores, exploramos como visões materializadas podem ser usadas no ClickHouse para pré-computar agregações, transformar linhas e otimizar consultas de observabilidade para diferentes padrões de acesso. Apresentamos um exemplo em que a visão materializada envia linhas para uma tabela de destino com uma chave de ordenação diferente da tabela original que recebe inserções, a fim de otimizar buscas por ID de trace. As projeções podem ser usadas para resolver o mesmo problema, permitindo que o usuário otimize consultas em uma coluna que não faz parte da chave primária. Em teoria, essa capacidade pode ser usada para fornecer múltiplas chaves de ordenação para uma tabela, com uma desvantagem clara: duplicação de dados. Especificamente, os dados precisarão ser gravados na ordem da chave primária principal, além da ordem especificada para cada projeção. Isso tornará as inserções mais lentas e consumirá mais espaço em disco.
Projeções vs. visões materializadasAs projeções oferecem muitos dos mesmos recursos que as visões materializadas, mas devem ser usadas com parcimônia, sendo as visões materializadas geralmente a opção preferida. Você deve entender as desvantagens e quando elas são apropriadas. Por exemplo, embora as projeções possam ser usadas para pré-computar agregações, recomendamos que os usuários usem visões materializadas para isso.
Considere a seguinte consulta, que filtra nossa tabela otel_logs_v2 por códigos de erro 500. Esse provavelmente é um padrão de acesso comum em logs, já que os usuários costumam querer filtrar por códigos de erro:
Use Null para medir o desempenhoNão exibimos os resultados aqui usando FORMAT Null. Isso faz com que todos os resultados sejam lidos, mas não retornados, evitando assim que a consulta seja encerrada antecipadamente por causa de um LIMIT. Isso serve apenas para mostrar o tempo necessário para varrer todas as 10 milhões de linhas.
A consulta acima exige uma varredura linear com a chave de ordenação escolhida (ServiceName, Timestamp). Embora pudéssemos adicionar Status ao fim da chave de ordenação, melhorando o desempenho da consulta acima, também podemos adicionar uma projeção.
Observe que primeiro precisamos criar a projeção e depois materializá-la. Esse último comando faz com que os dados sejam armazenados duas vezes em disco, em duas ordenações diferentes. A projeção também pode ser definida no momento da criação dos dados, como mostrado abaixo, e será mantida automaticamente à medida que os dados forem inseridos.
É importante observar que, se a projeção for criada por meio de um ALTER, sua criação ocorrerá de forma assíncrona quando o comando MATERIALIZE PROJECTION for emitido. Você pode acompanhar o progresso dessa operação com a consulta a seguir, aguardando até que is_done=1.
Se repetirmos a consulta acima, veremos que o desempenho melhorou significativamente, em troca de armazenamento adicional (veja “Medindo o tamanho da tabela e a compressão” para ver como medir isso).
No exemplo acima, especificamos na projeção as colunas usadas na consulta anterior. Isso significa que apenas essas colunas serão armazenadas em disco como parte da projeção, ordenadas por Status. Se, em vez disso, usássemos SELECT * aqui, todas as colunas seriam armazenadas. Embora isso permitisse que mais consultas (usando qualquer subconjunto de colunas) se beneficiassem da projeção, isso também consumiria armazenamento adicional. Para medir o espaço em disco e a compressão, consulte “Medindo o tamanho da tabela e a compressão”.

Índices secundários/de data skipping

Por mais bem ajustada que esteja a chave primária no ClickHouse, algumas consultas inevitavelmente exigirão varreduras completas da tabela. Embora isso possa ser mitigado com o uso de visões materializadas (e projeções, em algumas consultas), elas exigem manutenção adicional e que os usuários saibam que estão disponíveis para garantir que sejam aproveitadas. Embora bancos de dados relacionais tradicionais resolvam isso com índices secundários, eles são ineficazes em bancos de dados orientados a colunas como o ClickHouse. Em vez disso, o ClickHouse usa índices “Skip”, que podem melhorar significativamente o desempenho das consultas ao permitir que o banco de dados ignore grandes fragmentos de dados sem valores correspondentes. Os esquemas padrão do OTel usam índices secundários na tentativa de acelerar o acesso ao tipo map. Embora, em geral, consideremos isso ineficaz e não recomendemos copiá-los para o seu esquema personalizado, os índices de skipping ainda podem ser úteis. Você deve ler e entender o guia sobre índices secundários antes de tentar aplicá-los. Em geral, eles são eficazes quando existe uma forte correlação entre a chave primária e a coluna/expressão não primária alvo, e os usuários estão buscando valores raros, ou seja, aqueles que não ocorrem em muitos grânulos. O ClickHouse fornece um índice de texto especializado para busca de texto completo. Esse índice cria um índice invertido sobre dados de texto tokenizados, permitindo consultas de busca rápidas baseadas em tokens. Os índices de texto estão disponíveis a partir da versão 26.2 do ClickHouse. Eles podem ser definidos nos seguintes tipos de coluna em tabelas MergeTree: String, FixedString, Array(String), Array(FixedString) e Map (por meio das funções de map mapKeys e mapValues). Um índice de texto exige um argumento tokenizer em sua definição. Opcionalmente, uma função de pré-processador pode ser especificada para transformar a string de entrada antes da tokenização. As funções recomendadas para pesquisar no índice são: hasAnyTokens e hasAllTokens. Algumas funções tradicionais de busca em strings também são otimizadas automaticamente quando há um índice de texto. Consulte a documentação para mais detalhes e funções compatíveis aqui e aqui. Nos exemplos abaixo, usamos um conjunto de dados estruturado de logs.
Também podemos usar hasAnyTokens sem índice de texto, mas a consulta fará uma varredura completa e lenta da coluna Body:

Adicionando um índice de texto

É possível adicionar um índice de texto à coluna Body durante a criação da tabela:
ou adicionados posteriormente com ALTER TABLE:
Se executarmos a mesma consulta SELECT novamente, ela fará uma busca no índice de texto. O volume de dados acessados cai de gigabytes para megabytes, e o desempenho melhora em cerca de 45x.

Usando um pré-processador

Neste conjunto de dados, a coluna Body contém uma string em formato JSON com vários pares chave-valor (por exemplo, msg, id, ctx, attr, etc.). Suponha que estejamos interessados apenas em pesquisar no campo msg. Em vez de indexar a string JSON inteira, podemos definir um pré-processador para extrair somente o valor de msg antes da tokenização. Por exemplo:
Neste exemplo, o pré-processador:
  • reduz a quantidade de texto tokenizada e indexada,
  • diminui o tamanho do índice,
  • reduz a probabilidade de falsos positivos e
  • melhora o desempenho das consultas.
Em comparação com o índice sem pré-processamento, o desempenho melhora em cerca de 2x. O uso de um pré-processador também reduz o tamanho do índice de gigabytes para algumas centenas de kilobytes, aproximadamente 0,01% do tamanho original
**Outros índices para busca de texto Mais detalhes sobre índices secundários de salto podem ser encontrados aqui.

Extraindo de maps

O tipo Map é comum nos schemas do OTel. Esse tipo exige que os valores e as chaves tenham o mesmo tipo — o que é suficiente para metadados como labels do Kubernetes. Tenha em mente que, ao consultar uma subchave de um tipo Map, toda a coluna pai é carregada. Se o map tiver muitas chaves, isso pode causar um impacto significativo no desempenho da consulta, já que será necessário ler mais dados do disco do que se a chave existisse como uma coluna. Se você consulta com frequência uma chave específica, considere movê-la para uma coluna dedicada própria no nível raiz. Normalmente, isso é feito em resposta a padrões de acesso comuns e após a implantação, e pode ser difícil de prever antes da produção. Consulte “Gerenciando alterações de schema” para saber como modificar seu schema após a implantação.

Medindo o tamanho da tabela e a compressão

Um dos principais motivos pelos quais o ClickHouse é usado para observabilidade é a compressão. Além de reduzir drasticamente os custos de armazenamento, menos dados em disco significam menos E/S e consultas e inserções mais rápidas. A redução de E/S compensará com folga a sobrecarga de qualquer algoritmo de compressão em termos de CPU. Portanto, melhorar a compressão dos dados deve ser o primeiro foco ao trabalhar para garantir que as consultas no ClickHouse sejam rápidas. Detalhes sobre como medir a compressão podem ser encontrados aqui.
Última modificação em 23 de julho de 2026