O ClickHouse é muito rápido e muito literal. Faz o que lhe pediste, e várias das coisas que lhe podes pedir devolvem uma resposta que está errada de uma maneira sobre a qual nada te avisa: sem erro, sem nulo, sem resultado vazio. Apenas um número um bocadinho pequeno de mais, ou um bocadinho grande de mais, ou o texto "NaN" onde devia estar uma percentagem.

Estas são três com que nos deparámos ao construir analítica sobre ele. Cada uma esteve em produção antes de alguém reparar, e é isso que vale a pena registar.

1. Um estado agregado não é um número

O agregado diário guardava os visitantes únicos e as sessões como AggregateFunction(uniq, UUID), um sketch serializado, não uma contagem. É precisamente essa a ideia: podes fundir sketches de vários dias sem voltar às linhas em bruto.

A armadilha é que a coluna continua a parecer uma coluna. Daí decorrem duas coisas, e é a segunda que dói.

As linhas não são colapsadas enquanto não correr uma fusão em segundo plano. A mesma chave pode existir várias vezes, portanto ler a coluna e somar o que se recebe dá um valor abaixo do real, em silêncio e de forma plausível. Não há erro nenhum nem sintoma óbvio; o número está pura e simplesmente abaixo da verdade por uma quantidade que muda conforme a última fusão. Precisas do uniqMerge e de um GROUP BY, sempre.

E um cliente de SQL não consegue sequer apresentar o estado em bruto. O controlador JDBC do ClickHouse só consegue desserializar estados uniq sobre tipos inteiros nativos, portanto qualquer SELECT * sobre um estado uniq num UUID falha à cabeça:

Only native integer types are supported but we got: UUID

O que quer dizer que clicar na tabela no DataGrip ou no DBeaver (a coisa mais banal que alguém faz enquanto depura) lança uma exceção, e parece uma ligação partida em vez de uma decisão de modelação de dados que tomaste há meses.

A solução é uma vista que funde os estados e expõe valores UInt64 simples, para que a navegação funcione e os números estejam certos:

CREATE VIEW stats_daily_v AS
SELECT
    site_id, date, path,
    sum(page_views)            AS page_views,
    uniqMerge(unique_visitors) AS unique_visitors,
    uniqMerge(sessions)        AS sessions
FROM stats_daily
GROUP BY site_id, date, path;

O código da aplicação interrogava a tabela de base com o uniqMerge diretamente. A vista existia para que uma pessoa que abrisse a base de dados visse a verdade em vez de uma exceção.

Em setembro de 2026 removemos o agregado diário. Nessa altura já nada o lia: o painel interroga as tabelas em bruto, e o agregado era reconstruído em cada inserção sem um único leitor.

2. O avgIf sobre zero linhas devolve NaN, não NULL

Esta chegou a produção e lá ficou.

avgIf(duration_ms, is_bounce = 0)

Num período sem linhas coincidentes, o ClickHouse devolve o Float64 nan. Não NULL, que terias tratado, porque tratar nulos é coisa de que toda a gente se lembra. nan.

Duas consequências, e caem em cima de pessoas diferentes:

  • O painel apresentava o texto literal "NaN" onde devia estar uma taxa de rejeição e uma duração média. Feio, obviamente errado, comunicado depressa.
  • A API de consulta devolvia HTTP 500. O JSON não consegue codificar de todo um double não finito, portanto a serialização lançava uma exceção, e precisamente para os períodos que um cliente tem mais probabilidade de interrogar: hoje, antes do primeiro visitante da manhã, e qualquer intervalo sobre um sítio sossegado.

A segunda é muito pior do que a primeira e foi encontrada muito mais tarde. Um painel que diz "NaN" recebe um relatório de erro dentro de uma hora. Uma API que dá 500 só quando a resposta teria sido "não aconteceu nada" parece um ponto de acesso instável, e recebe um ciclo de novas tentativas em vez de um relatório de erro.

A solução é uma linha na fronteira de leitura: converter em zero tudo o que não seja finito, que é a mesma resposta que o caminho do denominador zero já dá, portanto os dois concordam:

var value = Convert.ToDouble(reader.GetValue(ordinal));
return double.IsFinite(value) ? value : 0;

O zero é honesto aqui porque a contagem de visitas ao lado dele também mostra zero. É o contexto que torna um zero verdadeiro em vez de um palpite.

3. Uma ReplacingMergeTree que é uma mentira sem FINAL

O tempo na página não é o intervalo entre duas páginas vistas. Um visitante que deixa um separador aberto em segundo plano não está a ler, e a última página de uma visita não tem página seguinte de que se possa subtrair. Portanto o rastreador reporta o tempo visível, e reporta-o mais do que uma vez: alguém que muda de separador e volta e muda outra vez escondeu mesmo a página duas vezes, e cada relato traz o total cumulativo corrente.

Somar isso contaria o mesmo minuto várias vezes e reportaria um tempo na página muito exagerado. Portanto a tabela é uma ReplacingMergeTree com chave por visita e página, versionada pelos milissegundos de interação: o relato mais longo ganha e os parciais anteriores fundem-se nele.

O que quer dizer que todas as leituras precisam de FINAL, e uma leitura que se esqueça dele vê as linhas parciais além da final. O número não é lixo; é plausível, apenas grande de mais. É esse o tipo de erro que sobrevive a uma revisão.

O custo honesto dessa chave, já que um desenho destes tem sempre um: um visitante que volta à mesma página mais tarde na mesma visita produz uma linha em vez de duas, e a leitura mais curta é descartada em vez de somada. Conta a menos num caso pouco comum, o que, numa métrica cuja única finalidade é não exagerar a atenção, é o sentido certo para errar.

O que as três têm em comum

Nenhuma delas lançou um erro no momento em que o engano foi cometido. Duas produziram números com um ar inteiramente razoável, e a terceira produziu uma falha noutro sítio completamente diferente, horas ou semanas depois.

A lição que tirámos mesmo: para cada agregado, escreve como seria uma resposta errada, e procura essa forma em vez de uma exceção. Um produto de analítica que está 8% abaixo é pior do que um que está em baixo, porque ninguém reporta um erro por causa de 8%.

Se quiseres ver como ficam os números quando estão certos, há uma demonstração em direto a correr numa loja verdadeira, sem conta nenhuma, e o glossário diz exatamente o que cada um conta.

Os três defeitos estavam por trás do produto Analytics, e nenhum deles se denunciou sozinho, o que é um argumento para confirmar um número por um segundo método em vez de confiar no seu ar plausível.