Pular para o conteúdo
Banco de Dados

ClickHouse: instalando na VPS e usando com Node.js

Um unicórnio de crina luminosa ao lado de colunas de dados recebendo pontos de luz, uma coruja com lupa sobre um gráfico de barras e um servidor em forma de torre de castelo

Olá meus Unicórnios! 🦄✨

Sabe quando você tem uma tabela de acessos no MySQL que cresce, cresce, e um dia o relatório do mês começa a demorar tanto que o painel dá timeout? 😅 Pois é. Esse é o problema que o ClickHouse resolve, e hoje eu vou do zero até o Node.js: instalar numa VPS Ubuntu limpinha, proteger com senha, criar uma tabela, jogar dez milhões de linhas nela e consultar tudo de um script.

Tudo o que aparece aqui saiu da VPS onde eu instalei, inclusive os tempos. Só troquei o IP por SEU_IP_AQUI e a senha por SUA_SENHA_AQUI.

🤔 O que é o ClickHouse (e quando ele não é a resposta)

O ClickHouse é um banco de dados colunar, de código aberto. No MySQL, cada linha mora junta no disco: para somar uma coluna, ele lê a linha inteira. No ClickHouse, cada coluna mora num arquivo próprio. Se o relatório só precisa de pais e tempo_ms, ele lê só essas duas, e ainda comprimidas.

Ele brilha em dado que chega muito e quase nunca muda: acessos de site, logs, eventos de aplicativo, leituras de sensor. E ele é péssimo no que o MySQL faz bem: cadastro de cliente com UPDATE linha a linha o dia inteiro. Não é um substituto, é um parceiro. 🤝

📦 Instalando na VPS

A VPS do artigo roda Ubuntu 24.04, com 2 vCPU e 5,7 GB de RAM. Entre nela pelo terminal (ssh seu_usuario@SEU_IP_AQUI) e vamos lá.

O ClickHouse não vem no repositório padrão do Ubuntu, então primeiro ensinamos o apt (o instalador de programas do Ubuntu) onde ele mora. Este bloco instala as ferramentas de download, baixa a chave que garante que o pacote é mesmo do ClickHouse e cadastra o repositório oficial:

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg

curl -fsSL https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key \
  | sudo gpg --dearmor -o /usr/share/keyrings/clickhouse-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg arch=amd64] https://packages.clickhouse.com/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/clickhouse.list

sudo apt-get update

O sudo na frente roda o comando como administrador e vai pedir a sua senha do Linux (enquanto você digita, nada aparece na tela; é normal). A barra invertida no fim da linha só diz "o comando continua na linha de baixo": copie o bloco inteiro.

Agora o ClickHouse em si: o servidor (clickhouse-server) e o programa de linha de comando para conversar com ele (clickhouse-client):

sudo apt-get install -y clickhouse-server clickhouse-client

Aqui saiu a versão 26.9.13.15.

🔑 A senha que a instalação não criou

Repare no detalhe cruel. 😬 Numa instalação interativa, o apt pergunta a senha do usuário default. Mas eu instalei por script, sem tela para responder, e ele simplesmente pulou a pergunta. A pasta de usuários ficou vazia, e o default ficou sem senha nenhuma. Se você instalou por automação (Ansible, script de provisionamento, cloud-init), confira isto antes de qualquer outra coisa.

O ClickHouse guarda a senha como um hash SHA-256, não como texto. Gere uma senha forte e o hash dela:

SENHA=$(openssl rand -hex 12)
echo "Anote a senha: $SENHA"
echo -n "$SENHA" | sha256sum

O -n do echo importa: sem ele, entra uma quebra de linha no fim e o hash passa a ser o de "senha + Enter". A senha certa daria Authentication failed para sempre. 🙃

Agora crie o arquivo de configuração. O nano é um editor de texto dentro do terminal:

sudo nano /etc/clickhouse-server/users.d/senha-default.xml

Cole o conteúdo abaixo, trocando o valor pelo hash que o sha256sum mostrou (só a parte das letras e números, sem o traço do fim). Para salvar: Ctrl+O, Enter e Ctrl+X para sair. No rodapé do nano, o ^ quer dizer Ctrl.

<clickhouse>
    <users>
        <default>
            <password remove="1" />
            <password_sha256_hex>COLE_AQUI_O_HASH</password_sha256_hex>
        </default>
    </users>
</clickhouse>

O <password remove="1" /> é a linha que ninguém lembra. O arquivo principal (users.xml) já declara um <password> vazio para o default, e o usuário não pode ter dois tipos de senha ao mesmo tempo. Essa linha apaga a vazia antes de a nova entrar.

Por fim, o dono do arquivo tem de ser o usuário clickhouse, senão o servidor não consegue lê-lo. Depois é só ligar:

sudo chown clickhouse:clickhouse /etc/clickhouse-server/users.d/senha-default.xml
sudo systemctl start clickhouse-server
sudo systemctl is-active clickhouse-server

A resposta tem de ser active. O serviço já fica configurado para subir sozinho quando a VPS reiniciar.

🚪 Em quais portas ele ficou ouvindo

Antes de pensar em firewall, veja o que o ClickHouse abriu. O ss -ltn lista as portas que estão esperando conexão:

sudo ss -ltn | grep -E "8123|9000"
LISTEN 0      4096       127.0.0.1:9000      0.0.0.0:*
LISTEN 0      4096       127.0.0.1:8123      0.0.0.0:*
LISTEN 0      4096           [::1]:9000         [::]:*
LISTEN 0      4096           [::1]:8123         [::]:*

Ótima notícia: por padrão ele escuta só em 127.0.0.1, ou seja, só a própria VPS consegue falar com ele. São duas portas:

PortaPara quêQuem usa
8123HTTPo Node.js, o curl e a interface Play no navegador
9000Protocolo nativoo clickhouse-client

Deixe assim. 🙏 Não troque para 0.0.0.0 só para acessar de casa: o jeito seguro é o túnel SSH, que eu mostro daqui a pouco. Banco de dados aberto para a internet é questão de horas até alguém tentar senha.

Testando a senha, primeiro sem ela e depois com ela:

clickhouse-client -q "SELECT 1"
clickhouse-client --password "SUA_SENHA_AQUI" -q "SELECT version()"
Code: 516. DB::Exception: Received from localhost:9000. DB::Exception: default: Authentication failed: password is incorrect, or there is no user with such name
26.9.13.15

Sem senha, recusado. Com senha, a versão. É exatamente isso que a gente queria ver. 🎉

🗄️ Criando a tabela de acessos

O exemplo do artigo é o de uma lojinha virtual que guarda cada visita: quando foi, qual página, de qual país, qual navegador e quanto tempo a página levou para carregar. Para não digitar a senha toda hora, deixe-a numa variável de ambiente que o clickhouse-client lê sozinho, e entre no modo interativo:

export CLICKHOUSE_PASSWORD="SUA_SENHA_AQUI"
clickhouse-client
CREATE DATABASE IF NOT EXISTS loja;

CREATE TABLE IF NOT EXISTS loja.acessos
(
    momento    DateTime,
    pagina     String,
    pais       LowCardinality(String),
    navegador  LowCardinality(String),
    tempo_ms   UInt32
)
ENGINE = MergeTree
ORDER BY (pagina, momento);

Três escolhas aí que não são enfeite:

  • ENGINE = MergeTree: é o motor de tabela padrão do ClickHouse para dado de verdade. Ele grava em blocos e vai juntando (o merge do nome) em segundo plano.
  • ORDER BY (pagina, momento): no ClickHouse, isto é a chave primária. Os dados ficam no disco nessa ordem, então filtrar por página (e depois por data) pula blocos inteiros sem ler. Escolha pelo que você mais filtra; mudar depois exige recriar a tabela.
  • LowCardinality(String): para coluna com poucos valores diferentes (meia dúzia de países, cinco navegadores). O ClickHouse guarda um dicionário e cada linha vira um número pequeno.

⚡ Dez milhões de linhas, de verdade

Agora a parte divertida. 🤩 Em vez de baixar um arquivo de exemplo, o próprio ClickHouse gera os dados: a função numbers(10000000) devolve dez milhões de linhas, e para cada uma eu sorteio um momento nos últimos 90 dias, uma de 500 páginas de produto, um país e um navegador. Saia do modo interativo com exit e salve este SQL num arquivo com o nano carga.sql:

INSERT INTO loja.acessos
SELECT
    now() - toIntervalSecond(rand() % (90 * 86400)),
    concat('/produto/', toString(rand() % 500)),
    ['BR', 'BR', 'BR', 'PT', 'US', 'AR'][rand() % 6 + 1],
    ['Chrome', 'Chrome', 'Safari', 'Firefox', 'Edge'][rand() % 5 + 1],
    50 + rand() % 2000
FROM numbers(10000000);

E rode, com o --time, que imprime quantos segundos a consulta levou:

clickhouse-client --time --queries-file carga.sql
clickhouse-client --time -q "SELECT count() FROM loja.acessos"
4.969
10000000
0.001

Dez milhões de linhas em menos de cinco segundos, numa VPS de 2 vCPU. E o count() respondeu em um milésimo, porque o MergeTree já sabe quantas linhas cada bloco tem e nem abre os dados. 🤯

Agora o relatório que dava timeout no MySQL: acessos e tempo médio de carregamento por país, varrendo a tabela inteira:

clickhouse-client --time --format PrettyCompact -q "
  SELECT pais, count() AS acessos, round(avg(tempo_ms)) AS tempo_medio_ms
  FROM loja.acessos
  GROUP BY pais
  ORDER BY acessos DESC"
   ┌─pais─┬─acessos─┬─tempo_medio_ms─┐
1. │ BR   │ 4999399 │           1049 │
2. │ PT   │ 1668035 │           1050 │
3. │ AR   │ 1666821 │           1050 │
4. │ US   │ 1665745 │           1049 │
   └──────┴─────────┴────────────────┘
0.079

0,079 segundo para agrupar dez milhões de linhas. E as cinco páginas mais vistas da última semana, com filtro de data:

   ┌─pagina───────┬─acessos─┐
1. │ /produto/26  │    1677 │
2. │ /produto/181 │    1672 │
3. │ /produto/459 │    1656 │
4. │ /produto/398 │    1654 │
5. │ /produto/406 │    1648 │
   └──────────────┴─────────┘
0.066

E quanto isso tudo ocupa? A tabela system.columns conta, coluna por coluna, o tamanho comprimido e o original:

SELECT
    formatReadableSize(sum(data_compressed_bytes))   AS no_disco,
    formatReadableSize(sum(data_uncompressed_bytes)) AS sem_compressao
FROM system.columns
WHERE database = 'loja' AND table = 'acessos';
   ┌─no_disco──┬─sem_compressao─┐
1. │ 64.42 MiB │ 284.04 MiB     │
   └───────────┴────────────────┘

Quatro vezes e pouco menor. É a vantagem de guardar cada coluna junto: uma coluna só de países, repetindo "BR" milhões de vezes, comprime que é uma beleza. 💜

🖥️ O ClickHouse no navegador, pelo túnel SSH

O ClickHouse traz uma interface web chamada Play, no endereço /play da porta 8123. Mas a porta só existe dentro da VPS... e é aí que entra o túnel SSH. Ele pega a porta 8123 do seu computador e a liga, por dentro da conexão SSH (que já é criptografada), na porta 8123 da VPS. No terminal do seu computador (não o da VPS):

ssh -N -L 8123:127.0.0.1:8123 seu_usuario@SEU_IP_AQUI

O -L monta o túnel e o -N diz "não abra um terminal, só segure o túnel". Ele parece travado depois da senha: é isso mesmo, deixe essa janela aberta. Agora abra http://localhost:8123/play no navegador, clique na chave 🔑 no canto superior direito, informe o usuário default e a sua senha, escreva a consulta e clique em Run:

A interface Play do ClickHouse mostrando a consulta de acessos por país e o resultado: BR com 4999403 acessos, PT, AR e US com cerca de 1,66 milhão cada, lidos em 0,05 segundo

Repare na linha cinza acima da tabela: Read 10.00 M rows, 50.00 MB. Dez milhões de linhas, mas só 50 MB lidos, porque ele abriu só as colunas pais e tempo_ms. É o colunar trabalhando na sua frente. (O BR tem quatro acessos a mais que no terminal porque, entre uma tela e outra, o script de Node que vem a seguir já tinha gravado as linhas dele.)

E se você esquecer a senha na chave, o Play mostra o mesmo erro que o terminal:

A interface Play do ClickHouse com o erro Code 516, Authentication failed: password is incorrect, ao rodar uma consulta sem informar a senha

Essa tela vermelha é a prova de que a senha está valendo também pelo HTTP, que é o caminho que o Node.js vai usar.

🟢 Usando o ClickHouse pelo Node.js

O cliente oficial é o @clickhouse/client. Ele fala HTTP com a porta 8123 e não precisa de nada compilado. Aqui usei a versão 1.24.1, com o Node 24:

npm install @clickhouse/client

O script inteiro, para salvar como acessos.js. Ele grava três acessos novos e depois pede as páginas mais vistas do Brasil nos últimos 7 dias:

// acessos.js: grava acessos no ClickHouse e lê um relatório
const { createClient } = require("@clickhouse/client");

// A senha vem do ambiente, nunca escrita aqui no código
const clickhouse = createClient({
    url: process.env.CLICKHOUSE_URL || "http://localhost:8123",
    username: "default",
    password: process.env.CLICKHOUSE_SENHA,
    database: "loja",
});

async function gravarAcessos(acessos) {
    await clickhouse.insert({
        table: "acessos",
        values: acessos,
        format: "JSONEachRow", // um objeto JavaScript por linha
    });
}

async function paginasMaisVistas(pais, dias) {
    const resultado = await clickhouse.query({
        // {pais: String} é um parâmetro: o valor nunca é colado no texto do SQL
        query: `
            SELECT pagina, count() AS acessos, round(avg(tempo_ms)) AS tempo_medio_ms
            FROM acessos
            WHERE pais = {pais: String}
              AND momento >= now() - INTERVAL {dias: UInt32} DAY
            GROUP BY pagina
            ORDER BY acessos DESC
            LIMIT 5
        `,
        query_params: { pais: pais, dias: dias },
        format: "JSONEachRow",
    });
    return await resultado.json();
}

async function main() {
    try {
        // Segundos desde 1970: não depende do fuso do servidor nem do formato da data
        const agora = Math.floor(Date.now() / 1000);
        await gravarAcessos([
            { momento: agora, pagina: "/produto/42", pais: "BR", navegador: "Chrome", tempo_ms: 830 },
            { momento: agora, pagina: "/produto/42", pais: "BR", navegador: "Safari", tempo_ms: 1210 },
            { momento: agora, pagina: "/carrinho", pais: "PT", navegador: "Firefox", tempo_ms: 410 },
        ]);
        console.log("3 acessos gravados.");

        const inicio = Date.now();
        const linhas = await paginasMaisVistas("BR", 7);
        console.log(`Páginas mais vistas do Brasil nos últimos 7 dias (${Date.now() - inicio} ms):`);
        console.table(linhas);
    } catch (erro) {
        console.error("Falei com o ClickHouse e deu erro:", erro.message);
        process.exitCode = 1;
    } finally {
        await clickhouse.close(); // fecha as conexões com o servidor antes de sair
    }
}

main();

Os pontos onde mora a armadilha, linha por linha:

  • password: process.env.CLICKHOUSE_SENHA: a senha vem do ambiente. Se você colar a senha no código, ela vai junto para o Git na primeira vez que alguém fizer git add ..
  • format: "JSONEachRow" no insert: você entrega um array de objetos JavaScript comuns, um por linha, com as chaves iguais aos nomes das colunas. Grave sempre em lote, várias linhas num insert só: cada insert vira um bloco novo no disco, e mil insert de uma linha viram mil blocos para o MergeTree juntar depois.
  • Math.floor(Date.now() / 1000): o momento vai como número de segundos desde 1970. Uma data montada como texto ("2026-10-09 12:25:59") é lida no fuso do servidor, que pode não ser o seu, e o relatório "de hoje" passa a começar três horas antes ou depois. O número de segundos não tem fuso nenhum: é o mesmo instante em qualquer lugar.
  • {pais: String} e query_params: o valor vai separado do SQL, e o próprio ClickHouse confere o tipo. Nunca monte a consulta colando texto ("WHERE pais = '" + pais + "'"): é a porta de entrada do SQL injection, igualzinho ao MySQL.
  • await resultado.json(): a consulta devolve um resultado que ainda não foi lido. Sem o .json() você recebe um objeto de resposta, não as linhas.
  • clickhouse.close() no finally: roda tanto no sucesso quanto no erro, e fecha as conexões com o servidor antes de o script sair.

Com o túnel SSH da seção anterior aberto, o localhost:8123 do script chega na VPS. Defina a senha e rode (no PowerShell do Windows, a primeira linha é $env:CLICKHOUSE_SENHA="SUA_SENHA_AQUI"):

export CLICKHOUSE_SENHA="SUA_SENHA_AQUI"
node acessos.js
3 acessos gravados.
Páginas mais vistas do Brasil nos últimos 7 dias (139 ms):
┌─────────┬────────────────┬─────────┬────────────────┐
│ (index) │ pagina         │ acessos │ tempo_medio_ms │
├─────────┼────────────────┼─────────┼────────────────┤
│ 0       │ '/produto/26'  │ 1141    │ 782            │
│ 1       │ '/produto/406' │ 1120    │ 1200           │
│ 2       │ '/produto/470' │ 1118    │ 1288           │
│ 3       │ '/produto/28'  │ 1105    │ 828            │
│ 4       │ '/produto/374' │ 1103    │ 1172           │
└─────────┴────────────────┴─────────┴────────────────┘

Os 139 ms incluem a ida e a volta pelo túnel SSH, do meu computador até a VPS. A consulta em si, lá dentro, é a casa dos décimos de segundo que a gente viu no terminal.

🚨 Quando a senha está errada

Agora o caminho da falha, que é o que mais acontece na vida real: o terminal novo, onde ninguém definiu a variável. Rodei com uma senha errada de propósito:

CLICKHOUSE_SENHA=errada node acessos.js

O cliente imprime antes um log longo dele, com o caminho dos arquivos internos. A última parte é a mensagem do nosso catch:

Falei com o ClickHouse e deu erro: default: Authentication failed: password is incorrect, or there is no user with such name

If you use ClickHouse Cloud, the password can be reset at https://clickhouse.cloud/
on the settings page for the corresponding service.

If you have installed ClickHouse and forgot password you can reset it in the configuration file.
The password for default user is typically located at /etc/clickhouse-server/users.d/default-password.xml
and deleting this file will reset the password.
See also /etc/clickhouse-server/users.xml on the server where ClickHouse is installed.

O script sai com código 1 (é o process.exitCode = 1), então quem chamou, seja um agendador ou outro script, fica sabendo que falhou. Um detalhe: a mensagem cita o arquivo default-password.xml, que é o nome que a instalação interativa usaria. No nosso caso a senha mora no senha-default.xml que criamos, então é ele que você confere.

🥊 ClickHouse contra MySQL, com os mesmos dados

"Tá, mas e se eu deixasse tudo no MySQL mesmo?" 🤔 Essa é a pergunta que todo mundo faz, então eu instalei o MySQL 8.0 na mesma VPS (sudo apt-get install -y mysql-server, que também ouve só em 127.0.0.1) e coloquei exatamente as mesmas linhas nos dois. Para ficar justo, subi o innodb_buffer_pool_size (a memória de trabalho do InnoDB) dos 128 MB de fábrica para 2 GB, num arquivo em /etc/mysql/mysql.conf.d/.

A tabela do MySQL é a mesma, com os tipos equivalentes e um índice que espelha o ORDER BY do ClickHouse:

CREATE TABLE loja.acessos (
    id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    momento    DATETIME NOT NULL,
    pagina     VARCHAR(100) NOT NULL,
    pais       CHAR(2) NOT NULL,
    navegador  VARCHAR(20) NOT NULL,
    tempo_ms   INT UNSIGNED NOT NULL,
    INDEX idx_pagina_momento (pagina, momento)
) ENGINE = InnoDB;

Exportei as dez milhões e quinze linhas do ClickHouse num arquivo TSV (texto separado por tabulação, 470 MB) e carreguei esse mesmo arquivo nos dois: no ClickHouse com INSERT ... FORMAT TSV, numa tabela vazia igual à original, e no MySQL com LOAD DATA INFILE, o jeito mais rápido que ele tem de engolir arquivo.

clickhouse-client -q "SELECT * FROM loja.acessos FORMAT TSV" > /var/lib/mysql-files/acessos.tsv

clickhouse-client -q "INSERT INTO loja.acessos_carga FORMAT TSV" < /var/lib/mysql-files/acessos.tsv

mysql -e "LOAD DATA INFILE '/var/lib/mysql-files/acessos.tsv' INTO TABLE loja.acessos
          FIELDS TERMINATED BY '\t' (momento, pagina, pais, navegador, tempo_ms)"

Cada consulta rodou três vezes e eu fiquei com a do meio (a mediana). O tempo é o relógio do terminal em volta do comando, então ele inclui abrir o cliente e conectar, nos dois lados igual. Antes de medir, conferi que os dois devolvem os mesmos números (o relatório por país deu 4.999.409 acessos do Brasil nos dois, e assim por diante).

📊 Os números, lado a lado

TesteClickHouseMySQLQuem ganha
Carga dos 10 milhões (mesmo TSV)6,20 s96,84 sClickHouse, 15x
Espaço em disco48,58 MiB1.131 MiB (599 de dados + 532 de índice)ClickHouse, 23x
Busca sem condição (agrupar por país)0,145 s3,581 sClickHouse, 25x
Busca com condição (Brasil, 7 dias)0,135 s1,909 sClickHouse, 14x
Busca pela chave (uma página, 7 dias)0,083 s0,009 sMySQL, 9x
Alterar uma página (20.287 linhas)0,074 s para voltar, 0,447 s para terminar0,131 sMySQL
Apagar uma página (cerca de 20 mil linhas)0,089 s para voltar, 1,139 s para terminar0,133 sMySQL
Apagar com o DELETE FROM leve0,138 s0,133 sempate

Um parêntese sobre o disco: lá em cima eu mostrei 64,42 MiB, e aqui são 48,58 MiB. Não é erro, são dois momentos diferentes. Os 64 MiB foram medidos logo depois da carga, com a tabela ainda em vários blocos. Antes da comparação eu rodei OPTIMIZE TABLE ... FINAL, que força o MergeTree a juntar tudo de uma vez, e blocos maiores comprimem melhor. Com o tempo ele faz essa junção sozinho, em segundo plano.

🔍 Por que a busca pela chave virou o jogo

A busca "com condição" filtra por pais, e o MySQL não tem índice nessa coluna. O EXPLAIN (que mostra como o MySQL pretende executar a consulta) entrega na hora:

+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | acessos | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 9732977 |     1.11 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+---------+----------+-------------+

type: ALL quer dizer "li a tabela inteira", quase dez milhões de linhas, linha por linha. O ClickHouse também lê tudo nesse caso, só que lê só as colunas pais, momento e tempo_ms, comprimidas. Por isso 0,135 s contra 1,9 s.

Já a busca por pagina e momento cai certinho no índice do MySQL:

+----+-------------+---------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
| id | select_type | table   | partitions | type  | possible_keys      | key                | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+---------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | acessos | NULL       | range | idx_pagina_momento | idx_pagina_momento | 407     | NULL | 1683 |   100.00 | Using index condition |
+----+-------------+---------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+

type: range e 1.683 linhas: ele foi direto nas linhas certas e respondeu em 9 milissegundos. O ClickHouse também usa a ordem da tabela, mas ele não aponta linhas: lê blocos inteiros de milhares delas (os granules), e isso tem um custo fixo. Para "me traga esta agulha", o índice B-tree do MySQL é imbatível. 🎯

⏳ A armadilha do UPDATE e do DELETE no ClickHouse

Se você só for ler um pedaço desta comparação, leia este. 🙏 No ClickHouse, alterar e apagar se escreve diferente:

ALTER TABLE loja.acessos_carga UPDATE tempo_ms = tempo_ms + 1 WHERE pagina = '/produto/26';
ALTER TABLE loja.acessos_carga DELETE WHERE pagina = '/produto/27';

E o comando volta em 0,07 segundo, mais rápido que o MySQL. Parece vitória, né? 😏 Não é. Isso é uma mutation: o comando só agenda o trabalho, e o ClickHouse reescreve os blocos afetados em segundo plano. Quem consultar logo em seguida ainda pode ver o dado velho. Para saber quando acabou de verdade, olhe a tabela system.mutations:

SELECT command, is_done
FROM system.mutations
WHERE database = 'loja' AND table = 'acessos_carga' AND is_done = 0;

Medindo até essa consulta voltar vazia, o UPDATE levou 0,45 s e o DELETE 1,14 s, contra 0,13 s do MySQL nos dois. Se o seu código grava e lê logo depois, ou espere a mutation, ou rode com SETTINGS mutations_sync = 1, que faz o comando só voltar no fim.

Para apagar existe ainda o DELETE leve, com a cara do SQL de sempre:

DELETE FROM loja.acessos_carga WHERE pagina = '/produto/30';

Ele marca as linhas como apagadas (elas somem das consultas na hora) e deixa a limpeza física para depois. Levou 0,138 s, empatado com o MySQL. Se for para apagar no ClickHouse, é esse que você quer.

⚖️ O veredito

Os números contam uma história bem clara. O ClickHouse ganhou com folga tudo o que é volume: carregou 15 vezes mais rápido, ocupa 23 vezes menos disco e somou a tabela inteira 25 vezes mais rápido. O MySQL ganhou tudo o que é pontual: achar poucas linhas pela chave (9 vezes mais rápido) e alterar ou apagar com o resultado valendo na hora.

Numa lojinha como a do exemplo, eles não brigam: o cadastro de produtos e pedidos fica no MySQL, e cada acesso vai também para o ClickHouse, que responde os relatórios. Um não substitui o outro. 💜

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Perguntas frequentes

O ClickHouse substitui o MySQL?
Não. Ele é feito para gravar muito e somar rápido (acessos, logs, eventos). Para cadastro que muda o tempo todo, com UPDATE linha a linha, o MySQL continua sendo a ferramenta certa.
Instalei sem responder a senha. O usuário default ficou sem senha?
Ficou. A instalação em modo não interativo não cria a senha. Crie um arquivo em /etc/clickhouse-server/users.d/ com password_sha256_hex e reinicie o serviço.
Por que dá "Authentication failed: password is incorrect"?
A senha enviada não confere com a do usuário. No Node, o caso mais comum é a variável de ambiente CLICKHOUSE_SENHA não ter sido definida no terminal onde o script roda.
Preciso abrir a porta 8123 no firewall para usar o Play?
Não. Deixe o ClickHouse ouvindo só em 127.0.0.1 e abra um túnel SSH (ssh -L 8123:127.0.0.1:8123). O navegador acessa localhost:8123/play e nada fica exposto na internet.
Quanto espaço 10 milhões de linhas ocupam no ClickHouse?
Na tabela de acessos do artigo, 64,42 MiB no disco, contra 284,04 MiB sem compressão. A compressão por coluna é o que faz a diferença.
O ClickHouse é mais rápido que o MySQL?
Para somar e agrupar, sim: com os mesmos 10 milhões de linhas, o agrupamento por país levou 0,145 s no ClickHouse e 3,58 s no MySQL, e a tabela ocupou 48,58 MiB contra 1.131 MiB. Para achar poucas linhas pelo índice, o MySQL ganhou: 0,009 s contra 0,083 s.
Por que o UPDATE do ClickHouse volta rápido e o dado não muda?
Porque ALTER TABLE ... UPDATE é uma mutation: o comando só agenda o trabalho. No artigo ele voltou em 0,07 s e só terminou em 0,45 s. Confira em system.mutations ou use SETTINGS mutations_sync = 1.

Leia também

Node.js

Avaliando o RAG com o Jev como juiz

Como dar nota ao seu RAG em Node.js: um gabarito, a busca do Qdrant medida sem IA e o Jev, da TypeSafe, dizendo se cada resposta é fiel e correta.