
Guia avançada de ClickHouse: otimização, vistas materializadas e truques reais
Publicado em:
Tempo de leitura: 20 min
Tema: Tecnologia
Autor: Leandro Valencia
Como tirar o máximo de ClickHouse: ORDER BY, vistas materializadas, projections, codecs, TTL, tiering para S3 e diagnóstico de consultas lentas. Com SQL real.
Índice
- Princípio fundamental: ler menos dados
- 3. Projections: a mesma tabela ordenada de duas formas
- 9. Consultas mais inteligentes
- A minha checklist de otimização
- Uma reflexão final sobre otimizar
Princípio fundamental: ler menos dados
Tudo o que se segue é uma variação da mesma ideia. ClickHouse não é rápido porque processa dados depressa —também o faz— mas porque processa muito menos dados do que pensas.
Cada consulta que lanças diz-te exatamente quanto leu:
6 rows in set. Elapsed: 0.021 sec. Processed 1.14 million rows, 8.32 MB
Essa linha é a tua métrica principal. Se a tua tabela tem 500 milhões de linhas e uma consulta processa 500 milhões, não otimizaste nada: estás a fazer um escaneamento completo muito rápido. O objetivo é sempre baixar esse número.
As ferramentas para o baixar, por ordem de impacto: a chave de ordenação, as vistas materializadas, as projections, os índices de salto e a particionação. Nessa ordem.
1. A chave de ordenação: a decisão que determina tudo
Se só levas uma coisa deste post, que seja esta secção.
Como funciona o índice esparso
ClickHouse não tem índices B-tree por linha. Ordena fisicamente os dados no disco segundo o teu ORDER BY e guarda uma marca a cada 8.192 linhas (um grânulo), anotando o valor da chave nessa posição. O índice completo ocupa uns poucos megabytes por terabyte.
Quando filtras por uma coluna que está no ORDER BY, ClickHouse olha para esse índice, identifica que grânulos poderiam conter valores coincidentes e lê só esses. Tudo o resto não se toca.
A consequência é direta: uma coluna só acelera a tua consulta se está no prefixo do ORDER BY. Se a tua chave é (evento, fecha, usuario_id), filtrar por evento é rapidíssimo, filtrar por evento AND fecha também, mas filtrar só por usuario_id obriga a um escaneamento completo. A ordem importa igual que num índice composto de Postgres, mas as consequências são muito maiores.
As três regras para escolher o ORDER BY
Regra 1: primeiro as colunas pelas quais filtras sempre. Olha para as tuas consultas reais, não as hipotéticas. Se 95% levam WHERE tenant_id = ?, tenant_id vai primeiro.
Regra 2: a igualdade de uso, menor cardinalidade primeiro. Uma coluna com 6 valores distintos (país) agrupa melhor os dados que uma com 50.000 (usuario_id). Pôr alta cardinalidade à frente fragmenta os grânulos e arruína tanto a compressão como o salto de blocos.
Regra 3: a coluna temporal quase sempre vai, mas raramente primeira. É tentador pôr timestamp ao início porque "tudo se filtra por data". Costuma ser melhor (tenant_id, evento, fecha) que (fecha, tenant_id, evento), porque a data já está bastante correlacionada com a ordem natural de inserção.
Um exemplo de tabela bem desenhada:
CREATE TABLE eventos
(
tenant_id UInt32,
fecha Date,
timestamp DateTime CODEC(Delta, ZSTD(1)),
evento LowCardinality(String),
pais LowCardinality(String),
usuario_id UInt32 CODEC(Delta, ZSTD(1)),
url String CODEC(ZSTD(3)),
duracion_ms UInt32 CODEC(T64, ZSTD(1))
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(fecha)
ORDER BY (tenant_id, evento, fecha, usuario_id);
Separar chave primária de chave de ordenação
Um detalhe que pouca gente usa: podes ter um ORDER BY longo e uma PRIMARY KEY mais curta.
ENGINE = MergeTree
ORDER BY (tenant_id, evento, fecha, usuario_id)
PRIMARY KEY (tenant_id, evento)
O ORDER BY controla a ordem física (e portanto a compressão), enquanto a PRIMARY KEY controla o que se guarda no índice em memória. Se a tua chave de ordenação tem cinco colunas e só filtras pelas duas primeiras, isto reduz o uso de RAM do índice sem perder nada.
Como verificar que a tua chave funciona
Ativa o trace e observa quantos grânulos se descartam:
SET send_logs_level = 'trace';
SELECT count() FROM eventos WHERE tenant_id = 42 AND evento = 'purchase';
Nos logs verás linhas como:
Key condition: (column 0 in [42, 42]), (column 1 in ['purchase', 'purchase'])
Selected 3/48 parts by partition key, 3 parts by primary key, 14/61035 marks by primary key
14/61035 marks significa que leste 0,02% da tabela. Isso é uma chave bem escolhida. Se ves 61035/61035, o teu filtro não está a usar o índice.
2. Vistas materializadas: mover o trabalho para o momento da inserção
Esta é a característica com mais impacto e a mais mal interpretada.
Uma vista materializada em ClickHouse não é uma cache nem uma tabela que se refresca periodicamente. É um trigger de inserção: cada vez que chega um bloco de dados à tabela origem, a consulta executa-se sobre esse bloco e o resultado escreve-se na tabela destino. Os dados originais nunca se voltam a ler.
Isto tem uma implicação crítica: uma vista materializada só vê os dados que se inserem depois de a criar. Os históricos têm de ser preenchidos à mão.
O padrão standard
Três peças: tabela origem, tabela destino de agregados, e a vista que as conecta.
-- 1. Tabela destino com estados de agregação
CREATE TABLE eventos_diarios
(
tenant_id UInt32,
fecha Date,
evento LowCardinality(String),
pais LowCardinality(String),
total UInt64,
usuarios_unicos AggregateFunction(uniq, UInt32),
duracion_media AggregateFunction(avg, UInt32)
)
ENGINE = SummingMergeTree
ORDER BY (tenant_id, fecha, evento, pais);
-- 2. A vista materializada que a alimenta
CREATE MATERIALIZED VIEW mv_eventos_diarios
TO eventos_diarios
AS
SELECT
tenant_id,
fecha,
evento,
pais,
count() AS total,
uniqState(usuario_id) AS usuarios_unicos,
avgState(duracion_ms) AS duracion_media
FROM eventos
GROUP BY tenant_id, fecha, evento, pais;
Repara em uniqState e avgState. O sufixo -State guarda o estado intermédio da agregação em vez do resultado final. Isto é o que permite combinar agregados de blocos distintos corretamente.
Para consultar, usas o sufixo -Merge:
SELECT
fecha,
evento,
sum(total) AS eventos,
uniqMerge(usuarios_unicos) AS usuarios,
avgMerge(duracion_media) AS duracion
FROM eventos_diarios
WHERE tenant_id = 42
AND fecha >= today() - 30
GROUP BY fecha, evento
ORDER BY fecha;
Por que isto importa tanto: esta consulta lê umas poucas milhares de linhas pré-agregadas em vez de centenas de milhões de linhas cruas. A diferença não é de 2x, é de duas ou três ordens de magnitude. E como o cálculo se faz na inserção, o custo está distribuído no tempo em vez de concentrado no momento em que o teu utilizador olha para o dashboard.
Um erro frequente: usar avg() em lugar de avgState() e depois promediar promédios ao consultar. Isso dá resultados incorretos porque a média de médias não é a média. Os estados de agregação existem precisamente para evitar isto.
Preencher o histórico
Como as vistas só capturam dados novos, depois de criar a vista preenche para trás com um INSERT ... SELECT sobre a mesma consulta:
INSERT INTO eventos_diarios
SELECT
tenant_id, fecha, evento, pais,
count() AS total,
uniqState(usuario_id) AS usuarios_unicos,
avgState(duracion_ms) AS duracion_media
FROM eventos
WHERE fecha < today()
GROUP BY tenant_id, fecha, evento, pais;
Faz-o por intervalos de datas se a tabela é grande, para não rebentar a memória.
Vistas materializadas refrescáveis
Desde há algumas versões existe também a variante refrescável, que sim volta a executar a consulta completa periodicamente:
CREATE MATERIALIZED VIEW mv_resumen
REFRESH EVERY 1 HOUR
ENGINE = MergeTree ORDER BY fecha
AS SELECT ... ;
Útil quando precisas JOINs com tabelas de dimensões que mudam, algo que as vistas incrementais lidam mal. Mais cara, mas muito mais simples de raciocinar.
3. Projections: a mesma tabela ordenada de duas formas
Aqui está o problema clássico: o teu ORDER BY está otimizado para filtrar por tenant_id, mas também precisas de consultas rápidas filtrando por url. Só podes ter uma ordem física.
As projections resolvem isto guardando uma cópia adicional dos dados ordenada de outra forma, dentro da mesma tabela. ClickHouse escolhe automaticamente qual usar.
ALTER TABLE eventos ADD PROJECTION proj_por_url
(
SELECT
url,
fecha,
count(),
avg(duracion_ms)
GROUP BY url, fecha
);
-- Materializar para os dados existentes
ALTER TABLE eventos MATERIALIZE PROJECTION proj_por_url;
Agora uma consulta que agrupe por url usará a projection sem que tenhas de mudar o SQL. Verifica-o com EXPLAIN:
EXPLAIN indexes = 1
SELECT url, count() FROM eventos WHERE fecha >= today() - 7 GROUP BY url;
Projections vs. vistas materializadas. A pergunta óbvia. O meu critério:
Usa projections quando queres os mesmos dados com outra ordem ou uma agregação derivada da mesma tabela, e valorizas que seja transparente (não mudas as consultas, a consistência é automática).
Usa vistas materializadas quando precisas de transformar os dados, escrever numa tabela com TTL distinto, encadear várias etapas ou combinar várias fontes. E quando a tabela destino deve sobreviver de forma independente.
O custo das projections é armazenamento (guardas os dados duas vezes) e velocidade de inserção. Não acrescentes cinco projections a uma tabela que recebe inserts constantes.
4. Codecs de compressão: o ajuste mais subvalorizado
Menos bytes no disco = menos I/O = consultas mais rápidas. A compressão não é um tema de poupança de armazenamento, é um tema de desempenho.
ClickHouse aplica LZ4 por defeito e funciona bem, mas afinar por coluna dá melhorias reais. A documentação oficial recomenda, por ordem de importância:
ZSTD como base. Oferece as melhores taxas de compressão e ZSTD(1) é um bom valor por defeito para a maioria dos tipos. Subir acima de ZSTD(3) raramente compensa o custo de inserção.
Delta para sequências de datas e inteiros. Funciona muito bem com sequências monótonas ou com diferenças pequenas entre valores consecutivos. Timestamps e IDs autoincrementais são o caso canónico. Se o resultado da primeira derivada não é pequeno o suficiente, prova DoubleDelta.
Delta melhora ZSTD. Combinam-se bem: CODEC(Delta, ZSTD(1)) costuma bater qualquer um dos dois separadamente.
LZ4 se empata com ZSTD. Se obténs compressão comparável, prefere LZ4 porque descomprime mais rápido e consome menos CPU. Na prática ZSTD ganha por bastante margem na maioria dos casos.
T64 para intervalos pequenos ou dados dispersos. Eficaz quando o intervalo de valores dentro de um bloco é reduzido. Evita-o com números aleatórios.
Gorilla para floats de tipo sensor. Desenhado para leituras de medidores com variações pequenas.
Na prática:
CREATE TABLE metricas
(
timestamp DateTime CODEC(Delta, ZSTD(1)),
sensor_id UInt32 CODEC(Delta, ZSTD(1)),
temperatura Float32 CODEC(Gorilla, ZSTD(1)),
estado LowCardinality(String),
payload String CODEC(ZSTD(3))
)
ENGINE = MergeTree
ORDER BY (sensor_id, timestamp);
Mede antes e depois
SELECT
name,
formatReadableSize(sum(data_compressed_bytes)) AS comprimido,
formatReadableSize(sum(data_uncompressed_bytes)) AS sin_comprimir,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.columns
WHERE table = 'metricas'
GROUP BY name
ORDER BY sum(data_compressed_bytes) DESC;
Ordena por tamanho comprimido e ataca só as duas ou três colunas maiores. Otimizar o codec de uma coluna que ocupa 3 MB é tempo perdido.
Nota sobre partes compactas: se ves zeros em compressed_size, é porque as tuas partes são do tipo compact em vez de wide (ocorre com inserções pequenas). Controlam-no min_bytes_for_wide_part e min_rows_for_wide_part.
O tipo de dado também é compressão
Antes de tocar codecs, revê tipos. Um UInt8 em vez de um Int64 para um valor de 0 a 100 é uma redução de 8x gratuita. LowCardinality(String) para colunas com menos de ~10.000 valores distintos. DateTime em vez de String para datas. A documentação de ClickHouse mostra que só otimizando tipos e chave de ordenação, um mesmo dataset passou de 50 GB a 25 GB comprimidos.
5. Índices de salto: usa-os com cuidado
Os data skipping indexes permitem saltar blocos filtrando por colunas que não estão no ORDER BY. Soam como a solução universal e quase nunca o são.
ALTER TABLE eventos ADD INDEX idx_pais pais TYPE set(100) GRANULARITY 4;
ALTER TABLE eventos MATERIALIZE INDEX idx_pais;
Tipos principais:
minmax guarda o mínimo e o máximo por bloco. O mais barato de aplicar. Ideal para colunas aproximadamente ordenadas e para filtros por intervalo.
set(N) guarda até N valores distintos por bloco. Bom com baixa cardinalidade dentro de cada bloco mas alta cardinalidade global.
bloom_filter para provar pertença a conjuntos grandes de valores. Funciona sobre arrays e maps.
text é o índice invertido real para pesquisa de texto completo. É o recomendado hoje; os antigos tokenbf_v1 e ngrambf_v1 estão marcados como deprecados.
O aviso importante
A documentação oficial é explícita nisto e merece repeti-lo: o impulso natural de acrescentar um índice a uma coluna que consultas muito costuma ser incorreto em ClickHouse.
Um índice de salto só serve se existe correlação forte entre a chave primária e a coluna indexada. Se os valores dessa coluna estão repartidos aleatoriamente por toda a tabela, cada bloco conterá algum e não se saltará nada. Terás pago o custo do índice (em inserção e em consulta) em troca de zero benefício.
Antes de acrescentar um índice de salto, prova nesta ordem: mudar o ORDER BY, acrescentar uma projection, ou criar uma vista materializada. Os índices de salto são o último recurso, não o primeiro.
Um caso onde sim brilham: valores raros mas importantes. Um índice set sobre error_code numa tabela de logs permite saltar a enorme maioria de blocos sem erros.
6. TTL e tiering: dados que se limpam sozinhos
A retenção automática é uma das melhores funcionalidades operativas de ClickHouse e uma das menos usadas.
Apagamento automático
ALTER TABLE eventos MODIFY TTL fecha + INTERVAL 90 DAY;
Os dados com mais de 90 dias desaparecem nas fusões de fundo. Sem cron, sem script, sem supervisão.
Agregação ao envelhecer
Mais interessante: reduzir a resolução dos dados antigos em vez de os apagar.
-- A chave de agrupamento do TTL deve ser prefixo da chave primária
CREATE TABLE metricas_sensores
(
timestamp DateTime CODEC(Delta, ZSTD(1)),
sensor_id UInt32 CODEC(Delta, ZSTD(1)),
temperatura Float32 CODEC(Gorilla, ZSTD(1)),
estado LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (sensor_id, toStartOfHour(timestamp), timestamp);
ALTER TABLE metricas_sensores MODIFY TTL
timestamp + INTERVAL 7 DAY
GROUP BY sensor_id, toStartOfHour(timestamp)
SET temperatura = avg(temperatura);
Dados por segundo durante uma semana, por hora depois. Manténs o histórico útil ocupando uma fração do espaço.
Olho com este detalhe, porque é onde falha toda a gente a primeira vez: ClickHouse exige que as colunas do GROUP BY do TTL sejam um prefixo da chave primária. Se a tua tabela está ordenada por (sensor_id, timestamp) e agrupas por toStartOfHour(timestamp), obterás o erro TTL Expression GROUP BY key should be a prefix of primary key. A solução é incluir a expressão arredondada no próprio ORDER BY, como no exemplo acima.
Tiering para S3
O truque que torna viável a retenção longa com baixo custo. Configura um disco S3 e uma política de armazenamento:
<!-- /etc/clickhouse-server/config.d/storage.xml -->
<clickhouse>
<storage_configuration>
<disks>
<s3_frio>
<type>s3</type>
<endpoint>https://mi-bucket.s3.eu-west-1.amazonaws.com/clickhouse/</endpoint>
<access_key_id>TU_ACCESS_KEY</access_key_id>
<secret_access_key>TU_SECRET_KEY</secret_access_key>
</s3_frio>
</disks>
<policies>
<caliente_frio>
<volumes>
<caliente><disk>default</disk></caliente>
<frio><disk>s3_frio</disk></frio>
</volumes>
</caliente_frio>
</policies>
</storage_configuration>
</clickhouse>
E aplica-a:
ALTER TABLE eventos MODIFY SETTING storage_policy = 'caliente_frio';
ALTER TABLE eventos MODIFY TTL
fecha + INTERVAL 30 DAY TO VOLUME 'frio',
fecha + INTERVAL 365 DAY DELETE;
Os últimos 30 dias em SSD local (rápido), o resto em S3 (barato), e tudo se apaga ao ano. Para um projeto pequeno isto é o que converte "guardo 30 dias porque não posso pagar mais" em "guardo dois anos por cêntimos". As consultas sobre dados frios são mais lentas, obviamente, mas continuam a funcionar e são as que menos se fazem.
7. Inserção: o erro que afunda toda a gente
O problema número um em produção não são as consultas lentas, é Too many parts.
Cada INSERT cria um part novo no disco. Os parts fundem-se em segundo plano, mas se inseres mais rápido do que se fundem, acumulam-se até que o servidor rejeita escritas.
Regra: insere em lotes
Mínimo 1.000 linhas por insert; idealmente entre 10.000 e 100.000. Acumula na tua aplicação e descarrega por tamanho ou por tempo (o que ocorrer antes).
Alternativa: inserts assíncronos
Se a tua arquitetura não permite acumular facilmente —por exemplo, muitos processos a escrever eventos soltos— deixa que ClickHouse o faça por ti:
SET async_insert = 1;
SET wait_for_async_insert = 1;
ClickHouse acumula num buffer do servidor e descarrega em lotes. Com wait_for_async_insert = 1 o cliente espera confirmação de escritura real (mais seguro); com 0 recebe confirmação imediata (mais rápido, mas podes perder dados se o servidor cair antes do flush).
Ajusta o comportamento do buffer com async_insert_max_data_size e async_insert_busy_timeout_ms.
Diagnóstico
SELECT
table,
count() AS num_parts,
sum(rows) AS filas,
formatReadableSize(sum(bytes_on_disk)) AS tamano
FROM system.parts
WHERE active AND database = 'creacosas'
GROUP BY table
ORDER BY num_parts DESC;
Se uma tabela tem centenas de parts ativos, o teu padrão de inserção é o problema.
E não, OPTIMIZE TABLE ... FINAL não é a solução. Força uma fusão completa que é caríssima em tabelas grandes e não conserta a causa. A documentação oficial recomenda explicitamente evitá-lo.
8. Diagnóstico: encontrar o que corre mal
O query log é a tua melhor ferramenta
ClickHouse regista cada consulta com as suas métricas:
SELECT
query_duration_ms,
formatReadableQuantity(read_rows) AS filas_leidas,
formatReadableSize(read_bytes) AS bytes_leidos,
formatReadableSize(memory_usage) AS memoria,
substring(query, 1, 120) AS consulta
FROM system.query_log
WHERE type = 'QueryFinish'
AND event_time > now() - INTERVAL 1 HOUR
ORDER BY query_duration_ms DESC
LIMIT 20;
Ordena por duração e ataca as piores. Compara read_rows com o total da tabela: se estão perto, o teu filtro não está a usar o índice.
EXPLAIN
EXPLAIN indexes = 1
SELECT ... ;
Mostra-te que índices e projections se estão a usar e quantos grânulos se descartam em cada passo.
PREWHERE
ClickHouse costuma aplicar esta otimização automaticamente, mas podes forçá-la. PREWHERE lê primeiro as colunas do filtro, descarta linhas, e só depois lê o resto das colunas:
SELECT url, payload_grande
FROM eventos
PREWHERE evento = 'error' -- lê só esta coluna primeiro
WHERE duracion_ms > 5000;
É especialmente útil quando filtras por uma coluna pequena e selecionas colunas grandes.
Cache de consultas
Para dashboards com consultas repetidas idênticas:
SELECT ... SETTINGS use_query_cache = 1;
Configura o TTL globalmente. Não é uma bala de prata —só serve se as consultas se repetem literalmente— mas num dashboard que várias pessoas olham ao mesmo tempo, poupa bastante.
9. Consultas mais inteligentes
Agregação aproximada quando a exatidão não é crítica. uniq() usa HyperLogLog e é muito mais rápido e leve em memória que uniqExact(). Para um dashboard de "utilizadores únicos", um erro de 0,5% é irrelevante.
Dicionários em vez de JOINs. Se fazes JOIN repetidamente contra uma tabela de dimensões pequena, carrega-a como dicionário em memória:
CREATE DICTIONARY dic_paises (
codigo String,
nombre String
)
PRIMARY KEY codigo
SOURCE(CLICKHOUSE(TABLE 'paises'))
LAYOUT(COMPLEX_KEY_HASHED())
LIFETIME(3600);
SELECT dictGet('dic_paises', 'nombre', pais) AS pais_nombre, count()
FROM eventos GROUP BY pais;
Um dictGet é uma consulta a um hash em memória: ordens de magnitude mais rápido que um JOIN.
Funções de combinador. -If, -Array, -Merge evitam subconsultas inteiras:
SELECT
fecha,
countIf(evento = 'purchase') AS compras,
countIf(evento = 'signup') AS registros,
avgIf(duracion_ms, evento = 'pageview') AS duracion_pageview
FROM eventos
GROUP BY fecha;
Uma só passagem sobre os dados em vez de três.
windowFunnel para funis de conversão. Calcular um funil em SQL padrão é doloroso. Aqui é uma função:
SELECT
nivel,
count() AS usuarios
FROM (
SELECT
usuario_id,
windowFunnel(3600)(
timestamp,
evento = 'pageview',
evento = 'signup',
evento = 'purchase'
) AS nivel
FROM eventos
WHERE fecha >= today() - 30
GROUP BY usuario_id
)
GROUP BY nivel
ORDER BY nivel;
A minha checklist de otimização
A ordem em que ataco um ClickHouse lento, por relação impacto/esforço:
- Olhar
system.query_loge encontrar as consultas realmente lentas. Não otimizes às cegas. - Comprovar se o
ORDER BYserve aos filtros mais frequentes. Se não, refazer a tabela. Dói, e é o que mais resultado dá. - Rever tipos de dados.
LowCardinality, inteiros do tamanho correto, tirarNullabledesnecessários. - Criar vistas materializadas para as agregações que alimentam dashboards.
- Verificar o padrão de inserção. Contar parts ativos; batching se há demasiados.
- Aplicar codecs às duas ou três colunas maiores.
- Acrescentar projections para o segundo padrão de acesso mais comum.
- Configurar TTL e tiering para que o custo de armazenamento não cresça sem controlo.
- Índices de salto, só se tudo o anterior não chega e há correlação real.
Os passos 1 a 4 costumam resolver 90% dos problemas. Se estás no passo 9 com frequência, provavelmente o problema está no desenho do esquema, não na falta de índices.
Uma reflexão final sobre otimizar
A melhor otimização que alguma vez fiz em ClickHouse foi apagar uma tabela.
Tínhamos uma tabela de eventos crus que ninguém consultava diretamente: toda a gente usava as vistas materializadas. Estava lá "por se acaso", ocupando a maior parte do disco e a abrandar as fusões. Pusemos-lhe um TTL de 30 dias, e tudo —inserções, consultas, backups— melhorou.
A tentação em ClickHouse é afinar. Há tantas alavancas —codecs, índices, projections, ajustes de merge— que é fácil passar semanas a ajustar parâmetros para ganhar 15%. Quase sempre há uma decisão de desenho que dá um 10x e está à vista: uma chave de ordenação mal escolhida, uma vista materializada que não existe, ou dados que não devias estar a guardar.
Otimiza o desenho antes dos parâmetros. E mede sempre: a linha de "processed N rows" no fim de cada consulta diz-te a verdade, não importando quão elegante seja a tua configuração.
Perguntas frequentes
Como escolho o ORDER BY em ClickHouse?
Põe primeiro as colunas pelas quais filtras em quase todas as consultas, e a igualdade de uso, as de menor cardinalidade antes que as de maior. Verifica o resultado com send_logs_level='trace' olhando quantas marcas se selecionam.
Qual é a diferença entre vista materializada e projection?
A vista materializada escreve numa tabela independente e dispara-se na inserção, permitindo transformações e TTL próprios. A projection guarda uma cópia alternativa dentro da mesma tabela e ClickHouse escolhe-a automaticamente, sem mudar as tuas consultas.
Por que motivo o meu ClickHouse dá o erro "Too many parts"?
Estás a inserir em lotes demasiado pequenos ou com demasiada frequência. Agrupa em lotes de pelo menos 1.000 linhas ou ativa async_insert = 1.
Devo usar OPTIMIZE TABLE FINAL?
Como regra geral, não. Força fusões muito custosas e não soluciona a causa do problema. A documentação oficial recomenda evitá-lo.
Que codec de compressão devo usar?
ZSTD(1) como base geral, Delta combinado com ZSTD para timestamps e inteiros sequenciais, e Gorilla para floats de sensores. Mede sempre com system.columns antes e depois.
Como reduzo o custo de armazenamento em ClickHouse?
TTL para apagar dados antigos, TTL com GROUP BY para reduzir a resolução do histórico, e políticas de armazenamento por níveis movendo os dados frios para S3.
Posts Relacionados
Continue explorando conteúdo similar que pode te interessar

O que é ClickHouse: características e instalação passo a passo (guia 2026)
Guia prático de ClickHouse para makers: o que é, por que é tão rápido, suas características chave e como instalá-lo em 5 minutos com curl ou Docker.

Alternativas a ClickHouse em 2026: comparativa honesta com tabela
DuckDB, StarRocks, Druid, Pinot, TimescaleDB, BigQuery e mais. Comparativa real de alternativas a ClickHouse com tabela e recomendação por caso de uso.

Bancos de dados OLAP: o que são, casos de uso reais e os principais fornecedores
O que é um banco de dados OLAP, em que se diferencia de OLTP, casos de uso práticos com SQL real e os principais fornecedores em 2026.