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/clientCriando 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.




