Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

ClickHouse no Node.js

Atualizado em: 17 de setembro de 2026

Rack de servidores processando fluxos de dados no Node.js

O ClickHouse no Node.js é uma combinação voltada a analytics em grande volume. ClickHouse é um banco colunar projetado para agregações rápidas sobre eventos, logs, métricas e dados históricos. A aplicação Node.js pode inserir lotes continuamente e executar consultas analíticas sem sobrecarregar o banco transacional principal.

O ganho depende de modelagem orientada às consultas. ClickHouse não substitui automaticamente PostgreSQL para transações, foreign keys e updates frequentes. Ele funciona melhor quando dados são inseridos de forma append-only, agrupados em lotes e consultados por filtros e agregações.

Neste guia, você aprenderá a usar o cliente oficial, criar tabelas MergeTree, inserir lotes, consultar com parâmetros, consumir resultados em streaming, usar async inserts, evitar perda de precisão e organizar retenção e observabilidade.

Cliente oficial JavaScript

A documentação de ClickHouse JS apresenta @clickhouse/client como o cliente oficial para Node.js. Ele é escrito em TypeScript, usa HTTP/HTTPS e suporta streaming, inserts, parâmetros, compressão e TLS.

O código-fonte está no repositório clickhouse-js.

Instalação

npm install @clickhouse/client

Criando o cliente

import { createClient } from '@clickhouse/client';

export const clickhouse = createClient({
  url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
  username: process.env.CLICKHOUSE_USER ?? 'default',
  password: process.env.CLICKHOUSE_PASSWORD ?? '',
  database: process.env.CLICKHOUSE_DATABASE ?? 'analytics',
  application: 'orders-analytics',
  request_timeout: 30_000,
  max_open_connections: 10
});

Use HTTPS e credenciais específicas em produção. Não coloque senha na URL registrada em logs.

Testando a conexão

const result = await clickhouse.ping();

if (!result.success) {
  throw result.error;
}

Ping ajuda no startup e health check, mas readiness não deve gerar carga excessiva nem bloquear por longos períodos.

Tabela MergeTree

await clickhouse.command({
  query: `
    CREATE TABLE IF NOT EXISTS order_events (
      occurred_at DateTime64(3, 'UTC'),
      event_id UUID,
      tenant_id UUID,
      order_id UUID,
      event_type LowCardinality(String),
      status LowCardinality(String),
      total_cents UInt64,
      payload JSON
    )
    ENGINE = MergeTree
    PARTITION BY toYYYYMM(occurred_at)
    ORDER BY (tenant_id, event_type, occurred_at, order_id)
  `
});

ORDER BY define a chave de ordenação física e influencia quais filtros conseguem pular blocos. Não escolha apenas uma coluna de ID aleatória.

PARTITION BY

Partições facilitam retenção e manutenção, mas não devem ser pequenas demais. Para eventos contínuos, uma partição mensal costuma ser um ponto inicial razoável. Avalie volume e frequência de remoção.

LowCardinality

Campos com poucos valores repetidos, como status e tipo, podem usar LowCardinality(String). ClickHouse armazena um dicionário e reduz espaço e custo em várias consultas.

Inserindo eventos

await clickhouse.insert({
  table: 'order_events',
  values: [
    {
      occurred_at: '2026-09-17 15:00:00.123',
      event_id: crypto.randomUUID(),
      tenant_id: tenantId,
      order_id: orderId,
      event_type: 'order.created',
      status: 'created',
      total_cents: '15990',
      payload: eventPayload
    }
  ],
  format: 'JSONEachRow'
});

O cliente aceita arrays e streams. Para tipos de 64 bits, use strings quando existe risco de ultrapassar Number.MAX_SAFE_INTEGER.

Por que inserir em lotes?

Muitos inserts pequenos criam partes demais no MergeTree. Agrupe eventos:

const buffer = [];

function enqueue(event) {
  buffer.push(event);
}

async function flush() {
  const batch = buffer.splice(0, 5000);
  if (!batch.length) return;

  await clickhouse.insert({
    table: 'order_events',
    values: batch,
    format: 'JSONEachRow'
  });
}

Defina flush por tamanho e tempo. Se o processo pode falhar, use uma fila durável antes do buffer.

Async inserts

ClickHouse pode agrupar inserts no servidor:

await clickhouse.insert({
  table: 'order_events',
  values: batch,
  format: 'JSONEachRow',
  clickhouse_settings: {
    async_insert: 1,
    wait_for_async_insert: 1
  }
});

wait_for_async_insert faz o cliente aguardar a confirmação do flush. Sem isso, a aplicação pode receber sucesso antes da persistência efetiva.

Consultando com parâmetros

const resultSet = await clickhouse.query({
  query: `
    SELECT
      toDate(occurred_at) AS day,
      count() AS orders,
      sum(total_cents) AS revenue_cents
    FROM order_events
    WHERE tenant_id = {tenantId: UUID}
      AND occurred_at >= {from: DateTime64(3)}
      AND occurred_at < {to: DateTime64(3)}
      AND event_type = 'order.created'
    GROUP BY day
    ORDER BY day
  `,
  query_params: {
    tenantId,
    from: '2026-09-01 00:00:00.000',
    to: '2026-10-01 00:00:00.000'
  },
  format: 'JSONEachRow'
});

const rows = await resultSet.json();

Use query parameters em vez de interpolar entrada do usuário.

Streaming de resultados

Para datasets grandes:

const resultSet = await clickhouse.query({
  query: 'SELECT * FROM order_events ORDER BY occurred_at',
  format: 'JSONEachRow'
});

for await (const chunk of resultSet.stream()) {
  for (const row of chunk) {
    await processRow(row.json());
  }
}

Consuma ou destrua o stream. Um resultado abandonado mantém a conexão ocupada até o timeout.

Cancelamento

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const result = await clickhouse.query({
    query: sql,
    abort_signal: controller.signal,
    format: 'JSONEachRow'
  });
  return await result.json();
} finally {
  clearTimeout(timeout);
}

Cancelar a conexão não garante que uma mutation ou insert não foi executado no servidor. Operações de escrita devem ser idempotentes.

Query ID

Cada consulta recebe query_id. Use-o para logs, cancelamento e investigação no system.query_log:

const result = await clickhouse.query({
  query: sql,
  query_id: crypto.randomUUID(),
  format: 'JSONEachRow'
});

Precisão numérica

ClickHouse retorna UInt64 e Int64 como strings em formatos JSON para evitar overflow. Não converta automaticamente para Number:

const total = BigInt(row.total_cents);

Decimals também devem ser tratados como string ou biblioteca decimal quando a precisão é importante.

Datas

Envie Date e DateTime no formato esperado. Defina UTC no banco e na aplicação. Não misture timestamps locais em eventos analíticos.

Materialized views

Agregações frequentes podem ser pré-calculadas:

CREATE TABLE daily_order_totals (
  day Date,
  tenant_id UUID,
  orders AggregateFunction(count),
  revenue AggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (tenant_id, day);

Materialized views exigem compreensão dos engines e funções de estado. Teste resultados antes de substituir consultas diretas.

Retenção com TTL

ALTER TABLE order_events
MODIFY TTL occurred_at + INTERVAL 365 DAY DELETE;

TTL remove dados de forma assíncrona durante merges. Não trate como exclusão imediata para requisitos legais urgentes.

Deduplicação

ClickHouse é orientado a append. Se o produtor pode reenviar, grave event_id e escolha uma estratégia:

  • deduplicar antes da inserção;
  • usar ReplacingMergeTree;
  • consultar com argMax;
  • aceitar duplicatas e corrigir na agregação.

Não suponha unicidade apenas porque a coluna é UUID.

ReplacingMergeTree

ENGINE = ReplacingMergeTree(version)
ORDER BY (tenant_id, event_id)

A substituição acontece durante merges, não necessariamente na hora. Consultas podem precisar de FINAL, que tem custo.

Ingestão por Kafka

Para volumes altos, ClickHouse pode consumir Kafka ou receber dados via ClickPipes. Em aplicações, Outbox e broker reduzem acoplamento entre transação e analytics.

Consulte Kafka com Node.js e Outbox Pattern no Node.js.

PostgreSQL e ClickHouse

PostgreSQL permanece como fonte transacional. ClickHouse recebe eventos ou replicação para analytics. Não faça a API de checkout depender de uma consulta analítica.

Veja PostgreSQL Pool no Node.js.

TLS

import { readFileSync } from 'node:fs';

const client = createClient({
  url: process.env.CLICKHOUSE_URL,
  username: process.env.CLICKHOUSE_USER,
  password: process.env.CLICKHOUSE_PASSWORD,
  tls: {
    ca_cert: readFileSync('./certs/ca.pem')
  }
});

Para mTLS, configure certificado e chave. Consulte mTLS no Node.js.

Usuários e permissões

Crie usuários diferentes para ingestão, leitura e migrations. O dashboard não precisa de permissão para alterar tabelas. Restrinja databases, tabelas e settings.

Keep-alive

O cliente mantém pool de conexões HTTP. Se houver socket hang up, verifique timeout do load balancer e idle_socket_ttl. Não aumente valores sem observar o servidor.

Compressão

const client = createClient({
  compression: {
    request: true,
    response: true
  }
});

Compressão reduz rede e aumenta CPU. Meça para lotes e resultados reais.

Observabilidade

Monitore:

  • latência de inserts;
  • linhas e bytes por lote;
  • partes ativas;
  • merges;
  • queries lentas;
  • memória e CPU;
  • erros de socket;
  • lag de ingestão;
  • query IDs.

Testes

Use ClickHouse real em container. Teste schema, inserts, precisão de UInt64, consultas parametrizadas, streaming, timeout, TTL e duplicatas.

Consulte Testcontainers no Node.js.

Erros comuns

  • Insert por linha: cria partes demais.
  • ORDER BY inadequado: filtros leem muitos blocos.
  • Number para UInt64: perde precisão.
  • Stream não consumido: conexão fica ocupada.
  • Dados transacionais no analytics: consistência fica confusa.
  • ReplacingMergeTree como unicidade imediata: duplicatas aparecem.
  • Query interpolada: risco de injection.
  • TTL como delete imediato: requisito não é cumprido.

Conclusão

O ClickHouse no Node.js oferece analytics rápido para eventos e grandes volumes. O cliente oficial suporta inserts em lote, streaming, parâmetros, compressão, TLS e controle de consultas.

Modele MergeTree a partir dos filtros, agrupe inserts, preserve precisão numérica e mantenha PostgreSQL como fonte transacional. Com retenção, permissões e observabilidade, ClickHouse se torna uma camada analítica eficiente sem comprometer o caminho crítico da aplicação.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita