Startups e empresas de médio porte geralmente começam sua jornada de infraestrutura com um único banco relacional monolítico — mais comumente o PostgreSQL. Inicialmente, é uma escolha arquitetural brilhante. PostgreSQL lida com conformidade ACID robusta, foreign keys, constraints e joins com confiabilidade imbatível.
Ele gerencia usuários, senhas, sessões, logs financeiros e estado de configuração perfeitamente. Este é o domínio do OLTP (Online Transaction Processing).
Porém, conforme a plataforma começa a oferecer dashboards pesados, tracking de tendências históricas e filtragem multidimensional, os tempos de resposta começam a quebrar. As “soluções” usuais são aplicadas: adicionar read replicas, jogar RAM na instância RDS, construir materialized views e aplicar indexações agressivas.
Mas com bilhões de linhas, uma verdade fundamental se torna aparente: Você não pode otimizar um engine OLTP para workloads OLAP (Online Analytical Processing) exaustivos sem eventualmente derrubar o sistema ou queimar dinheiro.
O Problema: Engines Row-Based vs Queries Column-Based
No PostgreSQL, dados são armazenados em linhas. Se você quer computar SUM(total_revenue) em 50 milhões de registros de vendas, o engine do banco precisa carregar campos inteiramente desnecessários (como descrições de clientes, blobs de metadados e flags booleanos) do bloco de disco só para acessar o campo total_revenue.
Isso causa saturação massiva de I/O. Caches de memória são limpos para abrir espaço para scans sequenciais enormes, consequentemente desacelerando as tarefas transacionais core do app (como um usuário simplesmente fazendo login).
Os Números Não Mentem
Fizemos benchmark de queries idênticas em ambos engines usando um dataset real de varejo:
| Query | PostgreSQL (50M linhas) | ClickHouse (50M linhas) | Speedup |
|---|---|---|---|
SUM(revenue) | 4.2s | 0.08s | 52x |
GROUP BY category, month | 12.7s | 0.31s | 41x |
COUNT DISTINCT customers WHERE region = X | 8.9s | 0.19s | 47x |
Cálculo de percentil (P95) | 23.4s | 0.52s | 45x |
Esses não são benchmarks sintéticos. É um dataset real com cardinalidade realista, rodando em hardware comparável. A diferença é arquitetural, não configuracional.
Por Que o ClickHouse É Rápido (E O Que Ele Sacrifica)
ClickHouse atinge sua velocidade através de quatro decisões fundamentais de design:
1. Armazenamento Colunar
Ao invés de armazenar (customer_id, name, email, revenue, date) como blocos contíguos de linhas, ClickHouse armazena cada coluna independentemente. Quando você consulta SUM(revenue), ele lê apenas a coluna revenue do disco. Todo o resto é ignorado.
Em uma tabela com 30 colunas, isso significa ler aproximadamente 1/30 dos dados para agregações de coluna única. A economia de I/O é enorme.
2. Compressão Agressiva
Porque dados colunares tendem a ter padrões repetidos (datas se agrupam por dia, categorias se repetem em milhões de linhas), ClickHouse aplica codecs de compressão especializados por coluna:
- LZ4 para compressão geral (descompressão rápida)
- Delta encoding para timestamps e inteiros sequenciais
- Dictionary encoding para strings de baixa cardinalidade
Taxas de compressão de 5-10x são comuns. Um dataset de 500GB no PostgreSQL pode ocupar 60-80GB no ClickHouse, significando que mais dados cabem em memória e leituras de disco são proporcionalmente mais rápidas.
3. Execução Vetorizada de Queries
ClickHouse processa dados em lotes de 8.192 linhas (configurável) ao invés de linha por linha. Isso aproveita instruções CPU SIMD para realizar operações aritméticas em milhares de valores em um único ciclo de CPU.
PostgreSQL processa linhas individualmente através do seu executor. Para workloads analíticos tocando milhões de linhas, o overhead por linha é o custo dominante.
4. O Que o ClickHouse Abre Mão
Nada é grátis. ClickHouse atinge sua performance analítica deliberadamente não suportando:
- Transações ACID completas (modelo de consistência eventual)
- UPDATE/DELETE a nível de linha (mutations são operações async de background)
- JOINs complexos (joins locais funcionam, joins distribuídos são caros)
- Constraints de foreign key
É precisamente por isso que você não usa ClickHouse como banco de aplicação. É um engine analítico, não transacional.
A Abordagem Montinegro: O Split Estrutural
Para garantir reporting sub-segundo para nossos clientes de Business Intelligence independente da escala de dados, impomos estritamente uma arquitetura de dados bifurcada em nossas soluções SaaS:
PostgreSQL: A Fonte de Verdade
Utilizamos PostgreSQL exclusivamente para o heartbeat transacional. Sessões de usuário, configurações core, matrizes de permissão e lógica de aplicação. Sua memória é rigidamente amarrada à manutenção de tempos de resposta sub-10ms para as APIs REST do backend.
O Django ORM fala com PostgreSQL. Autenticação de usuário, gestão de tenant, billing de assinatura, feature flags — todas operações transacionais ficam aqui.
ClickHouse: A Âncora Analítica
Para todos os eventos de payload, logs pesados, agregações de métricas e telemetria dimensional, o stream de dados é canalizado para o ClickHouse.
Porque ClickHouse armazena dados por colunas, a mesma query SUM(total_revenue) em 50 milhões de linhas leva milissegundos. Ele pega apenas os bytes físicos daquela coluna específica no disco, comprimindo nativamente via encodings avançados, ignorando completamente o ruído.
Princípios de Design de Tabelas
Seguimos padrões estritos para design de tabelas ClickHouse:
CREATE TABLE analytics.sales_events
(
event_date Date,
tenant_id UInt32,
product_id UInt64,
category LowCardinality(String),
region LowCardinality(String),
revenue Decimal(18, 2),
quantity UInt32,
created_at DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (tenant_id, event_date, product_id)
TTL event_date + INTERVAL 3 YEAR
SETTINGS index_granularity = 8192;
Decisões-chave:
- Partition por mês mantém gestão de arquivos eficiente e habilita pruning rápido
- Order by tenant primeiro garante que queries multi-tenant escaneiem apenas dados relevantes
- LowCardinality para colunas string com <10.000 valores distintos economiza 5-10x memória
- TTL automaticamente descarta dados mais antigos que 3 anos sem intervenção manual
Celery & Redis: O Buffer de Ingestão
Para prevenir que o backend Django bloqueie enquanto escreve dados crus no ClickHouse, descarregamos o processamento pesado. Tarefas assíncronas são roteadas via Redis para workers Celery. Esses workers parseiam, limpam e fazem batch-insert de payloads de dados (como metadados processados de .pbip de integrações Power BI) no ClickHouse com eficiência suprema.
O padrão de inserção em batch é crítico. ClickHouse performa mal com inserts frequentes de linha única (cada insert cria um novo data part). Nós bufferizamos registros e fazemos flush em batches de 10.000-50.000 linhas, atingindo velocidades de inserção de 500.000+ linhas/segundo.
# Celery task para inserção batch no ClickHouse
@shared_task(bind=True, max_retries=3)
def ingest_analytics_batch(self, records: list[dict]):
if len(records) < BATCH_THRESHOLD:
# Re-enfileira com delay exponencial para acumular mais registros
raise self.retry(countdown=30)
client = clickhouse_connect.get_client(...)
client.insert(
'analytics.sales_events',
data=[tuple(r.values()) for r in records],
column_names=list(records[0].keys())
)
A Camada de Consumo de Dashboards
Nossas camadas visuais — atualmente via Apache Superset (com transição planejada para headless via Cube.dev) — são diretamente conectadas ao ClickHouse, bypassando o Django ORM e o core transacional PostgreSQL inteiramente.
O resultado? A interface SaaS (renderizada em Dart/Flutter) permanece incrivelmente responsiva. Dashboards de clientes podem drill down por bilhões de linhas agregadas em tempo real, enquanto usuários da aplicação continuam fazendo login e navegando configurações no lado PostgreSQL com zero impacto de latência.
Materialized Views: Inteligência Pré-Computada
Para dashboards que sempre mostram as mesmas agregações (revenue diário por região, usuários ativos semanais por tenant), usamos materialized views do ClickHouse para pré-computar resultados no momento da inserção:
CREATE MATERIALIZED VIEW analytics.daily_revenue_mv
ENGINE = SummingMergeTree()
ORDER BY (tenant_id, event_date, region)
AS SELECT
tenant_id,
event_date,
region,
sum(revenue) AS total_revenue,
count() AS transaction_count
FROM analytics.sales_events
GROUP BY tenant_id, event_date, region;
Essa view atualiza automaticamente conforme novos dados chegam. Consultar daily_revenue_mv para um ano de dados diários em todas as regiões retorna em menos de 10ms, independente de quantos eventos raw subjacentes existam.
Quando Fazer o Split
Nem toda aplicação precisa de uma arquitetura dual-database. O ponto de inflexão tipicamente ocorre quando:
- Contagem de linhas excede 10-50 milhões nas suas tabelas analíticas primárias
- Queries de dashboard levam >2 segundos e usuários começam a reclamar
- Read replicas não são suficientes porque o padrão de query é fundamentalmente errado para o storage engine
- Você está considerando desnormalização para acelerar queries PostgreSQL (sinal de que está lutando contra a arquitetura)
Se você é pre-seed com 100.000 linhas, PostgreSQL dá conta de tudo. Não faça over-engineering. Mas quando o crescimento atinge o ponto de inflexão, ter um plano de migração pronto é a diferença entre uma migração de fim de semana e um fire drill de três meses.
A Realidade Arquitetural
Arquitetura de dados não deveria ser uma restrição monolítica. Escala real exige uma admissão honesta de limitações: Deixe PostgreSQL fazer o que foi construído para fazer — gerenciar transações com confiabilidade. Deixe ClickHouse fazer o que foi construído para fazer — destruir queries analíticas massivas em milissegundos. E deixe uma camada de ingestão adequada (Celery + Redis) mediar entre os dois para que nenhum sistema comprometa o outro.