ClickHouse: banco de dados analítico para quem precisa de velocidade real

1. Arquitetura Colunar: O Segredo da Performance Analítica

1.1 Armazenamento orientado a colunas vs. linhas: impacto direto em consultas OLAP

ClickHouse é um banco de dados colunar projetado para processamento analítico online (OLAP). Enquanto bancos relacionais tradicionais armazenam dados linha por linha, o ClickHouse armazena cada coluna em blocos separados. Isso transforma consultas que varrem milhões de linhas: em vez de ler dados desnecessários de colunas não utilizadas, o banco lê apenas as colunas relevantes.

Exemplo prático: Em uma tabela com 100 colunas, uma consulta que soma vendas por mês lê apenas 2 colunas (valor e data), reduzindo o I/O em 98%.

1.2 Compressão agressiva por coluna e redução de I/O em scans massivos

Dados do mesmo tipo armazenados juntos permitem compressão extremamente eficiente. ClickHouse usa codecs como LZ4, ZSTD, Delta e DoubleDelta. Uma coluna de timestamps pode ser comprimida em 10-20% do tamanho original.

CREATE TABLE vendas (
    data DateTime CODEC(DoubleDelta, LZ4),
    produto_id UInt32 CODEC(ZSTD(3)),
    valor Float64 CODEC(Gorilla, LZ4)
) ENGINE = MergeTree()
ORDER BY data;

1.3 Particionamento físico e ordenação primária (ORDER BY) como índices implícitos

A cláusula ORDER BY na criação da tabela define a ordenação física dos dados no disco. Combinada com particionamento (PARTITION BY), cria um índice esparso que permite pular blocos inteiros que não contêm dados relevantes.

CREATE TABLE logs_acesso (
    timestamp DateTime,
    usuario_id UInt32,
    pagina String,
    duracao_segundos UInt16
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (usuario_id, timestamp);

2. Modelagem de Dados para Máximo Rendimento

2.1 Esquemas otimizados: tabelas MergeTree, materialized views e AggregatingMergeTree

O motor MergeTree é a base. Para agregações pré-calculadas, use AggregatingMergeTree com funções de estado:

CREATE MATERIALIZED VIEW vendas_diarias
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(dia)
ORDER BY (produto_id, dia)
AS SELECT
    toDate(data) AS dia,
    produto_id,
    sumState(valor) AS receita_total,
    countState() AS total_vendas
FROM vendas
GROUP BY dia, produto_id;

2.2 Escolha de chave de particionamento e ordenação para consultas típicas

  • Ordenação: coloque colunas de filtro frequentes primeiro (ex: (cliente_id, data))
  • Particionamento: use toYYYYMM(data) para séries temporais; evite partições com menos de 100MB
  • Amostragem: SAMPLE BY permite consultas aproximadas rápidas

2.3 Uso de tipos de dados específicos

  • DateTime64(3) para timestamps com milissegundos
  • LowCardinality(String) para colunas com até ~10.000 valores únicos (reduz memória e acelera joins)
  • SimpleAggregateFunction(sum, Float64) para agregados leves em tabelas de agregação
CREATE TABLE eventos (
    timestamp DateTime64(3),
    tipo_evento LowCardinality(String),
    usuario_id UInt32,
    metrica Float32
) ENGINE = MergeTree()
ORDER BY (tipo_evento, timestamp);

3. Ingestão em Escala: Streaming e Carga em Lote

3.1 Inserção assíncrona e batch insert: evitando micro-partições ineficientes

Inserções individuais criam partes pequenas que degradam performance. Use lotes de 10.000-100.000 linhas:

INSERT INTO vendas VALUES
    (now(), 1, 100.50),
    (now(), 2, 250.00),
    ... (10.000 linhas)

Ative async_insert=1 no cliente para buffer automático.

3.2 Integração com Kafka (Kafka Engine) e filas de mensagens em tempo real

CREATE TABLE kafka_queue (
    mensagem String
) ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'localhost:9092',
    kafka_topic_list = 'eventos',
    kafka_group_name = 'clickhouse_consumer',
    kafka_format = 'JSONEachRow';

CREATE MATERIALIZED VIEW kafka_to_vendas
TO vendas AS
SELECT
    JSONExtractString(mensagem, 'produto') AS produto,
    JSONExtractFloat(mensagem, 'valor') AS valor,
    now() AS data
FROM kafka_queue;

3.3 Tabelas buffer e estratégias de flush para alta taxa de escrita

Tabelas Buffer acumulam inserções e fazem flush periódico para a tabela principal:

CREATE TABLE vendas_buffer AS vendas
ENGINE = Buffer(vendas, 16, 10, 100, 10000, 1000000, 10000000, 100000000, 1000000000);

4. Execução de Consultas e Otimização de Queries

4.1 Pipeline de execução vetorizada e processamento SIMD

ClickHouse processa dados em vetores de 1024 elementos por vez, utilizando instruções SIMD da CPU. Isso permite processar bilhões de linhas por segundo em hardware commodity.

4.2 Uso de funções analíticas sem subqueries

SELECT
    produto_id,
    sum(valor) AS receita,
    quantile(0.95)(valor) AS p95_valor,
    retention(
        data >= '2024-01-01',
        data >= '2024-02-01'
    ) AS retencao_mensal
FROM vendas
GROUP BY produto_id;

4.3 Identificação de gargalos com EXPLAIN e system.query_log

EXPLAIN SYNTAX
SELECT count() FROM vendas WHERE data > '2024-01-01';

-- Ver queries lentas:
SELECT
    query,
    query_duration_ms,
    read_rows,
    read_bytes,
    memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query_duration_ms > 1000
ORDER BY query_duration_ms DESC;

5. Estratégias de Replicação e Alta Disponibilidade

5.1 ReplicatedMergeTree: replicação síncrona sem dependência de coordenação externa

CREATE TABLE vendas_replicado ON CLUSTER cluster_3_nodes (
    data DateTime,
    produto_id UInt32,
    valor Float64
) ENGINE = ReplicatedMergeTree(
    '/clickhouse/tables/{shard}/vendas',
    '{replica}'
)
ORDER BY data;

5.2 Failover automático e consistência eventual em clusters multi-nó

O ClickHouse usa ZooKeeper ou ClickHouse Keeper para coordenação. Se um nó falha, as réplicas continuam servindo consultas. A consistência é eventual, mas suficiente para workloads analíticos.

5.3 Backup e restore com tabelas Freeze

-- Criar snapshot
ALTER TABLE vendas FREEZE;

-- Backup físico
cp -r /var/lib/clickhouse/shadow/ /backup/clickhouse_$(date +%Y%m%d)

6. ClickHouse na Prática: Casos de Uso e Armadilhas Comuns

6.1 Telemetria e observabilidade: logs, métricas e traces com alto throughput

Empresas como Cloudflare e Uber processam petabytes de logs diariamente com ClickHouse. A chave é usar ORDER BY (timestamp, host) e particionamento por hora.

6.2 Análise de séries temporais e dashboards em tempo real (Grafana + ClickHouse)

Grafana tem suporte nativo ao ClickHouse. Configure datasources e use funções como toStartOfInterval() para agregações temporais:

SELECT
    toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS minute,
    avg(latencia_ms) AS latencia_media,
    quantile(0.99)(latencia_ms) AS p99
FROM metricas
WHERE timestamp > now() - INTERVAL 1 HOUR
GROUP BY minute
ORDER BY minute;

6.3 Erros frequentes: joins desnecessários, cardinalidade mal escolhida e falta de TTL

  • Joins: Use dictGet() para lookup tables em vez de JOINs
  • Cardinalidade: Evite ORDER BY com colunas de alta cardinalidade (ex: UUID)
  • TTL: Configure expiração automática para dados antigos:
ALTER TABLE logs_acesso MODIFY TTL timestamp + INTERVAL 90 DAY;

7. Comparação com Alternativas e Limitações Reais

7.1 ClickHouse vs. Druid, Pinot e TimescaleDB

Característica ClickHouse Druid Pinot TimescaleDB
Inserção em tempo real Excelente (Kafka Engine) Excelente Excelente Bom
Consultas complexas Excelente Médio Médio Bom
Compressão Excelente (5-15x) Bom (3-8x) Bom (3-8x) Médio (2-4x)
Facilidade de operação Médio (ZooKeeper) Complexo Complexo Fácil

7.2 Limitações de UPDATE/DELETE e transações

ClickHouse não suporta transações ACID tradicionais. Para atualizações, use ALTER TABLE ... UPDATE (mutação pesada) ou recrie partições. Prefira modelos de dados append-only.

7.3 Quando NÃO usar ClickHouse

  • Workloads OLTP com muitas atualizações e transações curtas
  • Joins complexos entre muitas tabelas grandes
  • Dados altamente mutáveis (ex: carrinhos de compras)
  • Consultas que exigem consistência imediata após escrita

Referências