DevGuideTK
← Trilha Backend

Bancos colunares

Armazenamento orientado a colunas para escrita em volume e para consultas analíticas.

O código desta página foi executado nas cinco linguagens contra um ClickHouse 26. As medições do nível Conceito usaram o mesmo ClickHouse, um PostgreSQL 18 e um Cassandra 5.

Gravar em lotes e consultar agregados, em cada linguagem

O ClickHouse tem uma interface HTTP: o SQL vai na URL e os dados no corpo do pedido. Por isso, as cinco versões usam só a biblioteca padrão da linguagem, sem driver. Cada uma cria a tabela, grava 100 mil eventos em lotes de 10 mil, no formato TabSeparated, e consulta a soma por tipo.

const URL_CH = process.env.CLICKHOUSE_URL!; // ex.: http://clickhouse:8123
const cabecalhos = {
  "X-ClickHouse-User": "default",
  "X-ClickHouse-Key": process.env.CLICKHOUSE_PASSWORD!,
};
const TIPOS = ["visita", "carrinho", "compra", "busca"];

// Pela interface HTTP: o SQL vai na URL, os dados no corpo (sempre POST para gravar)
async function executar(sql: string, corpo = ""): Promise<string> {
  const resposta = await fetch(`${URL_CH}/?query=${encodeURIComponent(sql)}`,
    { method: "POST", headers: cabecalhos, body: corpo });
  if (!resposta.ok) throw new Error(await resposta.text());
  return resposta.text();
}

// Colunar gosta de lotes grandes: cada INSERT vira uma parte no disco
async function inserirEmLotes(total: number, tamanhoLote: number) {
  for (let inicio = 0; inicio < total; inicio += tamanhoLote) {
    const linhas: string[] = [];
    for (let i = inicio; i < Math.min(inicio + tamanhoLote, total); i++) {
      const ts = new Date(Date.UTC(2025, 0, 1) + i * 1000)
        .toISOString().slice(0, 19).replace("T", " ");
      linhas.push(`${ts}\t${TIPOS[i % 4]}\t${i % 5000}\t${((i % 1000) / 100).toFixed(2)}`);
    }
    await executar("INSERT INTO eventos_app FORMAT TabSeparated", linhas.join("\n") + "\n");
  }
}

await executar(`CREATE OR REPLACE TABLE eventos_app (ts DateTime, tipo LowCardinality(String),
  cliente_id UInt32, valor Decimal(12, 2)) ENGINE = MergeTree ORDER BY (tipo, ts)`);
await inserirEmLotes(100_000, 10_000);
console.log(await executar(
  "SELECT tipo, count(), sum(valor) FROM eventos_app " +
  "GROUP BY tipo ORDER BY tipo FORMAT TabSeparated"));

O resultado, nas cinco linguagens

As cinco versões gravaram os 100 mil eventos em 10 lotes e devolveram o mesmo resultado:

busca     25000   125250
carrinho  25000   124750
compra    25000   125000
visita    25000   124500

Os 10 lotes viraram 10 partes no disco, que o ClickHouse junta em segundo plano. No teste do nível Conceito, gravar linha a linha criou uma parte por linha e levou mil vezes mais.

O que muda de uma linguagem para outra

Cliente HTTPEscapar o SQL na URLData no formato do ClickHouse
TypeScriptfetchencodeURIComponenttoISOString() cortado e sem o T
Pythonurllib.requesturllib.parse.quotef"{data:%Y-%m-%d %H:%M:%S}"
Gonet/httpurl.QueryEscapetime.DateTime
Javajava.net.http.HttpClientURLEncoder.encodeDateTimeFormatter
C#HttpClientUri.EscapeDataStringyyyy-MM-dd HH:mm:ss

Dois cuidados que o teste mostrou: comandos que alteram dados precisam ir por POST, porque o ClickHouse trata pedidos GET como somente leitura, e números decimais precisam ser formatados com ponto, independentemente do idioma do sistema (por isso o CultureInfo.InvariantCulture no C#).

Para aplicações maiores, os drivers oficiais (@clickhouse/client, clickhouse-connect, clickhouse-go, o driver JDBC e o ClickHouse.Client) acrescentam pool de conexões, formatos binários e inserções assíncronas. A interface HTTP mostra o que eles fazem por baixo.

O esquema do teste

CREATE TABLE eventos (
  ts         DateTime,
  tipo       LowCardinality(String),  -- poucos valores distintos: guardado como dicionário
  cliente_id UInt32,
  valor      Decimal(12, 2)
)
ENGINE = MergeTree
ORDER BY (tipo, ts);                  -- a ordem física é o índice principal

Com a tabela ordenada por tipo e ts, o índice esparso do MergeTree permite à consulta das compras de março pular os blocos de outros tipos e de outras datas. No teste, ela respondeu em 9 ms, contra 128 ms da soma que percorre a tabela inteira.

Lista de revisão

Escolha

  • O volume e a frequência dos relatórios justificam um banco analítico, ou resumos e uma réplica bastam?
  • O banco de linha larga foi escolhido por escrita enorme e consultas pela chave, e não por moda?

Gravação

  • Os dados chegam ao banco analítico em lotes, e nunca linha a linha?
  • Há um caminho definido da fonte da verdade até o banco analítico (exportação, CDC ou eventos)?
  • Os dados são tratados como imutáveis, com correções como exceção?

Modelagem

  • A tabela analítica é larga, com a ordem pelas colunas mais filtradas?
  • Os tipos são os menores possíveis, com LowCardinality nas colunas de poucos valores?
  • No Cassandra, cada tabela atende a uma consulta, com partições de tamanho limitado?
  • Ninguém usa ALLOW FILTERING em produção, nem conta com erro de chave duplicada?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Alimentar o banco analítico em lotes

    Quero gravar os eventos da minha aplicação num banco analítico sem derrubá-lo com inserções pequenas.
    
    Pergunte qual linguagem, qual banco analítico e de onde vêm os eventos.
    
    Vamos em etapas:
    1. Juntar os eventos em lotes, por quantidade ou por tempo.
    2. Gravar cada lote com uma inserção só, com novas tentativas e sem duplicar o lote.
    3. Usar uma fila entre a aplicação e o banco analítico, se fizer sentido.
    4. Medir o número de partes e o atraso dos dados.
    5. Testar o que acontece se o banco analítico ficar fora do ar.
  • Migrar relatórios para o ClickHouse

    Quero mover relatórios pesados do PostgreSQL para o ClickHouse.
    
    Pergunte quais são os relatórios, as tabelas envolvidas e o volume.
    
    Me ajude a:
    1. Desenhar as tabelas largas no ClickHouse, com a ordem e os tipos certos.
    2. Escolher como manter os dados sincronizados com o PostgreSQL.
    3. Reescrever as consultas dos relatórios.
    4. Comparar os resultados nos dois bancos antes de trocar.
    5. Medir os tempos antes e depois.
  • Revisar o uso de um banco colunar

    Vou colar abaixo o código que grava e consulta um banco colunar (ClickHouse, BigQuery, Cassandra ou outro) e o esquema das tabelas. Revise.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. As gravações são feitas em lotes?
    2. Há atualizações ou exclusões frequentes?
    3. As tabelas estão ordenadas ou particionadas pelas consultas?
    4. Os tipos são os menores possíveis?
    5. No Cassandra: as consultas usam a chave de partição, e há ALLOW FILTERING?
    6. O banco está sendo usado para consultas de um registro só, que outro banco resolveria melhor?
    
    Para cada problema, descreva o efeito em produção e mostre a correção.
    
    Código e esquema:
    [cole aqui]